Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

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 declarative MarkerLayer / PolylineLayer / CircleLayer / PolygonLayer objects, each rendering a List of geometry from the geotypes package (Point, LineString, etc.) with one shared style. Rebuilding the layers list (via setState) 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 shared MarkerLayer style can't express.
  • CircleLayer.radius is a screen-pixel radius (MapLibre's circle-radius paint 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:

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> {
  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:

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> {
  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:

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> {
  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

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.