Real-Time Updates with Streambuilder in Flutter and Google Maps (Firestore)
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
arbiter
Updated on December 31, 2022Comments
-
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 over 2 yearsI 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 over 2 yearsTo 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 over 2 yearsMany thanks for a comprehensive answer, I'll test it out and get back to this thread asap, hopefully it works.
-
Louis Deveseleer over 2 yearsDid you manage to implement the solution or do you need more help? If it helped, could you mark my answer as accepted?