Reading the camera position
MapConductor's controllers have no getCameraPosition() — on any of the three platforms. It is the first method people look for, so this page says what to use instead and why the getter does not exist.
In one line: the camera is pushed to you, not pulled by you. The map SDK already knows when it changed — you only have to receive it.
01 · Three routes
Pick by what you are doing. All three hand you the same MapCameraPosition, with the visible area on visibleRegion.
mapViewState.cameraPosition — readable at any time. The visible area is cameraPosition.visibleRegion.bounds.onCameraMove / onCameraMoveEnd — fire during the move as well. Use these to keep UI in step.onCameraChanged. No fighting over the map controller's single-slot listeners.val camera = mapViewState.cameraPosition
val bounds = camera.visibleRegion?.bounds
// 変化を追う
MapView(
state = mapViewState,
onCameraMoveEnd = { camera -> reload(camera.visibleRegion?.bounds) },
)let camera = mapViewState.cameraPosition
let bounds = camera.visibleRegion?.bounds
// 変化を追う
MapLibreMapView(state: mapViewState, onCameraMoveEnd: { camera in
reload(camera.visibleRegion?.bounds)
})const camera = mapViewState.cameraPosition;
const bounds = camera.visibleRegion?.bounds;
// 変化を追う
<MapLibreMapView
state={mapViewState}
onCameraMoveEnd={(camera) => reload(camera.visibleRegion?.bounds)}
/>02 · Why there is no getter
MapConductor sits on declarative UI on all three platforms (Compose, SwiftUI, React). In that model there is one place that holds what is currently true, and the UI is a projection of it. The camera is state, so it lives on the state object.
Adding a getter to the controller creates a second source for the same value: one from state, one read straight off the SDK. They ought to agree, so all you have added is room to disagree. That is exactly what happened — before the removal the camera was built twice in the same frame: once for the SDK event, once again on every re-render.
地図 SDK のカメライベント
↓ プロバイダが SDK の生値を読み、統一ズームへ変換し、visibleRegion を載せる
state.updateCameraPosition(camera)
↓
├─ mapViewState.cameraPosition (いつでも読める)
├─ onCameraMove / onCameraMoveEnd (アプリのコールバック)
└─ 登録済みオーバーレイの onCameraChanged (拡張モジュール)03 · What a pull costs
Design arguments alone invite "surely one getter is fine", so here are the measurements. Building the camera once is not cheap.
Measured on the MapLibre marker sample in Chromium, counting getCameraPosition() calls during one drag (a pan of roughly 20 frames).
A ~65% reduction, and every removed call was a rebuild. The camera was assembled once for the SDK event and pushed to state, then every re-render assembled the same value again. The call sat in all 13 provider views, each throwing away a value that had just been pushed.
Idle it was 0 calls. Nothing was spinning — this is the kind of cost that only lands while the user is interacting, i.e. piled on at the moment the map is already busiest.
var camera by remember { mutableStateOf<MapCameraPosition?>(null) }
MapLibreMapView(
state = mapViewState,
modifier = Modifier.weight(1f),
// Fires throughout the gesture — keep this handler cheap
onCameraMove = { position -> camera = position },
// Once, when the map settles. Fetching and redrawing belong here
onCameraMoveEnd = { position -> viewModel.onCameraSettled(position) },
)
camera?.let { position ->
Text("lat %.5f".format(position.position.latitude))
Text("lng %.5f".format(position.position.longitude))
Text("zoom %.2f".format(position.zoom))
// Read the visible region off the value you were handed; do not rebuild it
position.visibleRegion?.bounds?.let { bounds ->
Text("SW ${bounds.southWest} NE ${bounds.northEast}")
}
}04 · The one-frame lag
mapViewState.cameraPosition is the last value pushed, so in principle it can be one frame behind the SDK. That is the one substantive reason to want a getter, so here it is stated plainly.
onCameraMove fires every frame during the move, so state is never more than the previous frame behind. Rendering, data fetching and range checks all look identical — the full 41-test browser suite passed unchanged after the removal.05 · When you really need the raw value
If state is not enough — you need to sync with the map SDK's own render loop, say — do not route around the unified API: take the native map instance out of MapViewHolder. It is provider-specific code, but it is also the fastest and most exact path.
const holder = mapViewState.getMapViewHolder(); const map = holder?.map as maplibregl.Map | undefined; const liveZoom = map?.getZoom();