Real-Time Updates with Streambuilder in Flutter and Google Maps (Firestore)

1,370

So you want to subscribe to a stream coming from a firestore collection, and use this data to update markers on a map in real-time, is that right?

Please note that you absolutely need to separate your services from your widgets, or you'll end up quickly with a jumbled mess of a code.

In one file you have the class defining access to your database (such as API calls, or Firestore in this case). ANY write or read with Firestore will go through this service.

class FirestoreService {
  FirestoreService._();
  static final instance = FirestoreService._();

  Stream<List<T>> collectionStream<T>({
    required String path,
    required T Function(Map<String, dynamic> data, String documentID) builder,
  }) {
    Query query = FirebaseFirestore.instance.collection(path);
    final Stream<QuerySnapshot> snapshots = query.snapshots();
    return snapshots.map((snapshot) {
      final result = snapshot.docs
          .map((snapshot) =>
              builder(snapshot.data() as Map<String, dynamic>, snapshot.id))
          .where((value) => value != null)
          .toList();
      return result;
    });
  }
}

In another file, you'll have the repository service, that contains the CRUD operations. Each operation makes a call to the DB service (defined in the previous step) and uses the objects's serialization (toJson, send to DB) or parsing (fromJson, get from DB) methods.

class MarkersRepo {
  final _service = FirestoreService.instance;

  Stream<List<Marker>> getMarkers() {
    return _service.collectionStream(
      path: 'marker',
      builder:(json, docId){
        return Marker.fromJson(json);
      }
    );
  }
}

In another file, you define your Marker object model with the serialization and parsing methods. Please don't use strings to access directly the document properties in your code, as that is error-prone and again will cause messy code. Instead, you define those once and for all in the object model.

class Marker{
  //properties of the object such as location, name and description
  //toJson and fromJson methods
}

And finally in your widget file, as you noted yourself you are only reading the document once, in the initstate, so the view does not update. Instead, one simple option is to have a StreamBuilder inside your build method, extract the markers there and then display them:

Widget build(Buildcontext context){
  return StreamBuilder(
    stream: MarkersRepos().getMarkers(),
    builder:(context, snapshot){
     //check for connection state and errors, see streambuilder documentation
      final List<Marker> markers = snapshot.data;
      return loadMap(markers);
    }
  );
}

EDIT: added more details

Share:
1,370
arbiter
Author by

arbiter

Updated on December 31, 2022

Comments

  • arbiter
    arbiter over 1 year

    I have a sample app that I am building that fetches multiple markers from Firestore and displays them on Google Maps.

    The issue is that the changes (markers added/deleted/updated) aren't displayed in real time since it only rebuilds in init state. I think that I could use Stream Builder to listen to the changes and update them in real time.

    I am not sure how to do it with Google Maps and Firestore, since the syntax is kinda weird with snapshots, I tried many ways, but not successful. Here's my code:

      @override
      void initState() {
        populateMarkers();
        super.initState();
       }
    
    
    populateMarkers() {
        FirebaseFirestore.instance.collection('marker').get().then((documents) {
          if (documents.docs.isNotEmpty) {
            for (int i = 0; i < documents.docs.length; i++) {
              initMarker(
                  documents.docs[i].data(), documents.docs[i].id); 
            }
          }
        });
          }
    
      void initMarker(request, requestId) {
        
        var markerIdVal = requestId;
        final MarkerId markerId = MarkerId(markerIdVal);
        //creating new markers
        final Marker marker = Marker(
          markerId: markerId,
          position:
              LatLng(request['location'].latitude, request['location'].longitude),
          infoWindow:
              InfoWindow(title: request['name'], snippet: request['description']),
          onTap: () => print('Test'),
        );
        setState(() {
          markers[markerId] = marker;
          //print(markerId);
        });
      }
    
    Widget loadMap() {
        return GoogleMap(
          
          markers: Set<Marker>.of(markers.values),
          mapType: MapType.normal,
          initialCameraPosition:
              CameraPosition(target: LatLng(43.8031287, 20.453008), zoom: 12.0),
          onMapCreated: onMapCreated,
    
          // },
        );
      }
    

    and in the buider, I just call loadMap() function as the body. As I mentioned, this works fine, but I would like to use Stream Builder for these functions to update in real time. Any ideas how to do it?

    This thread has a similar issue, but I feel it's not the best way to do it:

    Flutter - Provide Real-Time Updates to Google Maps Using StreamBuilder

    Here is the full code in case someone wants to test it out:

    import 'package:cloud_firestore/cloud_firestore.dart';
    import 'package:flutter/material.dart';
    import 'package:geolocator/geolocator.dart';
    import 'package:google_maps_flutter/google_maps_flutter.dart';
    
    class MapScreen extends StatefulWidget {
      @override
      _MapScreenState createState() => _MapScreenState();
    }
    class _MapScreenState extends State<MapScreen> {
      Map<MarkerId, Marker> markers = <MarkerId, Marker>{}; //--> google
      GoogleMapController _controller;
      
       @override
      void initState() {
        populateMarkers();
        super.initState();
      }
      
       onMapCreated(GoogleMapController controller) async {
        _controller = controller;
        String value = await DefaultAssetBundle.of(context)
            .loadString('assets/map_style.json');
        _controller.setMapStyle(value);
      }
    
      populateMarkers() {
        FirebaseFirestore.instance.collection('marker').get().then((documents) {
          if (documents.docs.isNotEmpty) {
            for (int i = 0; i < documents.docs.length; i++) {
              initMarker(
                  documents.docs[i].data(), documents.docs[i].id); //maybe error
              //documents.docs[i].data, documents.docs[i].id
            }
          }
        });
      }
      
       void initMarker(request, requestId) {
       
        var markerIdVal = requestId;
        final MarkerId markerId = MarkerId(markerIdVal);
        //creating new markers
        final Marker marker = Marker(
          markerId: markerId,
          position:
              LatLng(request['location'].latitude, request['location'].longitude),
          infoWindow:
              InfoWindow(title: request['name'], snippet: request['description']),
          onTap: () => print('Test'),
        );
        setState(() {
          markers[markerId] = marker;
          //print(markerId);
        });
      }
    
      Widget loadMap() {
        return GoogleMap(
          // markers: markers.values.toSet(),
          markers: Set<Marker>.of(markers.values),
          mapType: MapType.normal,
          initialCameraPosition:
              CameraPosition(target: LatLng(44.8031267, 20.432008), zoom: 12.0),
          onMapCreated: onMapCreated,
          cameraTargetBounds: CameraTargetBounds(LatLngBounds(
              northeast: LatLng(44.8927468, 20.5509553),
              southwest: LatLng(44.7465138, 20.2757283))),
          mapToolbarEnabled: false,
          //  onMapCreated: (GoogleMapController controller) {
          //   _controller = controller;
          // },
        );
      }
      
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    
    //-----------------------------------
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('App'),
            centerTitle: true,
            backgroundColor: Colors.blue[700],
            actions: [
              // ElevatedButton(
              //   onPressed: () => refresh(),
              //   child: Text('REFRESH'),
              // )
            ],
          ),
          body: loadMap(),
            );
      }
    }
    
    • Claudio Castro
      Claudio Castro over 2 years
      I use google maps in a vehicle tracking app. What I do is update the markers every 10 seconds. I do this with a streambuilder that is called by a timer and always before loading the data I do GoogleMap.of(_key).clearMarkers(); to clear all marks. Thus, those that were excluded disappear and the rest is updated.
    • Claudio Castro
      Claudio Castro over 2 years
      To put the marks from the streambuilder, I do GoogleMap.of(_key).addMarkerRaw( GeoCoord(item.latidute, item.longitude), icon: icone, infoSnippet: "<b>${item.listaMotoristas}</b>", label: item.placa, ); in each item in snapshot.
  • arbiter
    arbiter over 2 years
    Many thanks for a comprehensive answer, I'll test it out and get back to this thread asap, hopefully it works.
  • Louis Deveseleer
    Louis Deveseleer over 2 years
    Did you manage to implement the solution or do you need more help? If it helped, could you mark my answer as accepted?