Tutorial · 18 min · advanced

A native map

The real Google Maps SDK view in your widget tree — markers, camera moves, map types, with Dart overlays floating above the native surface.

What you'll build — running on device.

Maps are the platform-view stress test: gesture-heavy, GPU-rendered, and instantly wrong-feeling when embedded badly. In DartNative there’s no embedding to get wrong — every widget you write mounts a real platform view in one native hierarchy, and dartnative_google_maps makes the map one of them: the actual Google Maps SDK view, GMSMapView on iOS, MapView on Android. You’ll build a city-hopping map with markers, camera animation, map-type switching, and Dart UI floating on top.

What you need

  • A project from Your first DartNative app
  • dartnative_google_maps: ^0.1.0 in your pubspec (the regenerated registrant loads and initializes it — just keep DartNativePluginRegistrant.registerAll() first in main())
  • Two Google Maps API keys (Step 1)
  • iOS deployment target 15.0+ — the Maps SDK’s floor. Set platform :ios, '15.0' in ios/Podfile (new projects default lower) or pod install stops with a deployment-version error.

Step 1 — API keys, the safe way

Google Maps won’t draw a single tile without an API key, and the plugin takes it natively, never in Dart. That’s deliberate: a native-side key can be locked to your exact app identity in the Google Cloud console, and it never appears in your Dart source. On each platform the key lives in a gitignored file and flows into native config at build time:

  • iOS: ios/DartNative/Secrets.xcconfig (gitignored) fills a GMSApiKey entry in Info.plist; the AppDelegate reads it and hands it to GMSServices.provideAPIKey.
  • Android: android/secrets.properties (gitignored) fills the com.google.android.geo.API_KEY meta-data entry in AndroidManifest.xml through a manifest placeholder. (A dedicated secrets file, not local.properties — the dn tool rewrites that one on every run and would drop your key.)

Create the two keys in the Google Cloud console — you’ll need a project with billing enabled, even inside the free tier — and restrict each to one platform: the Android key by package name + SHA-1 and to Maps SDK for Android, the iOS key by bundle ID and to Maps SDK for iOS. The plugin README on dartpub.dev has the click-by-click console walkthrough plus the few copy-paste lines of native wiring (AppDelegate, build.gradle.kts, manifest), and its example app ships with all of it already in place.

Step 2 — The map

Everything the map shows is ordinary Flutter state — the camera, the map type, and the markers are plain fields on your State, starting over San Francisco:

CameraPosition _camera = const CameraPosition(
  latitude: 37.7749,
  longitude: -122.4194,
  zoom: 12,
);

The widget itself goes in a Stack, filled to the screen:

Positioned.fill(
  child: GoogleMaps(
    initialCameraPosition: _camera,
    mapType: _mapType,
    markers: _markers,
    zoomGesturesEnabled: true,
    scrollGesturesEnabled: true,
    onMarkerTap: (id) => setState(() => _tappedMarker = id),
  ),
),

Pan and zoom are handled inside the native view — the Maps SDK’s own gesture handling, tile streaming, and GPU rendering — while your Dart chips and cards float above it as ordinary Stack children: siblings in the same native hierarchy, not texture layers. And switching the look is a setState — the tutorial’s bottom chip row does onTap: () => setState(() => _mapType = t) for each renderable MapType: normal, satellite, terrain, and hybrid. (The enum has a fifth value, none, which draws no tiles at all — it exists for apps that render their own base layer, so the demo skips it.)

Step 3 — Markers

Markers are declarative, like children in a widget tree: the list you pass is what’s on the map. DartNative’s reconciler — the layer that turns widget rebuilds into minimal native mutations — diffs it, so you add, remove, or replace pins by rebuilding the list:

_markers = [
  Marker(id: city.name, latitude: city.lat, longitude: city.lng),
];

In the demo this is wired to the city chips: selecting a city rebuilds _markers with that city’s single pin, so the pin you see drop at the center after each camera flight is this list at work.

Taps flow the other way: the SDK reports the hit and onMarkerTap hands you back the id you chose. The tutorial stores it — onMarkerTap: (id) => setState(() => _tappedMarker = id) — and floats a Dart card over the native map reading Marker tapped: <id>. Try it: fly to a city, then tap its pin.

Step 4 — Move the camera

The camera is state too: hand the widget a new CameraPosition and the map flies there with the SDK’s own flight animation — no controller, no imperative animateCamera call. The city chips all funnel into one method:

const _cities = [
  (name: 'San Francisco', lat: 37.7749, lng: -122.4194),
  (name: 'London', lat: 51.5074, lng: -0.1278),
  (name: 'Tokyo', lat: 35.6762, lng: 139.6503),
];
void _goTo(({String name, double lat, double lng}) city) {
  setState(() {
    _camera = CameraPosition(
      latitude: city.lat + (_nonce * 1e-10),
      longitude: city.lng + (_nonce * 1e-10),
      zoom: 12,
    );
    _nonce++;
    _markers = [
      Marker(id: city.name, latitude: city.lat, longitude: city.lng),
    ];
  });
}

One practical trick in there: identical camera positions are deduped, so re-selecting the current city would normally do nothing. The _nonce counter nudges the coordinates by a sub-nanometer epsilon on every move, so each tap re-animates. (The odd-looking parameter type is a Dart record — the shape of the _cities entries above.)

Why this is native

Flutter’s google_maps_flutter embeds the map as a platform-view island — with the compositing seams, gesture arbitration, and performance cliffs that pattern brings. Here there is no island: the map is a regular native view in the same hierarchy as everything else, your overlays are siblings rather than texture layers, and gestures go straight to the SDK. It behaves exactly like the map in the Google Maps app, because it’s the same view.

The finished code

One file in the public repo — dn create ., dn run (after the API-key setup above).

Open the finished code →