Use Streams, Firebase Cloud Functions, and FireStore to display real-time changes in an application. This is the main theme of today's blog.
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 a splash screen, wrote a theme, made a custom app bar, made an authentication screen, set up a connection with firebase cloud and emulators, authenticated users, and accessed users’ locations on firebase projects.
We'll pick right where we left off 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 here.
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.
// 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:
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.
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, let's wrap only our favorite icon with stream builder instead.
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 argument for tooggle FavList
//#2
TempleProvider templeProvider =
Provider.of<TempleProvider>(context, listen: false);
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:
And with that latest real-time changes will be reflected in the app.
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 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,
)),
);
}
})
]),
],
),
),
);
}
}
In this exciting blog we:
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.
Also Published here