Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter: Streambuilder causing far too many reads on Firestore

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

like image 257
Varun Avatar asked Jun 06 '20 01:06

Varun


Video Answer


1 Answers

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) {
like image 192
Finaff17 Avatar answered Oct 07 '22 20:10

Finaff17