Architekturübersicht
MapConductor fungiert als Brücke zwischen der App und verschiedenen Karten-SDKs. Die App ruft nur die einheitliche Karten-API auf, und MapConductor leitet die Verarbeitung an den ausgewählten Anbieter weiter. Unter Android (Kotlin + Compose), iOS (Swift + SwiftUI) und React (Web) sind sowohl die Aufteilung der Pakete als auch die interne Aufgabenteilung gleich gestaltet.
01 · Gesamtbild
Der Ablauf ist auf jeder Plattform gleich: App → Einheitliche Karten-API → Core → Verschiedene Karten-SDK-Treiber → Verschiedene Karten-SDKs. Nur die oberen beiden Ebenen sind vom Code, den die App schreibt; die unteren beiden Ebenen können ausgetauscht werden.
UI und Geschäftslogik
Einheitliche Karten-API
Kernfunktionen (Karte, Marker, Formen, Ereignisse)
Anbieter-Treiber
Karten-SDKs (nativ / JavaScript)
Wenn der Anbieter gewechselt wird, werden nur die untersten beiden Ebenen ausgetauscht. Da der Code der Benutzeroberfläche gegen die einheitliche API geschrieben ist, funktioniert er unverändert weiter.
02 · Plattformspezifische Konfiguration
Core und Treiber sind als Pakete getrennt. Die App fügt nur Core und den Treiber des verwendeten Anbieters als Abhängigkeiten hinzu.
android-sdk-core enthält das gemeinsame Modell und die Differenzanwendung, während android-sdk-compose Statushalter für Compose bereitstellt. GeoJSON, Clustering und Heatmaps sind als zusätzliche Module (android-geo-layer / android-marker-clustering / android-heatmap) eigenständig.
ios-sdk-core enthält das gemeinsame Modell und die Basis für die SwiftUI-Ansichten, und jede SwiftUI-Ansicht wird von den Anbieterpaketen (ios-for-*) bereitgestellt. Der MapKit-Treiber ist für das native Apple SDK gedacht und kann ohne zusätzliche Gebühren oder API-Schlüssel verwendet werden.
js-sdk-core ist ein von React unabhängiges TypeScript-Core mit einem gemeinsamen Modell und Diff-Anwendung (fast die gleiche Struktur wie das core für Kotlin / Swift). js-sdk-react portiert dies nach React und stellt Komponenten wie Marker / Polygon / Polyline / Circle / GroundImage / RasterLayer / InfoBubble bereit. Die Kartenansicht selbst wird von den einzelnen Treibern (react-for-*) als Kombination aus „Ansicht + Hook“ veröffentlicht, zum Beispiel MapLibreMapView / MapLibreMapView2D und useMapLibreViewState. GeoJSON, Clustering, Heatmap und Icons sind als zusätzliche Pakete (react-geo-layer / react-marker-clustering / react-heatmap / react-icons) unabhängig.
03 · Ebenenzusammensetzung
Damit sie plattformübergreifend wiederverwendet werden können, sind die Verantwortlichkeiten in 6 Ebenen unterteilt. Die oberen Ebenen kennen die Implementierungen der unteren Ebenen nicht.
Durch diese Trennung kann Code, der sich darauf konzentriert, „was mit der Karte gemacht werden soll“ statt „welche Plattform“, direkt in mehreren Umgebungen bereitgestellt werden. Plattformspezifische Unterschiede werden grundsätzlich nur auf die Installations- / Deklarationsmethoden der Cross-Platform-Ebene beschränkt.
Im Fall von React Native
Web und React Native nutzen denselben Code, den die App schreibt. Auch die Zustandsobjekte und Komponenten sind identisch; Sie müssen nur das ausgewählte MapView austauschen. Der Unterschied liegt darunter. Im Web ruft der Treiber eine JavaScript-Kartenbibliothek in derselben Laufzeitumgebung auf, während React Native über eine Bridge zum nativen SDK von MapConductor gelangt und am Ende die nativen Karten-SDKs rendern.
React
React Native
Die Ebene 3 in der obigen Tabelle ist die Bridge. Im Web ist sie nicht vorhanden, da UI und Rendering in derselben Laufzeitumgebung abgeschlossen werden. In React Native ist sie erforderlich, da beide in unterschiedliche Laufzeitumgebungen aufgeteilt sind. Nur diese Ebene unterscheidet sich je nach Plattform; darüber bleibt alles unverändert.
Die mit React Native nutzbaren Provider sind Google Maps, MapLibre, ArcGIS und HERE – insgesamt vier. Da nur Anbieter mit einem entsprechenden nativen Modul möglich sind, ist die Anzahl geringer als die 13 Provider im Web.
04 · Interne Struktur von Core
Core ist für Elemente wie Marker, Polylines, Polygone, Kreise, Bodenbilder und Rasterebenen so aufgebaut, dass dieselben sieben Rollen wiederholt werden. Wenn man eine versteht, lassen sich die anderen Elemente auf dieselbe Weise nachvollziehen.
Wichtig ist die Position der Grenze. Die Core-Seite (State / Entity / Manager / Controller / Overlay) ist in fast gleichem Code in den 3 Sprachen geschrieben, und für jeden Anbieter müssen nur der Renderer und die Deklaration, dass dieser die Elemente verarbeiten kann, geschrieben werden.
PolygonCapableInterface.kt
AbstractPolygonOverlayRenderer.kt
polygon/PolygonOverlayRenderer.swift
polygon/PolygonHoleSplit.swift
polygon/PolygonCapable.ts
AbstractPolygonOverlayRenderer.ts
05 · Ablauf der Differenzanwendung
Die App übergibt nur ein „Array des Zustands, der jetzt bestehen sollte“. Core berechnet die Differenz zum vorherigen Mal, teilt sie in Zunahmen, Änderungen und Entfernungen und übergibt sie an den Treiber. Da die Kartenobjekte nicht neu erstellt werden, ist die Darstellung auch bei einer großen Menge von Elementen stabil.
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 empfängt sowohl die vorherige Entity (prev) als auch den aktuellen State (current). Der Treiber kann nur die geänderten Eigenschaften aktualisieren, und die Signatur ist auch in Kotlin / Swift gleich.
06 · Vom Treiber zu implementierende Dinge
Die Arbeit zur Unterstützung eines neuen Karten-SDK beschränkt sich darauf, von dem abstrakten Renderer für jedes Element zu erben und die 3 Operationen auszufüllen. Das Core-Modell und die Differenzlogik werden nicht berührt.
// Der Treiber erbt vom abstrakten Renderer und füllt nur drei Operationen aus
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) = /* Nur die Differenz anwenden */
override suspend fun removePolygon(entity: PolygonEntityInterface<GoogleMapActualPolygon>) =
entity.polygon.remove()
}Nur die 3 Vorgänge Erstellen, Aktualisieren und Löschen sind anbieterspezifisch. Die Differenzbestimmung, die Verwaltung des Registers und der Hit-Test werden aufgerufen, nachdem Core sie bereits erledigt hat.
// Dieselbe Form wie in Kotlin: vom abstrakten Renderer erben und drei Operationen ausfüllen
final class MapKitPolygonOverlayRenderer: AbstractPolygonOverlayRenderer<MKPolygon> {
override func createPolygon(state: PolygonState) async -> MKPolygon? { /* MKPolygon(...) */ }
override func updatePolygonProperties(polygon, current, prev) async -> MKPolygon? { /* Nur die Differenz anwenden */ }
override func removePolygon(entity: PolygonEntity<MKPolygon>) async { /* mapView.removeOverlay */ }
}
// Der Controller nutzt die generische Implementierung aus Core unverändert
final class MapKitPolygonController: PolygonController<MKPolygon, MapKitPolygonOverlayRenderer> { }In Swift bilden „der Controller, den dieser Anbieter hat“ die Funktionsliste. Funktionen, die die SDK-Seite nicht hat, wie Löcher in Polygonen oder Markierungsanimationen, werden ausgedrückt, indem der entsprechende Renderer nicht implementiert wird oder zu einer alternativen Darstellung auf der Core-Seite (wie PolygonHoleSplit, der in einfache Ringe ohne Löcher aufteilt) gewechselt wird.
// Dieselbe Form wie in Kotlin und Swift: vom abstrakten Renderer erben und drei Operationen ausfüllen
export class MapLibrePolygonOverlayRenderer extends AbstractPolygonOverlayRenderer<
MapLibreMapViewHolder,
MapLibreActualPolygon
> {
async createPolygon(state: PolygonState) { /* Das GeoJSON-Feature erzeugen */ }
async updatePolygonProperties({ current, prev }) { /* Nur die Differenz anwenden */ }
async removePolygon(entity: PolygonEntity<MapLibreActualPolygon>) { /* Entfernen */ }
// Nach dem Anwenden der Differenz die Quelle aus den verbliebenen Entities neu schreiben
override async onPostProcess() { this.layer.draw(this.polygonManager.allEntities()); }
}
// Was ein Treiber beherrscht, wird durch Implementieren der Capable-Interfaces als Typ deklariert
export class MapLibreViewController extends BaseMapViewController
implements MapViewControllerInterface, MarkerCapable, PolygonCapable, /* … */ { }Auch in TypeScript sind nur die drei Vorgänge Erstellen, Aktualisieren und Löschen providerspezifisch. Bei SDKs, die wie MapLibre Ebenen in einem Rutsch zeichnen, werden Unterschiede in der Zeichnungsweise — wie das Neuschreiben der gesamten Source in onPostProcess statt dem Löschen einzelner Elemente — im Renderer gekapselt. Die Elemente, die der Treiber verarbeiten kann, werden durch das von MapLibreViewController implementierte Capable-Interface (z. B. PolygonCapable) dargestellt, und die Verfügbarkeit ist am Typ erkennbar.
07 · Umfang der Abstraktion und Notausgänge
MapConductor kapselt nicht alle Funktionen jedes Karten-SDKs. Der Fokus liegt auf gemeinsam genutzten, häufig verwendeten Operationen; für Anforderungen, die darüber hinausgehen, werden zwei Auswege angeboten.
// map der gemeinsamen Schnittstelle ist unknown — vor der Nutzung auf den Anbietertyp einengen
const holder = mapViewState.getMapViewHolder();
const map = holder?.map as maplibregl.Map | undefined;
map?.addLayer({ id: 'buildings', type: 'fill-extrusion', source: 'composite' });Ein Entwurf, der die gemeinsame API einfach hält und gleichzeitig die spezifischen Stärken der einzelnen Anbieter nicht beeinträchtigt. Weitere Informationen finden Sie unter „Native Erweiterungen“.