MapViewHolderInterface
MapViewHolderInterface は、MapConductor の統一 API でカバーされていない特定の地図SDK機能が必要な高度なユースケースのために、ネイティブ地図 SDK インスタンスへのアクセスを提供します。MapConductor は共通のインターフェースを提供しますが、すべてのネイティブ機能を完全にラップするわけではなく、MapViewHolderInterface がこのギャップを埋めます。
MapConductor は地図SDK間で統一された API を提供することを目指していますが、完全な機能パリティは必ずしも可能ではありません。MapViewHolderInterface を使用すると、開発者は地図SDK固有の機能が必要な場合に、基盤となるネイティブ地図インスタンスにアクセスできます。
interface MapViewHolderInterface<ActualMapViewType, ActualMapType> { val mapView: ActualMapViewType val map: ActualMapType}MapViewHolderInterface へのアクセス
Section titled “MapViewHolderInterface へのアクセス”各地図SDKの MapViewStateInterface 実装は、その特定の MapViewHolderInterface へのアクセスを提供します:
// Any provider state exposes getMapViewHolder()val state = rememberGoogleMapViewState()
// May be null until the map is initializedval holder = state.getMapViewHolder()
// Use the concrete holder type when you need provider-specific APIs// (e.g. GoogleMapViewHolder / MapboxMapViewHolder / MapLibreMapViewHolder / ...)// holder?.googleMap?.uiSettings?.isZoomControlsEnabled = false地図SDK固有の実装
Section titled “地図SDK固有の実装”Google Maps
Section titled “Google Maps”@Composablefun MapViewHolderGoogleMapsExample(modifier: Modifier = Modifier) { val context = LocalContext.current
// 地図のカメラ位置 val mapViewState = rememberGoogleMapViewState( cameraPosition = MapCameraPosition( position = GeoPoint.fromLatLong(28.53456, 77.192845), zoom = 12.0 ), )
// mutableStateOfにすることで、mapStyleが変化したら 再描画 var mapStyle by remember { mutableStateOf<MapStyleOptions?>(null) }
// ViewHolderの取得 val googleMapViewHolder = mapViewState.getMapViewHolder() LaunchedEffect(googleMapViewHolder, mapStyle) { if (googleMapViewHolder == null) return@LaunchedEffect googleMapViewHolder.map.setMapStyle(mapStyle) }
Column(modifier = modifier) { Row( modifier = Modifier.fillMaxWidth(), ) { Spacer(modifier = Modifier.size(20.dp))
// 通常のGoogle Maps Button(onClick = { mapStyle = null }) { Text( text = "Normal" ) } Spacer(modifier = Modifier.size(20.dp))
// 地図を簡略化したデザイン Button(onClick = { mapStyle = MapStyleOptions.loadRawResourceStyle(context, R.raw.style_map) }) { Text( text = "Simplified" ) } }
GoogleMapView( state = mapViewState, modifier = Modifier.fillMaxSize(), ) {} }}Mapbox
Section titled “Mapbox”class MainActivity : ComponentActivity() { lateinit var permissionsManager: PermissionsManager
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge()
if (PermissionsManager.areLocationPermissionsGranted(this)) { // 位置情報アクセス許可後の処理(例: Maps SDK の LocationComponent を有効化してデバイスの位置を表示) } else { permissionsManager = PermissionsManager(this.permissionsListener) permissionsManager.requestLocationPermissions(this) }
setContent { MapConductorSDKTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> BasicMapExample( modifier = Modifier .padding(innerPadding) .fillMaxSize(), ) } } } }
var permissionsListener: PermissionsListener = object : PermissionsListener { override fun onExplanationNeeded(permissionsToExplain: List<String>) { // 位置情報アクセス権限を取得するための説明を表示 }
override fun onPermissionResult(granted: Boolean) { if (granted) { // 位置情報アクセス許可後の処理(例: Maps SDK の LocationComponent を有効化してデバイスの位置を表示) } else { // ユーザーが権限を拒否した } } }}
@Composablefun BasicMapExample(modifier: Modifier = Modifier) { val mapViewState = rememberMapboxMapViewState( cameraPosition = MapCameraPosition( position = GeoPoint.fromLatLong(35.706400, 139.763635), zoom = 13.0 ), )
// ViewHolderの取得 var mapboxMapViewHolder by remember { mutableStateOf<MapboxMapViewHolder?>(null) } LaunchedEffect(mapboxMapViewHolder) { mapboxMapViewHolder?.let { holder -> with(holder.mapView) { location.locationPuck = createDefault2DPuck(withBearing = true) location.enabled = true location.pulsingEnabled = true location.puckBearing = PuckBearing.COURSE location.puckBearingEnabled = true viewport.transitionTo( targetState = viewport.makeFollowPuckViewportState(), transition = viewport.makeImmediateViewportTransition() ) } } }
MapboxMapView( state = mapViewState, modifier = modifier, onMapLoaded = { mapboxMapViewHolder = mapViewState.getMapViewHolder() } ) {}}HERE Maps
Section titled “HERE Maps”typealias HereViewHolder = MapViewHolderInterface<MapView, MapScene>
// ネイティブ HERE SDK API にアクセスhereHolder?.let { holder -> val hereMapView: MapView = holder.mapView val mapScene: MapScene = holder.map
// HERE 固有の機能を使用 val searchEngine = SearchEngine() val textQuery = TextQuery("coffee shops", geoCoordinates)
searchEngine.search(textQuery) { searchError, searchResults -> // HERE 検索結果を処理 }
// HERE routing val routingEngine = RoutingEngine() // ルーティングを設定して使用}ArcGIS
Section titled “ArcGIS”@Composablefun BasicMapExample( modifier: Modifier = Modifier) { // 地図のカメラ位置 val mapViewState = rememberArcGISMapViewState( cameraPosition = MapCameraPosition( position = GeoPoint.fromLatLong(40.40195, -3.68698), zoom = 8.0 ), )
// AuthenticatorStateを設定 val authenticatorState = remember { AuthenticatorState() }
// ViewHolderの保持 var arcGisMapViewHolder by remember { mutableStateOf<ArcGISMapViewHolder?>(null) }
LaunchedEffect(arcGisMapViewHolder) { // 交通情報のレイヤーを表示 arcGisMapViewHolder?.let { holder -> val trafficLayer = ArcGISMapImageLayer("https://traffic.arcgis.com/arcgis/rest/services/World/Traffic/MapServer") holder.map.scene!!.operationalLayers.add(trafficLayer) } }
ArcGISMapView( state = mapViewState, modifier = modifier.fillMaxSize(), sdkInitialize = { context -> ArcGISOAuthHybridInitialize( // ログインダイアログを表示する AuthenticatorState authenticatorState = authenticatorState
// ArcGISポータルのURL portalUrl = "https://(your).maps.arcgis.com/",
// OAuth2アプリケーションのリダイレクトURL redirectUrl = "(application redirectUrl)",
// OAuth2アプリケーションのクライアントID clientId = "(application clientId)",
// (オプション) // OAuth2アプリケーションのクライアントシークレット // 省略した場合はログインダイアログが表示されます clientSecret = "(application client secret)", ) }, onMapLoaded = { // ViewHolderの取得 arcGisMapViewHolder = mapViewState.getMapViewHolder() } ) {}
// 認証UI(ユーザーログイン用)。ハイブリッド認証のフォールバックで使用されます。 Authenticator(authenticatorState = authenticatorState)}一般的なユースケース
Section titled “一般的なユースケース”カスタムスタイリング
Section titled “カスタムスタイリング”@Composablefun CustomStyledMap() { val mapViewState = rememberGoogleMapViewState()
LaunchedEffect(mapViewState) { // 地図の初期化を待つ delay(1000)
mapViewState.getMapViewHolder()?.let { holder -> val googleMap = holder.map
// カスタム地図スタイルを適用 val styleJson = loadStyleFromAssets(context, "custom_style.json") googleMap.setMapStyle(MapStyleOptions(styleJson))
// 地図 UI を設定 googleMap.uiSettings.isMyLocationButtonEnabled = false googleMap.uiSettings.isCompassEnabled = true } }
// MapView を GoogleMapView、MapboxMapView などの選択した地図SDKに置き換えてください GoogleMapView(state = mapViewState) { // Your MapConductor components }}@Composablefun AnalyticsIntegration() { val mapViewState = remember { MapboxViewState() }
LaunchedEffect(mapViewState) { mapViewState.getMapViewHolder()?.let { holder -> val mapboxMap = holder.map
// カスタム地図イベントを追跡 mapboxMap.addOnMapClickListener { point -> Analytics.track("map_click", mapOf( "lat" to point.latitude, "lng" to point.longitude, "zoom" to mapboxMap.cameraPosition.zoom )) true }
// スタイル変更を追跡 mapboxMap.addOnStyleLoadedListener { Analytics.track("style_loaded", mapOf( "style_id" to mapboxMap.style?.styleURI )) } } }
MapboxMapView(state = mapViewState) { // Your MapConductor components }}パフォーマンス最適化
Section titled “パフォーマンス最適化”@Composablefun PerformanceOptimizedMap() { val mapViewState = remember { HereViewState() }
LaunchedEffect(mapViewState) { mapViewState.getMapViewHolder()?.let { holder -> val mapScene = holder.map
// HERE 固有のパフォーマンス設定 mapScene.setLayerVisibility(MapScene.Layers.TRAFFIC_FLOW, VisibilityState.VISIBLE)
// 特定のユースケースのために最適化 val mapSettings = mapScene.mapSettings mapSettings.isTiltGesturesEnabled = false mapSettings.isRotateGesturesEnabled = false
// カスタム詳細レベル設定 mapScene.setLevelOfDetail(LevelOfDetail.HIGH) } }
HereMapView(state = mapViewState) { // Your MapConductor components }}サードパーティサービスとの統合
Section titled “サードパーティサービスとの統合”@Composablefun ThirdPartyIntegration() { val mapViewState = rememberArcGISMapViewState()
LaunchedEffect(mapViewState) { mapViewState.getMapViewHolder()?.let { holder -> val sceneView = holder.map
// ArcGIS Online サービスと統合 val featureTable = ServiceFeatureTable("https://services.arcgis.com/...") val featureLayer = FeatureLayer(featureTable)
sceneView.map.basemap.baseLayers.add(featureLayer)
// Query features val queryParams = QueryParameters().apply { whereClause = "STATE_NAME = 'California'" }
featureTable.queryFeaturesAsync(queryParams).addDoneListener { val results = it.result // フィーチャークエリ結果を処理 } } }
ArcGISMapView(state = mapViewState) { // Your MapConductor components }}ライフサイクル管理
Section titled “ライフサイクル管理”地図ライフサイクルイベント
Section titled “地図ライフサイクルイベント”@Composablefun LifecycleAwareMap() { val mapViewState = rememberGoogleMapViewState() val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> mapViewState.getMapViewHolder()?.let { holder -> val mapView = holder.mapView
when (event) { Lifecycle.Event.ON_RESUME -> mapView.onResume() Lifecycle.Event.ON_PAUSE -> mapView.onPause() Lifecycle.Event.ON_START -> mapView.onStart() Lifecycle.Event.ON_STOP -> mapView.onStop() Lifecycle.Event.ON_DESTROY -> mapView.onDestroy() else -> { } } } }
lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } }
GoogleMapView(state = mapViewState) { // Your MapConductor components }}エラー処理と安全性
Section titled “エラー処理と安全性”Null 安全性
Section titled “Null 安全性”fun accessNativeMap(mapViewState: GoogleMapViewState) { val holder = mapViewState.getMapViewHolder()
if (holder != null) { // ネイティブ API に安全にアクセス val googleMap = holder.map val mapView = holder.mapView
// Use native features googleMap.setOnMarkerClickListener { marker -> // Handle marker click true } } else { // 地図がまだ初期化されていないか利用できない Log.w("MapAccess", "MapViewHolderInterface not available") }}初期化タイミング
Section titled “初期化タイミング”@Composablefun SafeNativeAccess() { val mapViewState = remember { MapboxViewState() } var isMapReady by remember { mutableStateOf(false) }
LaunchedEffect(mapViewState) { // 地図が準備できるまでポーリング while (!isMapReady) { delay(100) isMapReady = mapViewState.getMapViewHolder() != null }
// Now safe to use native APIs mapViewState.getMapViewHolder()?.let { holder -> val mapboxMap = holder.map // ネイティブ機能を設定 } }
MapboxMapView(state = mapViewState) { // Your MapConductor components }}ベストプラクティス
Section titled “ベストプラクティス”- 初期化チェック: ネイティブ API にアクセスする前に、
getMapViewHolder()が非 null を返すことを常に確認 - ライフサイクル認識: ネイティブ API を使用する際は、地図のライフサイクルイベントを適切に処理
- エラー処理: 地図SDK API が例外をスローする可能性があるため、ネイティブ API 呼び出しを try-catch ブロックでラップ
- ドキュメント: ネイティブ API の使用については、各地図SDKの公式ドキュメントを参照
- テスト: ネイティブ API を使用する際は、すべてのターゲット地図SDKで徹底的にテスト
- フォールバック: ネイティブ機能が利用できない場合のフォールバック動作を提供
- バージョン互換性: ネイティブ API の使用が、ターゲットとしている SDK バージョンと互換性があることを確認
制限と考慮事項
Section titled “制限と考慮事項”- プラットフォーム依存性: ネイティブ API の使用は、特定の地図SDKにコードを結び付けます
- メンテナンスオーバーヘッド: 地図SDK API の変更により、ネイティブ API の使用を更新する必要があります
- テストの複雑性: 地図SDK固有のコードパスをカバーするため、より複雑なテストが必要
- 機能パリティ: すべての地図SDKが同等のネイティブ機能をサポートしているわけではありません
- MapConductor 統合: ネイティブの変更は MapConductor の状態管理と統合されない可能性があります
MapViewHolderInterface は高度なユースケースのための強力なエスケープハッチですが、MapConductor の統一 API アプローチの利点を維持するために慎重に使用する必要があります。