Skip to content

MapViewHolderInterface

MapViewHolderInterface は、MapConductor の統一 API でカバーされていない特定の地図SDK機能が必要な高度なユースケースのために、ネイティブ地図 SDK インスタンスへのアクセスを提供します。MapConductor は共通のインターフェースを提供しますが、すべてのネイティブ機能を完全にラップするわけではなく、MapViewHolderInterface がこのギャップを埋めます。

MapConductor は地図SDK間で統一された API を提供することを目指していますが、完全な機能パリティは必ずしも可能ではありません。MapViewHolderInterface を使用すると、開発者は地図SDK固有の機能が必要な場合に、基盤となるネイティブ地図インスタンスにアクセスできます。

interface MapViewHolderInterface<ActualMapViewType, ActualMapType> {
val mapView: ActualMapViewType
val map: ActualMapType
}

各地図SDKの MapViewStateInterface 実装は、その特定の MapViewHolderInterface へのアクセスを提供します:

// Any provider state exposes getMapViewHolder()
val state = rememberGoogleMapViewState()
// May be null until the map is initialized
val holder = state.getMapViewHolder()
// Use the concrete holder type when you need provider-specific APIs
// (e.g. GoogleMapViewHolder / MapboxMapViewHolder / MapLibreMapViewHolder / ...)
// holder?.googleMap?.uiSettings?.isZoomControlsEnabled = false
@Composable
fun 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(),
) {}
}
}
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 {
// ユーザーが権限を拒否した
}
}
}
}
@Composable
fun 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()
}
) {}
}
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()
// ルーティングを設定して使用
}
@Composable
fun 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)
}
@Composable
fun 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
}
}
@Composable
fun 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
}
}
@Composable
fun 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 “サードパーティサービスとの統合”
@Composable
fun 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
}
}
@Composable
fun 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
}
}
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")
}
}
@Composable
fun 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
}
}
  1. 初期化チェック: ネイティブ API にアクセスする前に、getMapViewHolder() が非 null を返すことを常に確認
  2. ライフサイクル認識: ネイティブ API を使用する際は、地図のライフサイクルイベントを適切に処理
  3. エラー処理: 地図SDK API が例外をスローする可能性があるため、ネイティブ API 呼び出しを try-catch ブロックでラップ
  4. ドキュメント: ネイティブ API の使用については、各地図SDKの公式ドキュメントを参照
  5. テスト: ネイティブ API を使用する際は、すべてのターゲット地図SDKで徹底的にテスト
  6. フォールバック: ネイティブ機能が利用できない場合のフォールバック動作を提供
  7. バージョン互換性: ネイティブ API の使用が、ターゲットとしている SDK バージョンと互換性があることを確認
  1. プラットフォーム依存性: ネイティブ API の使用は、特定の地図SDKにコードを結び付けます
  2. メンテナンスオーバーヘッド: 地図SDK API の変更により、ネイティブ API の使用を更新する必要があります
  3. テストの複雑性: 地図SDK固有のコードパスをカバーするため、より複雑なテストが必要
  4. 機能パリティ: すべての地図SDKが同等のネイティブ機能をサポートしているわけではありません
  5. MapConductor 統合: ネイティブの変更は MapConductor の状態管理と統合されない可能性があります

MapViewHolderInterface は高度なユースケースのための強力なエスケープハッチですが、MapConductor の統一 API アプローチの利点を維持するために慎重に使用する必要があります。