Native extensions
MapConductor does not wrap every feature of every map SDK. When you need something the unified API does not model, there are two ways down that do not cost you the abstraction — the rest of the screen keeps running on the shared API.
01 · Where abstraction stops
The unified API covers concepts that exist everywhere. Extruded 3D buildings, one vendor traffic layer, a bespoke shader — those are deliberately out of scope. Wrapping everything would turn the shared API into a lowest common multiple and make it unpleasant to use.
Borrow the real object
Pull out the native map instance and call that SDK directly. Available on all three platforms.
Inject a capability
An add-on module registers its capability under a typed key without modifying Core. This is the mechanism for extending the SDK itself.
02 · Reaching the native map
getMapViewHolder() on the state object returns a holder carrying that provider view and map instance. Narrow the type and you are on the raw SDK. The holder also does coordinate ⇄ screen-pixel conversion.
// Once narrowed, everything past this point is the plain Google Maps SDK
val holder = mapViewState.getMapViewHolder()
if (holder is GoogleMapViewHolder) {
holder.map.isTrafficEnabled = true
}// Narrow from AnyMapViewHolder to the concrete holder
if let holder = mapState.getMapViewHolder() as? MapKitViewHolder {
holder.mapView.showsTraffic = true
}// map on the shared interface is unknown — narrow it to the provider's type before use
const holder = mapViewState.getMapViewHolder();
const map = holder?.map as maplibregl.Map | undefined;
map?.addLayer({ id: 'buildings', type: 'fill-extrusion', source: 'composite' });The holder only exists once the map has initialised — it returns null before that, so call it inside onMapLoaded or later.
03 · Plugging in a module
MapServiceRegistry is the seam an add-on module uses to inject its capability without modifying Core: register under a typed key, retrieve with the same key, and because the key carries the type nothing needs casting on the way out. Marker clustering is what uses it today — it resolves the MarkerRenderingSupport its provider registered and renders through that.
MapServiceRegistry
Define a MapServiceKey<T> as a singleton, put it into the MutableMapServiceRegistry, and get it back where needed. On Android the map-scoped registry is reached through the LocalMapServiceRegistry composition local.
MapServiceRegistryScope
Keys are types conforming to MapServiceKey, put into a MutableMapServiceRegistry. The registry lives on the state object (MapViewState.serviceRegistry) and the provider makes it visible with MapServiceRegistryScope.with(...) for exactly as long as content is being assembled. It is not a SwiftUI Environment value because map content is a MapViewContent value, not a view hierarchy.
// Kotlin: define the key as a singleton object
object MarkerRenderingSupportKey : MapServiceKey<MarkerRenderingSupport<*>>
registry.put(MarkerRenderingSupportKey, support)
val support = registry.get(MarkerRenderingSupportKey) // It comes back with its type intact
// Swift: define the key as a type conforming to MapServiceKey
enum MarkerRenderingSupportKey: MapServiceKey {
typealias Value = any MarkerRenderingSupport
}
registry.put(MarkerRenderingSupportKey.self, support)
let support = registry.get(MarkerRenderingSupportKey.self)
// TypeScript: make the key with createMapServiceKey<T>()
const MarkerRenderingSupportKey = createMapServiceKey<MarkerRenderingSupport>();All three of these bring provider-specific code into your screen — the part that stops working when you swap providers. Keep the places you use them collected behind one branch.