paint-brush
How to Stream Real-time Changes With Firebase, Firestore and Flutterby@kcl
5,526 reads
5,526 reads

How to Stream Real-time Changes With Firebase, Firestore and Flutter

by Khadka's Coding Lounge.September 14th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Use Streams, Firebase Cloud Functions, and FireStore to display real-time changes in an application. This is the main theme of today's blog.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How to Stream Real-time Changes With Firebase, Firestore and Flutter
Khadka's Coding Lounge. HackerNoon profile picture


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

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.

  1. It'll search and fetch the current user's document. We've already created an array favTempleList while registering with the onCreate trigger.
  2. The function then will 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:

  1. We are extracting the firebase field named "favTempleList" from the user documentation. That field is an array.
  2. 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.
  3. 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.
  4. Check if the new ID, we got from the method call, is already present in the list we got from Firestore.
  5. If the id is already there then we need to remove it. We'll combine filter() and includes() methods. After filtering the current place_id, we'll update the Firestore user doc with the fresh list of ids.
  6. 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.
  7. Remember we should permanently 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, 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);
  1. As mentioned before we'll get the Stream from Firestore, a document using the current user's ID.
  2. 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:

  1. Used the stream we got from Firestore 'users' collection as a stream. That way the latest change is reflected asap.
  2. When the ConnectionState is done, first we get the latest data from the snapshot, which will be the current user's doc.
  3. Get the latest updated favTempleList array as a List from the document.
  4. Check if the current ID exists in the favItemList, which is the itemId field passed down from the temples screen.
  5. On pressing the heart icon button call the toggleFavList method of the class.
  6. 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.


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 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,
                                )),
                          );
                        }
                      })
                ]),
          ],
        ),
      ),
    );
  }
}


Blog By Khadka's Coding Lounge


Summary

In this exciting blog we:

  1. Created a Firebase HTTP Callable function to update a document in Firestore.
  2. We also wrote a simple instruction in the provider class to handle the callable.
  3. Lastly using stream and stream builder we displayed real-time changes on our screen.


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.

Like and Subscribe


Also Published here