Architecture overview
MapConductor sits between your app and the map SDKs. The app talks only to a unified map API, and MapConductor forwards the work to the provider you picked. Android (Kotlin + Compose), iOS (Swift + SwiftUI) and React (Web) share the same package split and the same internal roles.
01 · The whole picture
The flow is identical on every platform: app → unified map API → Core → provider driver → map SDK. Only the top two layers are your code; the bottom two are swappable.
UI and business logic
Unified map API
Core features (map, markers, shapes, events)
Provider drivers
Map SDKs (native / JavaScript)
Changing provider swaps only the bottom two layers. Screen code is written against the unified API, so it keeps working untouched.
02 · Per-platform packages
Core and drivers ship as separate packages. An app depends on Core plus the driver for the providers it actually uses.
android-sdk-core holds the shared model and the diffing; android-sdk-compose provides the Compose state holders. GeoJSON, clustering and heatmaps are separate modules (android-geojson-layer / android-marker-clustering / android-heatmap).
ios-sdk-core holds the shared model and the SwiftUI foundations; the SwiftUI views themselves are provided by the provider packages (ios-for-*). The MapKit driver targets Apple own SDK, so it needs no extra billing or API key.
js-sdk-core is the React-free TypeScript core holding the shared model and the diffing — laid out almost exactly like the Kotlin and Swift cores. js-sdk-react puts that on React and supplies the components: Marker, Polygon, Polyline, Circle, GroundImage, RasterLayer, InfoBubble. The map view itself comes from each driver (react-for-*) as a view plus hook pair, e.g. MapLibreMapView / MapLibreMapView2D and useMapLibreViewState. GeoJSON, clustering, heatmaps and icons are separate packages (react-geojson-layer / react-marker-clustering / react-heatmap / react-icons).
03 · Layers
Responsibilities are split into six layers so they can be reused across platforms. Each layer knows nothing about the implementation below it.
The split lets code that focuses on what you want on the map — not which platform you are on — move between environments as is. Platform specifics stay confined to how the cross-platform layer installs and declares things.
React Native
React on the web and React Native share what the app writes — the same state objects, the same components, with only the MapView swapped. They differ below that. On the web the driver calls a JavaScript map library in the same runtime; on React Native it hands off across a bridge to the native MapConductor SDK, and a native map SDK does the drawing.
React
React Native
The bridge is layer 3 in the table above. On the web it does not appear at all: the UI and the drawing are in one runtime. On React Native they are in two, so something has to carry between them. That layer is the only one that differs; everything above it is the same.
Four providers are available on React Native — Google Maps, MapLibre, ArcGIS and HERE. It is fewer than the thirteen on the web because each one needs a matching module on the native side.
04 · Inside Core
Core repeats the same seven roles for every element type — marker, polyline, polygon, circle, ground image, raster layer. Learn one and you can read the rest the same way.
What matters is where the boundary sits. The Core side (State / Entity / Manager / Controller / Overlay) is near-identical code in all three languages; per provider you only write the renderer and the declaration that the element is handled at all.
PolygonCapableInterface.kt
AbstractPolygonOverlayRenderer.kt
polygon/PolygonOverlayRenderer.swift
polygon/PolygonHoleSplit.swift
polygon/PolygonCapable.ts
AbstractPolygonOverlayRenderer.ts
05 · How diffing works
Your app just hands over the array of what should exist now. Core computes the difference from last time and passes added, changed and removed items to the driver. Map objects are never rebuilt, so rendering stays stable at scale.
export interface OverlayRendererInterface<ActualType, StateType, EntityType> {
onAdd(data: StateType[]): Promise<Array<ActualType | null>> | Array<ActualType | null>;
onChange(data: Array<ChangeParamsInterface<EntityType>>): Promise<Array<ActualType | null>> | Array<ActualType | null>;
onRemove(data: EntityType[]): Promise<void> | void;
onPostProcess(): Promise<void> | void;
}onChange receives both the previous entity (prev) and the current state (current), so a driver can update only the properties that moved. The signature is the same in Kotlin and Swift.
06 · What a driver implements
Supporting a new map SDK is confined to extending the per-element abstract renderer and filling in three operations. Core model and diffing logic are untouched.
// The driver extends the abstract renderer and fills in just three operations
internal class GoogleMapPolygonOverlayRenderer(
override val holder: GoogleMapViewHolder,
override val coroutine: CoroutineScope,
) : AbstractPolygonOverlayRenderer<GoogleMapActualPolygon>() {
override suspend fun createPolygon(state: PolygonState) = /* map.addPolygon(...) */
override suspend fun updatePolygonProperties(polygon, current, prev) = /* Apply only the diff */
override suspend fun removePolygon(entity: PolygonEntityInterface<GoogleMapActualPolygon>) =
entity.polygon.remove()
}Only create, update and remove are provider specific. Diffing, the ledger and hit-testing are already done by Core before the call.
// Same shape as Kotlin: extend the abstract renderer and fill in three operations
final class MapKitPolygonOverlayRenderer: AbstractPolygonOverlayRenderer<MKPolygon> {
override func createPolygon(state: PolygonState) async -> MKPolygon? { /* MKPolygon(...) */ }
override func updatePolygonProperties(polygon, current, prev) async -> MKPolygon? { /* Apply only the diff */ }
override func removePolygon(entity: PolygonEntity<MKPolygon>) async { /* mapView.removeOverlay */ }
}
// The controller uses Core's generic implementation as it is
final class MapKitPolygonController: PolygonController<MKPolygon, MapKitPolygonOverlayRenderer> { }In Swift the set of controllers a provider builds is its feature list. Things the SDK cannot do — polygon holes, marker animations — are expressed either by not implementing that renderer, or by falling back to a core-side substitute such as PolygonHoleSplit, which divides a holed polygon into hole-free simple rings.
// Same shape as Kotlin and Swift: extend the abstract renderer and fill in three operations
export class MapLibrePolygonOverlayRenderer extends AbstractPolygonOverlayRenderer<
MapLibreMapViewHolder,
MapLibreActualPolygon
> {
async createPolygon(state: PolygonState) { /* Build the GeoJSON feature */ }
async updatePolygonProperties({ current, prev }) { /* Apply only the diff */ }
async removePolygon(entity: PolygonEntity<MapLibreActualPolygon>) { /* Remove it */ }
// After the diff is applied, rewrite the source from the entities that remain
override async onPostProcess() { this.layer.draw(this.polygonManager.allEntities()); }
}
// Declare what a driver can handle by implementing the Capable interfaces
export class MapLibreViewController extends BaseMapViewController
implements MapViewControllerInterface, MarkerCapable, PolygonCapable, /* … */ { }In TypeScript too, only create, update and remove are provider specific. Differences in how an SDK draws — MapLibre batches into a layer, so it rewrites the whole source in onPostProcess instead of removing items one by one — stay inside the renderer. What a driver can handle is expressed by the Capable interfaces its MapViewController implements — PolygonCapable and its siblings — so availability is visible in the types.
07 · Where abstraction stops
MapConductor does not try to wrap every feature of every map SDK. It focuses on the commonly used operations and offers two ways out for everything beyond them.
// 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 shared API stays simple without giving up what each provider is good at — see Native extensions for details.