MapViewHolderInterface
MapViewHolderInterface provides access to the native map SDK instances for advanced use cases where MapConductor’s unified API doesn’t cover specific provider functionality. While MapConductor provides a common interface, it doesn’t completely wrap all native features, and MapViewHolderInterface bridges this gap.
Overview
Section titled “Overview”MapConductor aims to provide a unified API across map providers, but complete feature parity is not always possible. MapViewHolderInterface allows developers to access the underlying native map instances when they need provider-specific functionality.
interface MapViewHolderInterface<ActualMapViewType, ActualMapType> { val mapView: ActualMapViewType val map: ActualMapType}Accessing MapViewHolderInterface
Section titled “Accessing MapViewHolderInterface”Each map provider’s MapViewStateInterface implementation provides access to its specific 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 = falseProvider-Specific Implementations
Section titled “Provider-Specific Implementations”Google Maps
Section titled “Google Maps”@Composablefun MapViewHolderGoogleMapsExample(modifier: Modifier = Modifier) { val context = LocalContext.current
// Map camera position val mapViewState = rememberGoogleMapViewState( cameraPosition = MapCameraPosition( position = GeoPoint.fromLatLong(28.53456, 77.192845), zoom = 12.0 ), )
// Using mutableStateOf, recompose when mapStyle changes var mapStyle by remember { mutableStateOf<MapStyleOptions?>(null) }
// Get 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))
// Normal Google Maps Button(onClick = { mapStyle = null }) { Text( text = "Normal" ) } Spacer(modifier = Modifier.size(20.dp))
// Simplified map design 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)) { // Permission sensitive logic called here, such as activating the Maps SDK's LocationComponent to show the device's location } 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>) { // Display an explanation to obtain the location access permissions }
override fun onPermissionResult(granted: Boolean) { if (granted) { // Permission sensitive logic called here, such as activating the Maps SDK's LocationComponent to show the device's location } else { // User denied the permission } } }}
@Composablefun BasicMapExample(modifier: Modifier = Modifier) { val mapViewState = rememberMapboxMapViewState( cameraPosition = MapCameraPosition( position = GeoPoint.fromLatLong(35.706400, 139.763635), zoom = 13.0 ), )
// Get 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>
// Access native HERE SDK APIshereHolder?.let { holder -> val hereMapView: MapView = holder.mapView val mapScene: MapScene = holder.map
// Use HERE specific features val searchEngine = SearchEngine() val textQuery = TextQuery("coffee shops", geoCoordinates)
searchEngine.search(textQuery) { searchError, searchResults -> // Handle HERE search results }
// HERE routing val routingEngine = RoutingEngine() // Configure and use routing}ArcGIS
Section titled “ArcGIS”@Composablefun BasicMapExample( modifier: Modifier = Modifier) { // Map camera position val mapViewState = rememberArcGISMapViewState( cameraPosition = MapCameraPosition( position = GeoPoint.fromLatLong(40.40195, -3.68698), zoom = 8.0 ), )
// Set up AuthenticatorState val authenticatorState = remember { AuthenticatorState() }
// Hold ViewHolder var arcGisMapViewHolder by remember { mutableStateOf<ArcGISMapViewHolder?>(null) }
LaunchedEffect(arcGisMapViewHolder) { // Display traffic information layer 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 to display login dialog authenticatorState = authenticatorState
// ArcGIS portal URL portalUrl = "https://(your).maps.arcgis.com/",
// OAuth2 application redirect URL redirectUrl = "(application redirectUrl)",
// OAuth2 application client ID clientId = "(application clientId)",
// (Optional) // OAuth2 application client secret // If omitted, login dialog will be displayed clientSecret = "(application client secret)", ) }, onMapLoaded = { // Get ViewHolder arcGisMapViewHolder = mapViewState.getMapViewHolder() } ) {}
// Authentication UI (for user login). Used as fallback for hybrid authentication. Authenticator(authenticatorState = authenticatorState)}Common Use Cases
Section titled “Common Use Cases”Custom Styling
Section titled “Custom Styling”@Composablefun CustomStyledMap() { val mapViewState = rememberGoogleMapViewState()
LaunchedEffect(mapViewState) { // Wait for map initialization delay(1000)
mapViewState.getMapViewHolder()?.let { holder -> val googleMap = holder.map
// Apply custom map style val styleJson = loadStyleFromAssets(context, "custom_style.json") googleMap.setMapStyle(MapStyleOptions(styleJson))
// Configure map UI googleMap.uiSettings.isMyLocationButtonEnabled = false googleMap.uiSettings.isCompassEnabled = true } }
// Replace MapView with your chosen map provider, such as GoogleMapView, MapboxMapView GoogleMapView(state = mapViewState) { // Your MapConductor components }}Advanced Analytics
Section titled “Advanced Analytics”@Composablefun AnalyticsIntegration() { val mapViewState = remember { MapboxViewState() }
LaunchedEffect(mapViewState) { mapViewState.getMapViewHolder()?.let { holder -> val mapboxMap = holder.map
// Track custom map events mapboxMap.addOnMapClickListener { point -> Analytics.track("map_click", mapOf( "lat" to point.latitude, "lng" to point.longitude, "zoom" to mapboxMap.cameraPosition.zoom )) true }
// Track style changes mapboxMap.addOnStyleLoadedListener { Analytics.track("style_loaded", mapOf( "style_id" to mapboxMap.style?.styleURI )) } } }
MapboxMapView(state = mapViewState) { // Your MapConductor components }}Performance Optimization
Section titled “Performance Optimization”@Composablefun PerformanceOptimizedMap() { val mapViewState = remember { HereViewState() }
LaunchedEffect(mapViewState) { mapViewState.getMapViewHolder()?.let { holder -> val mapScene = holder.map
// HERE-specific performance settings mapScene.setLayerVisibility(MapScene.Layers.TRAFFIC_FLOW, VisibilityState.VISIBLE)
// Optimize for specific use case val mapSettings = mapScene.mapSettings mapSettings.isTiltGesturesEnabled = false mapSettings.isRotateGesturesEnabled = false
// Custom level of detail settings mapScene.setLevelOfDetail(LevelOfDetail.HIGH) } }
HereMapView(state = mapViewState) { // Your MapConductor components }}Integration with Third-Party Services
Section titled “Integration with Third-Party Services”@Composablefun ThirdPartyIntegration() { val mapViewState = rememberArcGISMapViewState()
LaunchedEffect(mapViewState) { mapViewState.getMapViewHolder()?.let { holder -> val sceneView = holder.map
// Integrate with ArcGIS Online services 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 // Process feature query results } } }
ArcGISMapView(state = mapViewState) { // Your MapConductor components }}Lifecycle Management
Section titled “Lifecycle Management”Map Lifecycle Events
Section titled “Map Lifecycle Events”@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 }}Error Handling and Safety
Section titled “Error Handling and Safety”Null Safety
Section titled “Null Safety”fun accessNativeMap(mapViewState: GoogleMapViewState) { val holder = mapViewState.getMapViewHolder()
if (holder != null) { // Safe to access native APIs val googleMap = holder.map val mapView = holder.mapView
// Use native features googleMap.setOnMarkerClickListener { marker -> // Handle marker click true } } else { // Map not yet initialized or unavailable Log.w("MapAccess", "MapViewHolderInterface not available") }}Initialization Timing
Section titled “Initialization Timing”@Composablefun SafeNativeAccess() { val mapViewState = remember { MapboxViewState() } var isMapReady by remember { mutableStateOf(false) }
LaunchedEffect(mapViewState) { // Poll until map is ready while (!isMapReady) { delay(100) isMapReady = mapViewState.getMapViewHolder() != null }
// Now safe to use native APIs mapViewState.getMapViewHolder()?.let { holder -> val mapboxMap = holder.map // Configure native features } }
MapboxMapView(state = mapViewState) { // Your MapConductor components }}Best Practices
Section titled “Best Practices”- Initialization Check: Always check if
getMapViewHolder()returns non-null before accessing native APIs - Lifecycle Awareness: Properly handle map lifecycle events when using native APIs
- Error Handling: Wrap native API calls in try-catch blocks as provider APIs may throw exceptions
- Documentation: Refer to each provider’s official documentation for native API usage
- Testing: Test thoroughly across all target map providers when using native APIs
- Fallback: Provide fallback behavior when native features are unavailable
- Version Compatibility: Ensure native API usage is compatible with the SDK versions you’re targeting
Limitations and Considerations
Section titled “Limitations and Considerations”- Platform Dependency: Native API usage ties your code to specific map providers
- Maintenance Overhead: Provider API changes may require updates to native API usage
- Testing Complexity: More complex testing required to cover provider-specific code paths
- Feature Parity: Not all providers support equivalent native features
- MapConductor Integration: Native changes may not integrate with MapConductor’s state management
MapViewHolderInterface is a powerful escape hatch for advanced use cases, but should be used judiciously to maintain the benefits of MapConductor’s unified API approach.