MapView のライフサイクルとイベント
地図はビューと状態オブジェクトの2つで扱います。状態(mapViewState)がカメラ・デザイン・操作の窓口で、ビューは各プラットフォームのUIツリーに置くだけ。初期化は共通の段階を踏み、読み込み完了とユーザー操作はイベントで受け取ります。
01 · mapViewState
mapViewState は地図1面に対応する状態オブジェクトです。プロバイダごとの実装はありますが、公開されているメンバーは共通で、アプリのコードは共通インターフェースだけを見ていれば足ります。
状態はビューより長く生きます。ビューが作り直されても、カメラ位置やデザインは状態側に残るため、地図はそのまま復元されます。
// 状態は remember* で作る。画面回転をまたいでも同じ地図が復元される
val mapViewState =
rememberGoogleMapViewState(
mapDesign = GoogleMapDesign.Normal,
cameraPosition = initCameraPosition,
)
GoogleMapView(state = mapViewState, modifier = Modifier.fillMaxSize())// ObservableObject。@StateObject で View に持たせる
@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 はサンプルアプリのプロバイダ切替ラッパー(SDK 同梱ではありません)
<MapViewContainer provider={provider} cameraPosition={INIT_CAMERA} onStateReady={setMapViewState}>
<Markers states={markerStates} />
</MapViewContainer>02 · 初期化のライフサイクル
地図の初期化はどのプロバイダでも同じ段階をたどります。SDKの読み込み、ビューの生成、地図インスタンスの生成、そしてタイルの描画完了。段階が InitState として共通化されているので、「まだ操作してはいけない時期」をプロバイダごとに覚える必要がありません。
カメラ操作やオーバーレイの追加は MapCreated 以降で受け付けられ、内部でキューに入ります。読み込み完了を待ってから何かしたい場合は onMapLoaded を使います。
画面回転・再生成をまたぐ
Android では状態が rememberSaveable で保存され、画面回転や設定変更のあともカメラ位置とデザインが復元されます。構成変更のときは地図ビュー自体を破棄せず再利用します。
破棄されるとき
本当の破棄(画面離脱)ではコントローラが持つオーバーレイ管理やタイルサーバのルート、コルーチンスコープをまとめて解放します。プロバイダを切り替えたときも同じ経路で古い地図が片付けられます。
var ready by remember { mutableStateOf(false) }
MapLibreMapView(
state = mapViewState,
// タイルまで描き終わったところで 1 回だけ呼ばれる
onMapLoaded = { state ->
ready = true
state.fitBounds(routeBounds, padding = 48)
},
) {
// ここでの宣言は MapCreated 以前でも失われない。内部でキューに入る
Marker(markerState)
}
if (!ready) {
// 読み込み中に自前の表示を重ねたい場合。地図の操作を止める必要はない
Box(Modifier.fillMaxSize()) { CircularProgressIndicator() }
}03 · イベント
ビューに渡すハンドラは3プラットフォームで同じ構成です。型も共通で、読み込み完了は状態オブジェクト、タップは座標、カメラ変化はカメラ位置を受け取ります。
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
provider={provider}
cameraPosition={INIT_CAMERA}
onMapClick={() => setSelected(null)}
onCameraMove={setCameraPosition}
>
<Markers states={markerStates} />
</MapViewContainer>カメラ系のイベントは状態オブジェクトの cameraPosition も同時に更新するため、ハンドラを付けなくても最新のカメラは常に読み出せます。移動中の再取得を避けたい場合は onCameraMoveEnd だけを使ってください。
04 · ネイティブへの逃げ道
共通APIで足りないときのために、ネイティブの地図インスタンスへ降りる口を用意しています。ふだんは使いませんが、プロバイダ固有の機能を1か所だけ使いたい場合の逃げ道です。
ネイティブ地図を包むホルダー
ホルダーはプラットフォームのビューと地図インスタンスの2つを持ちます。SDK固有のコードはここから先に閉じ込め、共通コードには漏らさない構成です。
座標と画面位置の変換
地理座標と画面上のピクセル位置を相互に変換できます。地図の上に独自のUIを重ねるときに使います。
// どうしてもネイティブ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 / ...
ホルダーを使ったコードはプロバイダに依存します。共通のまま保ちたい部分と、あえて固有APIを使う部分を意識して分けてください。