问题
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(() {});
},
),
],
),
],
),
),
);
}
}
回答1:
This fixed it for me: Where you define your stream, make sure to limit the query to a certain number, and that will be the number of documents retrieved (read).
Firestore.instance.collection('collection').orderBy('name').limit(ItemCount + 10).snapshots()
Assuming you'd like to retrieve documents incrementally as a user scrolls, consider adding a scroll controller to your list view, and when the scroll controller is at it's max extent, increment the query limit (ItemCount)(you'll also want to give yourself a little leeway with the increment to give Flutter time to render the new widgets as you scroll, hence the +10 in my case). Not a perfect solution, as that query will still be run every time you increment (so reads will be 10, then 10+10, then 10+10+10, and so on, but definitely an improvement already instead of all documents at once).
It's also useful to set up a Google Cloud Dashboard to track document reads (more up to date than firebase console for some reason), and set up alerts if you breach a certain threshold of document reads. Notice the difference below before and after adding the limit mentioned previously:
P.S. For awhile I was also trying to figure out why my StreamBuilders made so many reads.
回答2:
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) {
来源:https://stackoverflow.com/questions/62226143/flutter-streambuilder-causing-far-too-many-reads-on-firestore