Docs / Concepts / Architecture

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.

ANDROID
android-sdk-core
iOS
ios-sdk-core
REACT
js-sdk-core / js-sdk-react

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.

APP

UI and business logic

React · Compose · SwiftUI
UNIFIED API

Unified map API

MapViewState · Marker · Camera
CORE

Core features (map, markers, shapes, events)

Manager · Controller · Overlay
DRIVER

Provider drivers

*-for-googlemaps / -maplibre / …
MAP SDK

Map SDKs (native / JavaScript)

Google Maps · MapLibre · Mapbox · MapKit · ArcGIS · HERE …

Changing provider swaps only the bottom two layers. Screen code is written against the unified API, so it keeps working untouched.

Platform

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).

03 · Layers

Responsibilities are split into six layers so they can be reused across platforms. Each layer knows nothing about the implementation below it.

#
Layer
Role
1
UI framework
The declarative UI your app uses — React, Vue, Jetpack Compose, SwiftUI.
2
Unified map API
MapConductor API, treating markers, shapes, camera and events as shared concepts. This is the layer you write code against.
3
Bridge
Passes unified API operations down according to the runtime. Not needed when the UI and the rendering share one runtime; it steps in only when they are split across two.
4
Cross-platform layer
How native map SDKs are installed and declared differs per platform only here.
5
Native SDK / drivers
The layer that actually renders camera, style and overlays — native on Android / iOS, or the JS driver on the web.
6
Map SDKs
The real thing: Google Maps, MapKit, MapLibre, Mapbox, ArcGIS, HERE and others.

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.

Where the same code parts company
SHARED

What the app writes

js-sdk-core · js-sdk-react
WEB

React

DRIVER
JavaScript driver
react-for-*
No bridge — one runtime
MAP SDK
JavaScript map SDK
Google Maps · MapLibre · Leaflet · Cesium …
REACT NATIVE

React Native

DRIVER
Driver that hands off to native
reactnative-for-*
BRIDGE
RN native module
Layer 3
NATIVE SDK
MapConductor native SDK → map SDK
Google Maps · MapLibre · ArcGIS · HERE

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.

Role
Responsibility
Lives in
State
The immutable values your app passes: coordinates, colors, zIndex. fingerPrint() returns a hash of the content.
core
Entity
One record binding the State, the object the provider actually created, and the fingerprint at that moment.
core
Manager
The ledger of entities by id. Hit-testing from a coordinate (find) lives here too.
core
Controller
The operation surface: add / update / clear / find / onCameraChanged / destroy. Decides what changed.
core
Overlay
The drawing unit grouping elements of one kind, and the zIndex that orders them.
core
OverlayRenderer
Translates onAdd / onChange / onRemove / onPostProcess into map SDK calls. Extend the abstract class for that element — AbstractPolygonOverlayRenderer and its siblings — and fill in three methods.
driver
Capable
Declares as a type which elements a provider handles (compositionPolygons / updatePolygon / hasPolygon). There is one per element — PolygonCapableInterface in Kotlin, PolygonCapable in TypeScript. Swift expresses the same thing by whether the provider builds a PolygonController at all.
driver

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.

ANDROID · Kotlin
iOS · Swift
REACT · TypeScript
core/polygon/PolygonManager.kt
PolygonCapableInterface.kt
AbstractPolygonOverlayRenderer.kt
polygon/PolygonManager.swift
polygon/PolygonOverlayRenderer.swift
polygon/PolygonHoleSplit.swift
polygon/PolygonManager.ts
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.

controller/OverlayRendererInterface.ts · the contract shared by all three platforms
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.

android-for-googlemaps · renderer implementation
// 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.

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.

getMapViewHolder() · calling a native feature directly
// 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.

Related pages