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.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Live Marker Position Update
Simulate a moving vehicle by updating a marker's position at regular intervals:
dart
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> {
MapMetricsController? mapController;
Timer? updateTimer;
LatLng vehiclePosition = LatLng(48.8566, 2.3522);
List<LatLng> 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: [
MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: vehiclePosition,
zoom: 14.0,
),
markers: {
Marker(
markerId: MarkerId('vehicle'),
position: vehiclePosition,
icon: BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueBlue),
infoWindow: InfoWindow(title: 'Vehicle'),
),
},
polylines: trail.length > 1
? {
Polyline(
polylineId: PolylineId('trail'),
points: trail,
color: Colors.blue.withOpacity(0.5),
width: 3,
),
}
: {},
),
// 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 = LatLng(
vehiclePosition.latitude + (random.nextDouble() - 0.4) * 0.002,
vehiclePosition.longitude + (random.nextDouble() - 0.3) * 0.002,
);
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 radius in real-time to show an expanding search area:
dart
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> {
MapMetricsController? mapController;
Timer? expandTimer;
double searchRadius = 200.0; // meters
bool isSearching = false;
final LatLng center = LatLng(48.8566, 2.3522);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Expanding Search')),
body: Stack(
children: [
MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: center,
zoom: 14.0,
),
circles: {
Circle(
circleId: CircleId('search_area'),
center: center,
radius: searchRadius,
fillColor: Colors.blue.withOpacity(0.1),
strokeColor: Colors.blue,
strokeWidth: 2,
),
},
markers: {
Marker(
markerId: MarkerId('center'),
position: center,
infoWindow: InfoWindow(title: 'Search Center'),
),
},
),
// 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: ${searchRadius.toInt()}m',
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: searchRadius,
min: 100,
max: 2000,
onChanged: (value) {
setState(() {
searchRadius = value;
});
},
),
ElevatedButton(
onPressed: isSearching ? null : _startExpanding,
child: Text(
isSearching ? 'Searching...' : 'Auto Expand'),
),
],
),
),
),
),
],
),
);
}
void _startExpanding() {
setState(() {
isSearching = true;
searchRadius = 200;
});
expandTimer = Timer.periodic(Duration(milliseconds: 50), (_) {
if (searchRadius >= 2000) {
expandTimer?.cancel();
setState(() {
isSearching = false;
});
return;
}
setState(() {
searchRadius += 20;
});
});
}
@override
void dispose() {
expandTimer?.cancel();
super.dispose();
}
}Multiple Vehicles Dashboard
Track multiple moving objects simultaneously:
dart
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> {
MapMetricsController? mapController;
Timer? updateTimer;
final random = Random();
List<Map<String, dynamic>> vehicles = [
{
'id': 'bus_1',
'name': 'Bus 42',
'position': LatLng(48.860, 2.340),
'color': BitmapDescriptor.hueBlue,
'speed': '35 km/h',
},
{
'id': 'bus_2',
'name': 'Bus 76',
'position': LatLng(48.850, 2.360),
'color': BitmapDescriptor.hueGreen,
'speed': '28 km/h',
},
{
'id': 'bus_3',
'name': 'Bus 91',
'position': LatLng(48.870, 2.330),
'color': BitmapDescriptor.hueOrange,
'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 LatLng;
vehicle['position'] = LatLng(
pos.latitude + (random.nextDouble() - 0.5) * 0.002,
pos.longitude + (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 LatLng;
mapController?.animateCamera(
CameraUpdate.newLatLngZoom(pos, 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'],
style:
TextStyle(fontWeight: FontWeight.bold)),
Text(v['speed'],
style: TextStyle(
color: Colors.grey, fontSize: 12)),
],
),
],
),
),
),
);
},
),
),
// Map
Expanded(
child: MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.857, 2.345),
zoom: 13.0,
),
markers: vehicles.map((v) {
return Marker(
markerId: MarkerId(v['id']),
position: v['position'] as LatLng,
icon: BitmapDescriptor.defaultMarkerWithHue(
v['color'] as double),
infoWindow: InfoWindow(
title: v['name'],
snippet: v['speed'],
),
);
}).toSet(),
),
),
],
),
);
}
@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.