Search code examples
flutterfirebasegoogle-cloud-firestoredisposestream-builder

Firestore: How does StreamBuilder work and how to use it efficiently


I want to know:

Suppose I have a streamBuilder in an application like Chat, so if a user is not on the screen that has the streamBuilder and a new doc is added would it automatically count as a read?

How to properly use StreamBuilder and then Close it when the user leaves the screen for consuming the least amount of reads.

Thank You


Solution

  • By definition :

    A StreamBuilder is a widget that builds its output based on a stream of data. The stream can be anything that emits data over time, such as a Firestore collection. When the stream emits new data, the StreamBuilder will rebuild its output to reflect the new data.

    The StreamBuilder takes two arguments: the stream and a builder function. The builder function will be called whenever the stream emits new data.

    The following code shows how to use a StreamBuilder to display a list of Firestore documents:

    class _HomePageState extends State<HomePage> {
      Stream<QuerySnapshot> stream; // Declaring Stream 
    
      @override
      void initState() {
        super.initState();
    
        stream = FirebaseFirestore.instance
            .collection('documents')
            .snapshots();
      }
    
      @override
      void dispose() {
    // To close a StreamBuilder, you can use the dispose() method. 
        stream.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('StreamBuilder with Debounce and Throttle'),
          ),
          body: StreamBuilder<QuerySnapshot>(
            stream: stream,
            builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
             if (snapshot.hasError) return Text('Something went wrong');
            if (snapshot.connectionState == ConnectionState.waiting) return Text("Loading");
            List<DocumentSnapshot> documents = snapshot.data!.docs;
            return ListView.builder(
                itemCount: documents.length,
                itemBuilder: (BuildContext context, int index) {
                  DocumentSnapshot document = documents[index];
                  return ListTile(
                    title: Text(document['title']),
                    subtitle: Text(document['body']),
                  );
                },
              );
            },
          ),
        );
      }
    }
    
    • When a new document is added to a collection, the StreamBuilder will rebuild the list of documents. This will count as a read from Firestore.

    • But when the user navigates to the new screen the stream will dispose. This is because the stream is only listening for changes to the data while the page is in view. When you go to the next page, the stream will no longer be listening for changes, and you will not be charged for any reads.

    • If you come back to the last page, the stream will be re-created and you will be charged for the reads to rebuild the StreamBuilder. So only use a StreamBuilder if you need to display data that is changing over time(realtime changes). If you only need to display data that is static, you can use a ListView or FutureBuilder or other widget that does not require a stream.

    TLDR : Do some research on your app. Do you really need real-time updates of documents if you do then handle those StreamBuilders cautiously and make sure to dispose of those streams once the user navigates to the new screen.

    Update : When using streamController we also need to dispose it using StreamSubscription.cancel() or calling streamController.cancel() as follows:

    class _MyWidgetState extends State<MyWidget> {
      final StreamController<DocumentSnapshot> _streamController =
          StreamController.broadcast(); 
    
      StreamSubscription _subscription;
    
      @override
      void initState() {
        super.initState();
    
        // Listen to the stream and update the UI when a new document is added.
        _streamController.stream.listen((document) {
          setState(() {
            // Update the UI here.
          });
        },
        onDispose: () {
          // Dispose of the stream controller when the subscription is disposed.
          _streamController.dispose();
        });
    
      }
    
      @override
      void dispose() {
        // Dispose of the stream controller when the widget is disposed.
        _streamController.dispose(); // when used just StreamController
        _subscription.cancel(); // when used StreamSubscription 
        super.dispose();
      }
    }
    

    Reference :

    1. One-time Read
    2. Realtime changes
    3. StreamBuilder class