Skip to content

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:

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

PatternMethodUse Case
Timer.periodicPoll at fixed intervalSimulated data, sensors
StreamBuilderReact to stream eventsWebSocket, Firebase
setState()Rebuild with new dataAny data source

Next Steps


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.