MapView lifecycle and events
A map is a view plus a state object. The state (mapViewState) is your handle on the camera, the design and every operation; the view just goes into the platform UI tree. Initialisation walks a shared set of stages, and load completion and user input arrive as events.
01 · mapViewState
mapViewState is the state object for one map. Implementations differ per provider, but the exposed members are the same, so your code only ever sees the shared interface.
The state outlives the view. When the view is rebuilt, camera position and design remain on the state, so the map comes back as it was.
// Create the state with remember*, so the same map survives a rotation
val mapViewState =
rememberGoogleMapViewState(
mapDesign = GoogleMapDesign.Normal,
cameraPosition = initCameraPosition,
)
GoogleMapView(state = mapViewState, modifier = Modifier.fillMaxSize())// An ObservableObject: hold it on the view with @StateObject
@StateObject private var mapLibreState = MapLibreViewState(
mapDesignType: MapLibreDesign.OsmBright,
cameraPosition: viewModel.initCameraPosition
)
MapLibreMapView(state: mapLibreState) { MapViewContent() }const [mapViewState, setMapViewState] =
useState<MapViewStateInterface<MapDesignTypeInterface<unknown>> | null>(null);
// MapViewContainer is the sample app's provider-switching wrapper — it is not part of the SDK
<MapViewContainer initialCamera={INIT_CAMERA} onStateReady={setMapViewState}>
<Markers states={markerStates} />
</MapViewContainer>02 · Initialisation lifecycle
Every provider walks the same stages: load the SDK, create the view, create the map instance, finish drawing tiles. Because the stages are one shared InitState, you do not have to memorise per-provider rules about when the map is safe to touch.
Camera moves and overlay registration are accepted from MapCreated onward and queued internally. To wait for a fully drawn map, use onMapLoaded.
Load the map SDK — a script tag on the web, platform init on mobile. Failure moves to Failed.
Wrap the native map instance in a holder; SDK-specific types stay behind it.
Wrap the holder in a controller that exposes the shared API and wires events, then attach it to the state.
Surviving rotation and rebuilds
On Android the state is kept through rememberSaveable, so camera and design are restored after rotation or a configuration change; the map view itself is reused rather than destroyed.
When it is destroyed
On real teardown the controller releases its overlay controllers, tile-server routes and coroutine scope together. Switching providers cleans up the previous map through the same path.
var ready by remember { mutableStateOf(false) }
MapLibreMapView(
state = mapViewState,
// Called once, when the tiles have finished drawing
onMapLoaded = { state ->
ready = true
state.fitBounds(routeBounds, padding = 48)
},
) {
// Declarations here are not lost before MapCreated — they are queued internally
Marker(markerState)
}
if (!ready) {
// Your own loading overlay, if you want one. The map does not need blocking
Box(Modifier.fillMaxSize()) { CircularProgressIndicator() }
}03 · Events
The handlers you pass to the view are identical across the three platforms, and so are their types: load completion hands you the state object, taps a coordinate, camera changes a camera position.
GoogleMapView(
state = mapViewState,
onMapLoaded = { state -> viewModel.onMapLoaded(state) },
onMapClick = { point -> viewModel.onMapClick(point) },
onCameraMove = { camera -> viewModel.onCameraChanged(camera) },
onCameraMoveEnd = { camera -> viewModel.onCameraSettled(camera) },
) { /* markers, overlays */ }MapLibreMapView(
state: mapLibreState,
onMapLoaded: { state in viewModel.onMapLoaded(state) },
onCameraMoveStart: viewModel.onMapCameraMoveStart,
onCameraMove: viewModel.onCameraChanged,
onCameraMoveEnd: viewModel.onMapCameraMoveEnd
) {
MapViewContent()
}<MapViewContainer
initialCamera={INIT_CAMERA}
onMapClick={() => setSelected(null)}
onCameraMove={setCameraPosition}
>
<Markers states={markerStates} />
</MapViewContainer>Camera events also update cameraPosition on the state, so the latest camera is readable even without a handler. To avoid refetching mid-gesture, use only onCameraMoveEnd.
04 · The escape hatch to native
When the shared API is not enough, there is a way down to the native map instance. You rarely need it — it exists for that one provider-specific feature.
A holder around the native map
The holder carries the platform view and the map instance. SDK-specific code stays behind it and never leaks into shared code.
Coordinate and screen conversion
Convert between geographic coordinates and screen pixels — needed when you overlay your own UI on the map.
// Take it out only when you genuinely need the native API val holder = mapViewState.getMapViewHolder() val nativeMap = holder?.map // GoogleMap / MapLibreMap / ... val offset = holder?.toScreenOffset(point)
if let holder = mapViewState.getMapViewHolder() {
let nativeMap = holder.map
let offset = holder.toScreenOffset(position: point)
}const holder = mapViewState.getMapViewHolder(); const nativeMap = holder?.map; // google.maps.Map / maplibregl.Map / ...
Code that uses the holder is provider-specific. Keep a clear line between what stays shared and where you deliberately use a native API.