2
votes

This happens in my main application and I replicate it with the given codelabs: https://codelabs.developers.google.com/codelabs/flutter-firebase/index.html?index=..%2F..index#10

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Baby Names',
     home: MyHomePage(),
   );
 }
}

class MyHomePage extends StatefulWidget {
 @override
 _MyHomePageState createState() {
   return _MyHomePageState();
 }
}

class _MyHomePageState extends State<MyHomePage> {
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: Text('Baby Name Votes')),
     body: _buildBody(context),
   );
 }

Widget _buildBody(BuildContext context) {
 return StreamBuilder<QuerySnapshot>(
   stream: Firestore.instance.collection('baby').snapshots(),
   builder: (context, snapshot) {
     if (!snapshot.hasData) return LinearProgressIndicator();

     return _buildList(context, snapshot.data.documents);
   },
 );
}

Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot){
   return ListView(
     padding: const EdgeInsets.only(top: 20.0),
     children: snapshot.map((data) => _buildListItem(context, data)).toList(),
   );
 }

 Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
  final record = Record.fromSnapshot(data);

   return Padding(
     key: ValueKey(record.name),
     padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
     child: Container(
       decoration: BoxDecoration(
         border: Border.all(color: Colors.grey),
         borderRadius: BorderRadius.circular(5.0),
       ),
       child: ListTile(
         title: Text(record.name),
         trailing: Text(record.votes.toString()),
         onTap: () => print(record),
       ),
     ),
   );
 }
}

class Record {
 final String name;
 final int votes;
 final DocumentReference reference;

 Record.fromMap(Map<String, dynamic> map, {this.reference})
     : assert(map['name'] != null),
       assert(map['votes'] != null),
       name = map['name'],
       votes = map['votes'];

 Record.fromSnapshot(DocumentSnapshot snapshot)
     : this.fromMap(snapshot.data, reference: snapshot.reference);

 @override
 String toString() => "Record<$name:$votes>";
}

Only plugin i use is the cloud_firestore 0.9.5+2.You need to be patient with this testing process please. You will not see the issue right away. Run the app first, set this project up. You can follow the directions in the given codelabs. Once everything is set up on front end and backend(create documents on firstore). Go have a lunch break, dinner, play video games or hang out with friends. Come back after 1 hour. Run the app, you will be incurred charges for those reads as new. Do it again, come back 1 hour and it will happen again

How to replicate it in real life: Start this app by given code from codelabs. Run it, it should incur 4 document reads if you stored 4 documents into the firestore.

Start it up again. No reads are charged. Great it works! but no, it really doesnt.

I wake up next day and open up the app, im charged 4 reads. Okay maybe some magic happened. I restart it right away and no charges incur(great!). Later in 1 hour, i start up the app and I get charged 4 reads to display the very same 4 documents that have not been changed at all.

The problem is, on app start up. It seems to be downloading documents from the query snapshot. No changes have been made to the documents. This stream-builder has been previously run many times.

Offline mode(airplane mode), the cached data is displayed with no issues.

in my main application for example, I have a photoUrl and on fresh App start, you can see it being loaded from the firestore(meaning downloaded as a fresh document thus incurring a READ charge). I restart my main application, no charges are made and photo does not refresh(great!). 1 hour later i start up the app and charges are made for every document I retrieve(none of changed).

Is this how cloud firestore is supposed to behave? From what I have read, its not supposed to behave like this :(

2
You should edit the question to show the relevant code and how the queries are actually being performed, so we can all be in agreement about what your app is actually doing,Doug Stevenson
Also bear in mind that the Firestore console bills reads when you use it.Doug Stevenson
I know it does as I noticed that. I stopped looking at the console. I look at 2 hours after I use my app. Close my app and go back(after 2 hours) to console to look at the usage in the console and also in the appengine Quotas I have been fighting against this for 1 month now but I think i need helpBrooklyn Adventure
Also thank you for the quick response. If you get this code working and the backend works as it is supposed to. Please respond with the flutter channel, plugin version for cloudstore, gradle settings etc. I would try to replicate it and then respond after testing againBrooklyn Adventure

2 Answers

5
votes

You should not do actual work in build() except building the widget tree

Instead of code like this in build

stream: Firestore.instance.collection('baby').snapshots(),

you should use

Stream<Snapshot> babyStream;
@override
void initState() {
  super.initState();
  babyStream = Firestore.instance.collection('baby').snapshots();
}

Widget _buildBody(BuildContext context) {
 return StreamBuilder<QuerySnapshot>(
   stream: babyStream,
   builder: (context, snapshot) {
     if (!snapshot.hasData) return LinearProgressIndicator();

     return _buildList(context, snapshot.data.documents);
   },
 );
}

The FutureBuilder docs don't mention it that explicitly but its the same

https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html

The future must have been obtained earlier, e.g. during State.initState, State.didUpdateConfig, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.

0
votes
Also, if the listener is disconnected for more than 30 minutes (for example, if the user goes offline), you will be charged for reads as if you had issued a brand-new query.

https://firebase.google.com/docs/firestore/pricing

I guess I am facing this. For 30 minutes(I tested it many times), I can start up my app and will not count toward reads. After waiting(closed the app for>) 30~35 minutes, i start up the app again and i get charged for every document in query.

Now, if i keep a listener open(not closing its subscription) this should avoid that and download documents that get modified instead.

I tried this:

  List<DocumentSnapshot> nameDocs = [];
  StreamSubscription nameSub;
  @override
  void initState() {
    super.initState();
    nameSub = Firestore.instance.collection('baby').snapshots().listen((data) {
      print('listener Fired at line 39 main.dart');
      data.documentChanges.forEach((x) {
        print('${x.type} this is the type line 40');
        if (x.type == DocumentChangeType.modified) {
          print('modified');
          nameDocs.removeAt(x.oldIndex);
          setState(() {
            nameDocs.add(x.document);
          });
        }
        if (x.type == DocumentChangeType.added) {
          print('added');
          setState(() {
            nameDocs.add(x.document);
          });
        }
        if (x.type == DocumentChangeType.removed) {
          print('removed');
          setState(() {
            nameDocs.removeAt(x.oldIndex);
          });
        }
        print(x.document.data['name']);
      });
    });
  }

I do not cancel the subscription as i do not want to cancel it. Same problem occurs after revisiting the app after 31 minutes.

Is there a way to keep the listener alive, I know some cons can be battery life and what not. I want to keep live listeners in some parts of my app (such as documents that do not get changed often in firestore, I would like to avoid re downloading such documents in my REAL application) and other parts I will leave as normal(listen and cancel on dispose method)