Flutter:Streambuilder 导致 Firestore 上的读取过多

Posted

技术标签:

【中文标题】Flutter:Streambuilder 导致 Firestore 上的读取过多【英文标题】:Flutter: Streambuilder causing far too many reads on Firestore 【发布时间】:2020-09-25 07:08:57 【问题描述】:

我正在尝试构建一个简单的引号 Flutter 应用程序,在其中显示引号列表并允许用户“喜欢”引号。为此,我正在使用 Streambuilder。我的问题是 Firestore 使用仪表板显示的读取次数非常多(每个用户几乎 300 次),即使我最多有 50 个引号。我有一种预感,我的代码中的某些内容导致 Streambuilder 多次触发(可能是用户“喜欢”一个报价),而且 Streambuilder 正在加载所有报价,而不仅仅是那些在用户视口中的报价。任何有关如何解决此问题以减少读取次数的帮助将不胜感激。

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


【问题讨论】:

你确定这不仅仅是让他的 Firestore 控制台在一个忙着写入的集合上打开吗?控制台成本读数也是如此。至于您的代码,您必须使用某种计数器来检测它,该计数器可以准确地告诉您它在做什么。 @DougStevenson:我每天只打开一次控制台,所以这可能不是问题。我看到只有 80 个用户的 24K 写入。是否有任何文档说明如何检测从 StreamBuilder 读取的 Firebase 文档? 您必须编写自己的代码来捕获和记录使用情况。 Firestore 不会为您执行此操作。 【参考方案1】:

你的预感是正确的。由于您的 Streambuilder 在您的 Build 方法中,因此每次重新构建小部件树时都会在 Firestore 上进行读取。这比我here 解释得更好。

为防止这种情况发生,您应该在 initState 方法中监听 Firestore 流。这样它只会被调用一次。像这样:

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) 

【讨论】:

以上是关于Flutter:Streambuilder 导致 Firestore 上的读取过多的主要内容,如果未能解决你的问题,请参考以下文章

Flutter StreamBuilder 在加载时删除旧数据

Flutter 实现局部刷新 StreamBuilder 实例详解

Flutter 实现局部刷新 StreamBuilder 实例详解

FirebaseStorage + Flutter,streamBuilder?

来自一个 StreamBuilder 的 Flutter 快照显示在另一个 StreamBuilder 中

Flutter:Streambuilder - 关闭流