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.0in your pubspec (the regenerated registrant loads and initializes it — just keepDartNativePluginRegistrant.registerAll()first inmain())- Two Google Maps API keys (Step 1)
- iOS deployment target 15.0+ — the Maps SDK’s floor. Set
platform :ios, '15.0'inios/Podfile(new projects default lower) orpod installstops 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 aGMSApiKeyentry inInfo.plist; the AppDelegate reads it and hands it toGMSServices.provideAPIKey. - Android:
android/secrets.properties(gitignored) fills thecom.google.android.geo.API_KEYmeta-data entry inAndroidManifest.xmlthrough a manifest placeholder. (A dedicated secrets file, notlocal.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_flutterembeds 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).