Docs / Map view / Reading the camera

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.

need it now
mapViewState.cameraPosition — readable at any time. The visible area is cameraPosition.visibleRegion.bounds.
follow changes
onCameraMove / onCameraMoveEnd — fire during the move as well. Use these to keep UI in step.
extension module
register as an overlay controller and receive onCameraChanged. No fighting over the map controller's single-slot listeners.
Platform
read from state
val camera = mapViewState.cameraPosition
val bounds = camera.visibleRegion?.bounds

// 変化を追う
MapView(
    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.

the push flow
地図 SDK のカメライベント
        ↓  プロバイダが SDK の生値を読み、統一ズームへ変換し、visibleRegion を載せる
   state.updateCameraPosition(camera)
        ↓
   ├─ mapViewState.cameraPosition       (いつでも読める)
   ├─ onCameraMove / onCameraMoveEnd    (アプリのコールバック)
   └─ 登録済みオーバーレイの onCameraChanged (拡張モジュール)
All three SDKs work this way. The two native ones never had a getter on the controller; react-sdk did, and it was removed on 2026-08-06.

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.

What one call does
Read center / zoom / bearing / tilt from the SDK, convert to the unified zoom through the provider's ZoomAltitudeConverter, then unproject the four screen corners to build visibleRegion. That last step is the expensive one — this is not a plain getter.

Measured on the MapLibre marker sample in Chromium, counting getCameraPosition() calls during one drag (a pan of roughly 20 frames).

initial load
one drag
with the getter
5
86
after removal
3
30

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.

What the video shows · Android + MapLibreKotlin · Jetpack Compose
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}")
    }
}
Sample video · reading values while the map moves
Video not shot yetLatitude, longitude and zoom updating on screen from the callback while the map is dragged. The difference between pull and push shows up in how that tracking behaves.

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.

in practice
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.
where it could matter
Drawing that must be locked to the same frame mid-drag. That belongs on the map SDK's own render loop, which is the escape hatch below rather than the unified API.

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.

drop to native
const holder = mapViewState.getMapViewHolder();
const map = holder?.map as maplibregl.Map | undefined;
const liveZoom = map?.getZoom();
Each provider does have code that converts the SDK's raw values into the unified zoom — that is what produces the value carried on camera events. It stays inside the implementation and is not on the public interface, on all three platforms (android-sdk keeps a private getMapCameraPosition(), ios-sdk a private currentCameraPosition(from:)).

Related pages