Update a Feature in Realtime in Flutter
This tutorial shows how to update map features (markers, polylines, circles) in real-time — essential for live tracking, IoT dashboards, and real-time data visualization.
A Note on the Real API
The real MapMetrics SDK has no Google-Maps-style Marker/Polyline/Circle widgets with their own markers:/polylines:/circles: set parameters on MapMetricsView. Instead:
layers: [...]takes declarativeMarkerLayer/PolylineLayer/CircleLayer/PolygonLayerobjects, each rendering aListof geometry from thegeotypespackage (Point,LineString, etc.) with one shared style. Rebuilding thelayerslist (viasetState) is how you animate them.mapChildren: [WidgetLayer(markers: [Marker(...)])]renders real Flutter widgets pinned to map coordinates when you need per-marker styling, text, or tap handling that a single sharedMarkerLayerstyle can't express.CircleLayer.radiusis a screen-pixel radius (MapLibre'scircle-radiuspaint property), not a geographic buffer in meters — it won't visually grow at the same real-world scale as you zoom in the way a geodesic circle would.
The examples below use WidgetLayer for markers that need individual styling (colors, tap-to-fly) and layers for the trail/search-radius shapes.
Live Marker Position Update
Simulate a moving vehicle by updating a marker's position at regular intervals:
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class RealtimeMarkerScreen extends StatefulWidget {
@override
_RealtimeMarkerScreenState createState() => _RealtimeMarkerScreenState();
}
class _RealtimeMarkerScreenState extends State<RealtimeMarkerScreen> {
MapController? mapController;
Timer? updateTimer;
Position vehiclePosition = Position(2.3522, 48.8566); // lng, lat — Paris
List<Position> trail = [];
final random = Random();
bool isTracking = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Live Tracking'),
actions: [
Switch(
value: isTracking,
onChanged: (value) {
if (value) {
_startTracking();
} else {
_stopTracking();
}
},
activeColor: Colors.white,
),
],
),
body: Stack(
children: [
MapMetricsView(
options: MapOptions(
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
initCenter: vehiclePosition,
initZoom: 14.0,
),
onMapCreated: (MapController controller) {
mapController = controller;
},
layers: [
if (trail.length > 1)
PolylineLayer(
polylines: [LineString(coordinates: trail)],
color: Colors.blue.withValues(alpha: 0.5),
width: 3,
),
],
mapChildren: [
WidgetLayer(
markers: [
Marker(
point: vehiclePosition,
size: const Size.square(32),
alignment: Alignment.center,
child: const Icon(
Icons.directions_car,
color: Colors.blue,
size: 32,
),
),
],
),
],
),
// Status indicator
Positioned(
top: 16,
left: 16,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isTracking ? Colors.green : Colors.grey,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
SizedBox(width: 6),
Text(
isTracking ? 'LIVE' : 'OFFLINE',
style: TextStyle(
color: Colors.white, fontWeight: FontWeight.bold),
),
],
),
),
),
],
),
);
}
void _startTracking() {
setState(() {
isTracking = true;
trail = [vehiclePosition];
});
updateTimer = Timer.periodic(Duration(seconds: 1), (_) {
// Simulate GPS update (small random movement)
setState(() {
vehiclePosition = Position(
vehiclePosition.lng + (random.nextDouble() - 0.3) * 0.002, // lng
vehiclePosition.lat + (random.nextDouble() - 0.4) * 0.002, // lat
);
trail.add(vehiclePosition);
});
});
}
void _stopTracking() {
updateTimer?.cancel();
setState(() {
isTracking = false;
});
}
@override
void dispose() {
updateTimer?.cancel();
super.dispose();
}
}Live Circle Radius Update
Update a circle's (pixel) radius in real-time to show an expanding search area:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class RealtimeCircleScreen extends StatefulWidget {
@override
_RealtimeCircleScreenState createState() => _RealtimeCircleScreenState();
}
class _RealtimeCircleScreenState extends State<RealtimeCircleScreen> {
MapController? mapController;
Timer? expandTimer;
double searchRadiusPx = 10.0; // CircleLayer.radius is in screen pixels
bool isSearching = false;
final Position center = Position(2.3522, 48.8566); // lng, lat
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Expanding Search')),
body: Stack(
children: [
MapMetricsView(
options: MapOptions(
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
initCenter: center,
initZoom: 14.0,
),
onMapCreated: (MapController controller) {
mapController = controller;
},
layers: [
CircleLayer(
points: [Point(coordinates: center)],
radius: searchRadiusPx.toInt(),
color: Colors.blue.withValues(alpha: 0.1),
strokeColor: Colors.blue,
strokeWidth: 2,
),
],
mapChildren: [
WidgetLayer(
markers: [
Marker(
point: center,
size: const Size.square(28),
child: const Icon(Icons.location_on, color: Colors.blue, size: 28),
),
],
),
],
),
// Radius display
Positioned(
bottom: 90,
left: 0,
right: 0,
child: Center(
child: Container(
padding:
EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Radius: ${searchRadiusPx.toInt()}px',
style: TextStyle(color: Colors.white),
),
),
),
),
// Slider control
Positioned(
bottom: 24,
left: 16,
right: 16,
child: Card(
child: Padding(
padding: EdgeInsets.all(8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Slider(
value: searchRadiusPx,
min: 5,
max: 150,
onChanged: (value) {
setState(() {
searchRadiusPx = value;
});
},
),
ElevatedButton(
onPressed: isSearching ? null : _startExpanding,
child: Text(
isSearching ? 'Searching...' : 'Auto Expand'),
),
],
),
),
),
),
],
),
);
}
void _startExpanding() {
setState(() {
isSearching = true;
searchRadiusPx = 10;
});
expandTimer = Timer.periodic(Duration(milliseconds: 50), (_) {
if (searchRadiusPx >= 150) {
expandTimer?.cancel();
setState(() {
isSearching = false;
});
return;
}
setState(() {
searchRadiusPx += 2;
});
});
}
@override
void dispose() {
expandTimer?.cancel();
super.dispose();
}
}Multiple Vehicles Dashboard
Track multiple moving objects simultaneously. Each vehicle needs its own color and tap-to-fly behavior, so this uses WidgetLayer markers rather than a single shared MarkerLayer:
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class FleetTrackerScreen extends StatefulWidget {
@override
_FleetTrackerScreenState createState() => _FleetTrackerScreenState();
}
class _FleetTrackerScreenState extends State<FleetTrackerScreen> {
MapController? mapController;
Timer? updateTimer;
final random = Random();
List<Map<String, dynamic>> vehicles = [
{
'id': 'bus_1',
'name': 'Bus 42',
'position': Position(2.340, 48.860), // lng, lat
'color': Colors.blue,
'speed': '35 km/h',
},
{
'id': 'bus_2',
'name': 'Bus 76',
'position': Position(2.360, 48.850),
'color': Colors.green,
'speed': '28 km/h',
},
{
'id': 'bus_3',
'name': 'Bus 91',
'position': Position(2.330, 48.870),
'color': Colors.orange,
'speed': '42 km/h',
},
];
@override
void initState() {
super.initState();
// Start live updates
updateTimer = Timer.periodic(Duration(seconds: 2), (_) {
setState(() {
for (var vehicle in vehicles) {
final pos = vehicle['position'] as Position;
vehicle['position'] = Position(
pos.lng + (random.nextDouble() - 0.5) * 0.002,
pos.lat + (random.nextDouble() - 0.5) * 0.002,
);
vehicle['speed'] =
'${20 + random.nextInt(30)} km/h';
}
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Fleet Tracker')),
body: Column(
children: [
// Vehicle list
Container(
height: 80,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.all(8),
itemCount: vehicles.length,
itemBuilder: (context, i) {
final v = vehicles[i];
return Card(
child: InkWell(
onTap: () {
final pos = v['position'] as Position;
mapController?.animateCamera(center: pos, zoom: 15.0);
},
child: Padding(
padding: EdgeInsets.all(12),
child: Row(
children: [
Icon(Icons.directions_bus, color: Colors.blue),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(v['name'] as String,
style:
TextStyle(fontWeight: FontWeight.bold)),
Text(v['speed'] as String,
style: TextStyle(
color: Colors.grey, fontSize: 12)),
],
),
],
),
),
),
);
},
),
),
// Map
Expanded(
child: MapMetricsView(
options: MapOptions(
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
initCenter: Position(2.345, 48.857),
initZoom: 13.0,
),
onMapCreated: (MapController controller) {
mapController = controller;
},
mapChildren: [
WidgetLayer(
markers: vehicles.map((v) {
return Marker(
point: v['position'] as Position,
size: const Size.square(32),
alignment: Alignment.center,
child: Icon(
Icons.directions_bus,
color: v['color'] as Color,
size: 32,
),
);
}).toList(),
),
],
),
),
],
),
);
}
@override
void dispose() {
updateTimer?.cancel();
super.dispose();
}
}Realtime Update Patterns
| Pattern | Method | Use Case |
|---|---|---|
Timer.periodic | Poll at fixed interval | Simulated data, sensors |
StreamBuilder | React to stream events | WebSocket, Firebase |
setState() | Rebuild with new data | Any data source |
Next Steps
- Animate Point Along Route — Move along a path
- Animate a Marker — Marker animations
- Locate the User — GPS position tracking
Tip: For production real-time apps, use StreamBuilder with a WebSocket or Firebase Realtime Database instead of Timer.periodic. This is more efficient and battery-friendly as it only updates when new data arrives.