Flutter: Streambuilder causing far too many reads on Firestore
Asked Answered
B

1

12

I am trying to build a simple quotes Flutter app, where I show a list of quotes and allow users to 'like' the quotes. I am using the Streambuilder for that. My problem is that the Firestore usage dashboard shows a very high number of reads (almost 300 per user), even though I have 50 quotes at max. I have a hunch that something in my code is causing Streambuilder to trigger multiple times (maybe the user 'liking' a quote) and also the Streambuilder is loading ALL the quotes instead of only those that are in the user's viewport. Any help on how to fix this to reduce the number of reads would be appreciated.

import 'dart:convert';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:positivoapp/utilities.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:share/share.dart';


class QuotesScreen extends StatefulWidget {
  @override
  QuotesScreenLayout createState() => QuotesScreenLayout();
}

class QuotesScreenLayout extends State<QuotesScreen> {
  List<String> quoteLikeList = new List<String>();

  // Get Goals from SharedPrefs
  @override
  void initState() {
    super.initState();
    getQuoteLikeList();
  }

  Future getQuoteLikeList() async {
    if (Globals.globalSharedPreferences.getString('quoteLikeList') == null) {
      print("No quotes liked yet");
      return;
    }

    String quoteLikeListString =
    Globals.globalSharedPreferences.getString('quoteLikeList');
    quoteLikeList = List.from(json.decode(quoteLikeListString));
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
            padding: const EdgeInsets.all(10.0),
            child: StreamBuilder<QuerySnapshot>(
              stream: Firestore.instance
                  .collection(FireStoreCollections.QUOTES)
                  .orderBy('timestamp', descending: true)
                  .snapshots(),
              builder: (BuildContext context,
                  AsyncSnapshot<QuerySnapshot> snapshot) {
                if (snapshot.hasError)
                  return new Text('Error: ${snapshot.error}');
                switch (snapshot.connectionState) {
                  case ConnectionState.waiting:
                    return Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        new CircularProgressIndicator(),
                        new Text("Loading..."),
                      ],
                    );
                  default:
                    print('Loading Quotes Stream');
                    return new ListView(
                      children: snapshot.data.documents
                          .map((DocumentSnapshot document) {
                        return new QuoteCard(
                          quote:
                              Quote.fromMap(document.data, document.documentID),
                          quoteLikeList: quoteLikeList,
                        );
                      }).toList(),
                    );
                }
              },
            )),
      ),
    );
  }
}

class QuoteCard extends StatelessWidget {
  Quote quote;
  final _random = new Random();
  List<String> quoteLikeList;

  QuoteCard({@required this.quote, @required this.quoteLikeList});

  @override
  Widget build(BuildContext context) {
    bool isLiked = false;
    String likeText = 'LIKE';
    IconData icon = Icons.favorite_border;
    if (quoteLikeList.contains(quote.quoteid)) {
      icon = Icons.favorite;
      likeText = 'LIKED';
      isLiked = true;
    }

    return Center(
      child: Card(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Container(
              constraints: new BoxConstraints.expand(
                height: 350.0,
                width: 400,
              ),
              child: Stack(children: <Widget>[
                Container(
                  decoration: BoxDecoration(
                    image: DecorationImage(
                      colorFilter: new ColorFilter.mode(
                          Colors.black.withOpacity(0.25), BlendMode.darken),
                      image: AssetImage('images/${quote.imageName}'),
                      fit: BoxFit.cover,
                    ),
                  ),
                ),
                Center(
                  child: Padding(
                    padding: const EdgeInsets.all(15.0),
                    child: Text(
                      quote.quote,
                      textAlign: TextAlign.center,
                      style: TextStyle(
                          fontSize: 30.0,
                          fontFamily: 'bold',
                          fontWeight: FontWeight.bold,
                          color: Color.fromRGBO(255, 255, 255, 1)),
                    ),
                  ),
                ),
              ]),
            ),
            Padding(
              padding: EdgeInsets.fromLTRB(18, 10, 10, 0),
              child: Text(
                'Liked by ${quote.numLikes} happy people',
                textAlign: TextAlign.left,
                style: TextStyle(
                    fontFamily: 'bold',
                    fontWeight: FontWeight.bold,
                    color: Colors.black),
              ),
            ),
            ButtonBar(
              alignment: MainAxisAlignment.start,
              children: <Widget>[
                FlatButton(
                  child: UtilityFunctions.buildButtonRow(Colors.red, icon, likeText),
                  onPressed: () async {
                    // User likes / dislikes this quote, do 3 things
                    // 1. Save like list to local storage
                    // 2. Update Like number in Firestore
                    // 3. Toggle isLiked
                    // 4. Setstate - No need

                    // Check if the quote went from liked to unlike or vice versa

                    if (isLiked == false) {
                      // False -> True, increment, add to list
                      quoteLikeList.add(quote.quoteid);

                      Firestore.instance
                          .collection(FireStoreCollections.QUOTES)
                          .document(quote.documentID)
                          .updateData({'likes': FieldValue.increment(1)});

                      isLiked = true;
                    } else {
                      // True -> False, decrement, remove from list
                      Firestore.instance
                          .collection(FireStoreCollections.QUOTES)
                          .document(quote.documentID)
                          .updateData({'likes': FieldValue.increment(-1)});
                      quoteLikeList.remove(quote.quoteid);

                      isLiked = false;
                    }

                    // Write to local storage
                    String quoteLikeListJson = json.encode(quoteLikeList);
                    print('Size of write: ${quoteLikeListJson.length}');
                    Globals.globalSharedPreferences.setString(
                        'quoteLikeList', quoteLikeListJson);

                    // Guess setState(); will happen via StreamBuilder - Yes
//                    setState(() {});
                  },
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Bloodsucker answered 6/6, 2020 at 1:9 Comment(3)
Are you sure it's not just actually just leaving he Firestore console open on a collection with busy writes? The console costs reads as well. As for your code, you will have to instrument it with some sort of counter that tells you exactly what it's doing.Redemptioner
@DougStevenson: I only open the console once a day so that may not be the issue. I am seeing 24K writes with just 80 users. Is there any documentation on how I can instrument Firebase document reads from a StreamBuilder?Bloodsucker
You will have to write your own code to capture and log usage. Firestore doesn't do that for you.Redemptioner
C
12

Your hunch is correct. Since your Streambuilder is in your Build method, every time your widget tree is rebuilt causes a read on Firestore. This is explained better than I could here.

To prevent this from happening, you should listen to your Firestore stream in your initState method. That way it will only be called once. Like this :

class QuotesScreenLayout extends State<QuotesScreen> {
  List<String> quoteLikeList = new List<String>();
  Stream yourStream;

  // Get Goals from SharedPrefs
  @override
  void initState() {
  yourStream = Firestore.instance
        .collection(FireStoreCollections.QUOTES)
        .orderBy('timestamp', descending: true)
        .snapshots();

    super.initState();
    getQuoteLikeList();
  }

  Future getQuoteLikeList() async {
    if (Globals.globalSharedPreferences.getString('quoteLikeList') == null) {
      print("No quotes liked yet");
      return;
    }

    String quoteLikeListString =
    Globals.globalSharedPreferences.getString('quoteLikeList');
    quoteLikeList = List.from(json.decode(quoteLikeListString));
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
            padding: const EdgeInsets.all(10.0),
            child: StreamBuilder<QuerySnapshot>(
              stream: yourStream,
              builder: (BuildContext context,
                  AsyncSnapshot<QuerySnapshot> snapshot) {
Calathus answered 8/6, 2020 at 11:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.