ドキュメント / 地図ビュー / MapView と状態

MapView のライフサイクルとイベント

地図はビューと状態オブジェクトの2つで扱います。状態(mapViewState)がカメラ・デザイン・操作の窓口で、ビューは各プラットフォームのUIツリーに置くだけ。初期化は共通の段階を踏み、読み込み完了とユーザー操作はイベントで受け取ります。

ANDROID
MapViewStateInterface
iOS
MapViewStateProtocol
REACT
MapViewStateInterface
プラットフォーム

01 · mapViewState

mapViewState は地図1面に対応する状態オブジェクトです。プロバイダごとの実装はありますが、公開されているメンバーは共通で、アプリのコードは共通インターフェースだけを見ていれば足ります。

状態はビューより長く生きます。ビューが作り直されても、カメラ位置やデザインは状態側に残るため、地図はそのまま復元されます。

メンバー
説明
id
状態の識別子。ビューの再生成をまたいで同じ地図を指し続けます。
cameraPosition
現在のカメラ位置。表示領域(visibleRegion)もここから取れます。
mapDesignType
現在の地図デザイン。代入すると即座に切り替わります。
moveCameraTo()
カメラを移動します。時間を渡せばアニメーションします。
fitBounds()
指定した範囲が収まるようにカメラを合わせます。3プラットフォームとも状態オブジェクトのメソッドで、内部でコントローラに委譲します。
getMapViewHolder()
ネイティブの地図インスタンスを包んだホルダーを返します。
MapViewState · Jetpack Compose
// 状態は remember* で作る。画面回転をまたいでも同じ地図が復元される
val mapViewState =
    rememberGoogleMapViewState(
        mapDesign = GoogleMapDesign.Normal,
        cameraPosition = initCameraPosition,
    )

GoogleMapView(state = mapViewState, modifier = Modifier.fillMaxSize())

02 · 初期化のライフサイクル

地図の初期化はどのプロバイダでも同じ段階をたどります。SDKの読み込み、ビューの生成、地図インスタンスの生成、そしてタイルの描画完了。段階が InitState として共通化されているので、「まだ操作してはいけない時期」をプロバイダごとに覚える必要がありません。

カメラ操作やオーバーレイの追加は MapCreated 以降で受け付けられ、内部でキューに入ります。読み込み完了を待ってから何かしたい場合は onMapLoaded を使います。

図 · InitState の進み方
NotStarted
まだ何も始まっていない状態。
Initializing
初期化を開始。APIキーの検証やSDKの読み込みが走ります。
SdkInitialized
地図SDK本体の読み込みが完了。
MapViewCreated
プラットフォームのビュー(MapView / UIView / DOM要素)を生成。
MapCreating
ビューの中に地図インスタンスを生成中。
MapCreated
地図インスタンスが利用可能に。カメラ操作やオーバーレイの登録が反映され始めます。
MapLoaded
タイルの描画まで完了。onMapLoaded が呼ばれるのはこの段階です。
Failed
初期化に失敗(APIキー不正、ネットワーク断など)。
段階は Android・iOS・React で同じ列挙として定義されています。プロバイダを差し替えても、待つべきタイミングの判断は変わりません。
01 sdkInitialize
地図SDKを読み込みます。Webではスクリプトの注入、モバイルでは初期化処理。失敗すれば Failed に進みます。
02 createHolder
ネイティブの地図インスタンスをホルダーで包みます。以降、SDK固有の型はここに隠れます。
03 createController
ホルダーをコントローラで包み、共通APIとイベント配線を用意します。状態にコントローラが接続されます。
RESTORE

画面回転・再生成をまたぐ

Android では状態が rememberSaveable で保存され、画面回転や設定変更のあともカメラ位置とデザインが復元されます。構成変更のときは地図ビュー自体を破棄せず再利用します。

TEARDOWN

破棄されるとき

本当の破棄(画面離脱)ではコントローラが持つオーバーレイ管理やタイルサーバのルート、コルーチンスコープをまとめて解放します。プロバイダを切り替えたときも同じ経路で古い地図が片付けられます。

動画で見せる実装 · Android + MapLibreKotlin · Jetpack Compose
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() }
}
サンプル動画 · 初期化が進む様子
動画 未撮影地図が生成されてから操作を受け付けるまで、InitState が段階的に進む様子。読み込み中の表示と切り替わるタイミングが見えると分かりやすくなります。

03 · イベント

ビューに渡すハンドラは3プラットフォームで同じ構成です。型も共通で、読み込み完了は状態オブジェクト、タップは座標、カメラ変化はカメラ位置を受け取ります。

イベント
受け取る値
説明
onMapLoaded
MapViewState
地図の読み込みが完了。引数の状態オブジェクトを保持して以降の操作に使うのが定番です。
onMapClick
GeoPoint
地図のタップ。情報バブルを閉じる、ピンを置くなどに。
onMapLongClick
GeoPoint
長押し。
onCameraMoveStart
MapCameraPosition
カメラ操作の開始。ユーザー操作・プログラム移動の両方で呼ばれます。
onCameraMove
MapCameraPosition
移動中に連続で呼ばれます。表示中の座標表示などに。
onCameraMoveEnd
MapCameraPosition
移動が止まったとき。データ再取得はここで行うのが基本です。
GoogleMapView · イベントの受け取り
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 */ }

カメラ系のイベントは状態オブジェクトの cameraPosition も同時に更新するため、ハンドラを付けなくても最新のカメラは常に読み出せます。移動中の再取得を避けたい場合は onCameraMoveEnd だけを使ってください。

04 · ネイティブへの逃げ道

共通APIで足りないときのために、ネイティブの地図インスタンスへ降りる口を用意しています。ふだんは使いませんが、プロバイダ固有の機能を1か所だけ使いたい場合の逃げ道です。

MapViewHolder

ネイティブ地図を包むホルダー

ホルダーはプラットフォームのビューと地図インスタンスの2つを持ちます。SDK固有のコードはここから先に閉じ込め、共通コードには漏らさない構成です。

toScreenOffset / fromScreenOffset

座標と画面位置の変換

地理座標と画面上のピクセル位置を相互に変換できます。地図の上に独自のUIを重ねるときに使います。

ホルダーの取り出し
// どうしてもネイティブAPIが必要なときだけ取り出す
val holder = mapViewState.getMapViewHolder()
val nativeMap = holder?.map // GoogleMap / MapLibreMap / ...
val offset = holder?.toScreenOffset(point)

ホルダーを使ったコードはプロバイダに依存します。共通のまま保ちたい部分と、あえて固有APIを使う部分を意識して分けてください。

関連ページ