Update Changes at Firestore at RealTime In Flutter
Flutter App Development Tutorial | Part - XII
Use Streams, Firebase Cloud Functions, and FireStore to display real-time changes in an application. This is the main theme of today's blog.
Intro
Hello and Welcome to the 12th of the Flutter App Development Tutorial Series. This is Nibesh from Khadka's Coding Lounge. We traveled a long way to be here. We created splash screen, wrote a theme, made a custom app bar, made authentication screen, set up a connection with firebase cloud and emulators, authenticated users and accessed user's location on firebase projects.
We'll pick right where we left last time and toggle the favorite icon for temples. We'll use Firestore to store all the favorites list for each user's document in the "users" collection. The Firebase Function will help us fetch the immediate list and update it. While Stream will provide the changes in real-time for users to see. You can find the source code so far from here.
Firebase Cloud Functions To Update Firestore In RealTime
Now, we'll create a cloud function in our index.js file. This function will take an "id" as input. This id is place_id provided by Google Maps Places API.
- It'll search and fetch the current user's document. We've already created an array favTempleList while registering with the onCreate trigger.
- The function will then look through the list and toggle the value.
// Add temple to my fav list:
exports.addToFavList = functions.runWith({
timeoutSeconds: 120,
memory: "128MB"
}).https.onCall(async (data, context) => {
const templeId = data.templeId;
try {
// Get user doc
let userDocRef = await db.collection('users').doc(context.auth.uid).get();
// extract favTempleLis from the doc
// #1
let favTempleList = userDocRef._fieldsProto.favTempleList;
// if fav list is empty
// #2
//============================//
if (favTempleList.arrayValue.values.length === 0) {
// Put the id in the list
const templeList = [templeId];
functions.logger.log("Fav list is empty");
// Update the favTemple list
await db.collection('users').doc(context.auth.uid).set({ favTempleList: templeList }, { merge: true });
//============#2 ends here=====================//
} else {
functions.logger.log("Fav Temple List is not empty");
// Make list of available ids
// firebase providers arrays values as such fileName.arrayValue.values array
// consisting dictionary with stringValue as key and its value is the item stored
// #3
functions.logger.log(favTempleList.arrayValue.values[0]);
let tempArrayValList = favTempleList.arrayValue.values.map(item => item.stringValue);
// if not empty Check if the temple id already exists
// #4
let hasId = tempArrayValList.includes(templeId);
// if so remove the id if no just add the list
// #5
//============================//
if (hasId === true) {
// Usr filter to remove value if exists
let newTemplesList = tempArrayValList.filter(id => id !== templeId);
await db.collection('users').doc(context.auth.uid).set({ favTempleList: newTemplesList }, { merge: true });
//==============#5 ends here===========//
}
// If the id doesnot already exists
// #6
//============================//
else {
// first create a fresh copy
let idList = [...tempArrayValList];
// add the new id to the fresh list
idList.push(templeId);
// update the fresh list to the firesotre
await db.collection('users').doc(context.auth.uid).set({ favTempleList: idList }, { merge: true });
//==============#6 ends here===========//
}
}
} catch (e) { functions.logger.log(e); }
// Return the Strig done.
//#7
return "Done";
});
A couple of lines of code here are on other functions and triggers we've already done. So, let's only go over a few important ones:
We are extracting the firebase field named "favTempleList" from the user documentation. That field is an array.
We're checking if that array is empty. If so, we can just add the new place_id we got from the function call to the array and then update the doc. One thing to notice here is that I am using the merge option for an empty array. That's because this set() method doesn't just set/update a field in a doc, it sets/updates the whole document itself. So, if the {merge: true} option is not provided it'll overwrite every field there, in our case username, email, location, and so on.
Now, we enter the section where the temple list is not empty. Then first we need to extract the list of values(id's) that are already present there.
Check if the new ID, we got from the method call, is already present in the list we got from Firestore.
If the id is already there then we need to remove it. We'll do so by combining filter() and includes() methods. After filtering the current place_id, we'll update the Firestore user doc with the new list of ids.
If the place_id is not there then we need to add it. We'll push the current place_id into the list. And then update the doc like before.
Remember we should always terminate Firebase functions in the end. We don't need any data from this so we'll just terminate it by returning a string.
Provider
Now, we'll need to add a method in our Provider class that'll call the HTTPS callable function we just created. In our TempleProvider class let's add another method addToFavList.
void addToFavList(String templeId) async {
// Instantiate callable from index.js
HttpsCallable addToFav = functions.httpsCallable('addToFavList');
try {
// Run the callable with the passing the current temples ID
await addToFav.call(<String, String>{
'templeId': templeId,
});
} catch (e) {
rethrow;
}
}
We're not updating or returning anything here. That's because we can get data from snapshots from a stream, as you'll see later. BTW, you could add this method to AuthStateProvider because it deals with user collection.
Use Stream & StreamBuilder To Output Changes In RealTime
We can simply use the streams for this. So, we'll connect with Firesotre with streams and update the screen with StreamBuilder. So, where exactly are we using this StreamBuilder? Good question, you see getting real-time updates, means reloading(re-reading) the same collections on every change. It is obviously memory expensive. But it can cost expensive as well since Firebase charges for several reads. So, we don't want to load the list of temples, again and again, to toggle a single favorite icon. So, instead, let's just wrap only our favorite icon with stream builder.
On temple_item_widget.dart make these changes.
Create a function that'll call the addToFavList method from the provider class.
// function to call addToFavList from provider class
// It'll take id and providerclass as input
void toggleFavList(String placeId, TempleProvider templeProvider) {
templeProvider.addToFavList(placeId);
}
Inside the build method of class before the return statement.
// Fetch the user doc as a stream
//#1
Stream<DocumentSnapshot> qSnapShot = FirebaseFirestore.instance
.collection('users')
.doc(FirebaseAuth.instance.currentUser!.uid)
.snapshots();
// Instantiate provider method to pass as an argument for toggle FavList
//#2
TempleProvider templeProvider =
Provider.of<TempleProvider>(context, listen: false);
- As mentioned before we'll get the Stream from Firestore, a document using the current user's ID.
- Instantiate provider method to pass as an argument for toggleFavList function. But make sure to turn off the listener.
Replace FavIcon Section With StreamBuilder
StreamBuilder(
// Use latest update provided by stream
// #1
stream: qSnapShot,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting ||
snapshot.connectionState == ConnectionState.none) {
return const CircularProgressIndicator();
} else {
// Get documentsnaphot which is given from the stream
// #2
final docData = snapshot.data as DocumentSnapshot;
// Fetch favTempleList array from user doc
// # 3
final favList = docData['favTempleList'] as List;
// Check if the curent widget id is among the favTempLlist
// #4
final isFav = favList.contains(widget.itemId);
return Expanded(
child: IconButton(
// Call toggleFavlist method on tap
// #5
onPressed: () => toggleFavList(
widget.itemId, templeProvider),
icon: Icon(
Icons.favorite,
// Show color by value of isFav
// #6
color: isFav ? Colors.red : Colors.grey,
)),
);
}
})
Here, in the StreamBuilder Class we:
- Used the stream we got from Firestore 'users' collection as a stream. That way the latest change is reflected asap.
- When the ConnectionState is done, first we get the latest data from the snapshot, which will be the current user's doc.
- Get the latest updated favTempleList array as a List from the document.
- Check if the current ID exists in the favItemList, the itemId field passed down from the temples screen.
- On press of the heart icon button call the toggleFavList method of the class.
- Change the color of the heart icon based on the presence or absence of itemId on favTempleList.
And with that latest real-time changes will be reflected in the app.
Summary
In this exciting blog we:
- Created a Firebase HTTP Callable function to update a document in Firestore.
- We also wrote a simple instruction in the provider class to handle the callable.
- Lastly using stream and stream builder we displayed real-time changes on our screen.
Final Code
The temple_item_widget.dart file looks like this after the changes.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:temple/screens/temples/providers/temple_provider.dart';
class TempleItemWidget extends StatefulWidget {
// Fields that'll shape the Widget
final String title;
final String imageUrl;
final String address;
final double width;
final String itemId;
const TempleItemWidget(
{required this.title,
required this.imageUrl,
required this.address,
required this.width,
required this.itemId,
// required this.establishedDate,
Key? key})
: super(key: key);
@override
State<TempleItemWidget> createState() => _TempleItemWidgetState();
}
class _TempleItemWidgetState extends State<TempleItemWidget> {
// function to call addToFavList from provider class
// It'll take id and providerclass as input
void toggleFavList(String placeId, TempleProvider templeProvider) {
templeProvider.addToFavList(placeId);
}
@override
Widget build(BuildContext context) {
// Fetch the user doc as a stream
Stream<DocumentSnapshot> qSnapShot = FirebaseFirestore.instance
.collection('users')
.doc(FirebaseAuth.instance.currentUser!.uid)
.snapshots();
// Instantiate provider method to pass as argument for tooggle FavList
TempleProvider templeProvider =
Provider.of<TempleProvider>(context, listen: false);
return SizedBox(
// Card will have height of 260
height: 260,
width: widget.width,
child: Card(
key: ValueKey<String>(widget.itemId),
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
margin: const EdgeInsets.all(10),
child: Column(
// Column will have two children stack and a row
children: [
// Stack will have two children image and title text
Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
child: Image.network(
widget.imageUrl,
fit: BoxFit.cover,
width: widget.width,
height: 190,
),
),
Positioned(
bottom: 1,
child: Container(
color: Colors.black54,
width: widget.width,
height: 30,
child: Text(
widget.title,
style: Theme.of(context)
.textTheme
.headline2!
.copyWith(color: Colors.white),
// softWrap: true,
overflow: TextOverflow.fade,
textAlign: TextAlign.center,
),
),
),
],
),
Row(
// Rows will have two icons as children
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: IconButton(
onPressed: () {
print("Donate Button Pressed");
},
icon: const Icon(
Icons.attach_money,
color: Colors.amber,
),
),
),
StreamBuilder(
// User the ealier stream
stream: qSnapShot,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting ||
snapshot.connectionState == ConnectionState.none) {
return const CircularProgressIndicator();
} else {
// Get documentsnaphot which is given from the stream
final docData = snapshot.data as DocumentSnapshot;
// Fetch favTempleList array from user doc
final favList = docData['favTempleList'] as List;
// Check if the curent widget id is among the favTempLlist
final isFav = favList.contains(widget.itemId);
return Expanded(
child: IconButton(
// Call toggleFavlist method on tap
onPressed: () => toggleFavList(
widget.itemId, templeProvider),
icon: Icon(
Icons.favorite,
// Show color by value of isFav
color: isFav ? Colors.red : Colors.grey,
)),
);
}
})
]),
],
),
),
);
}
}
Show Support
Alright, this is it for this time. The next part will be the last one for this series. We won't be doing anything new there. I'll give you some tasks to do on your own to continue this app and practice what you've learned so far. I'll also enlist some courses, blogs, and books that have helped me a lot to learn flutter.
Please like, comment, and share the article with your friends. Thank you for your time and for those who are subscribing to the blog's newsletter, we appreciate it. Keep on supporting us. This is Nibesh from Khadka's Coding Lounge, a freelancing agency that makes websites and mobile applications.