I'm using Riverpod and I have a relatively simple provider that I'm using to get the number of pending writes from Firestore. This is basically used to help give users feedback as to whether or not their data is fully synced.
final pendingUploadsCounterProvider =
FutureProvider.autoDispose.family<int, List<Report>>((ref, reports) async {
final db = ref.read(firebaseServiceProvider).db;
final reportIds = reports.map((e) => e.id).toList();
final documents = await Future.wait([
for (final name in kReportTypes)
db
.collection(name)
.where('report_id', whereIn: reportIds)
.get(GetOptions(source: Source.cache))
]);
return documents.where((event) => event.metadata.hasPendingWrites).length;
});
As you can see, all I'm doing is reading some data from the Firestore cache and then filtering that list based on the hasPendingWrites property.
I'm attempting to use this provider on multiple screens as I need to provide the user feedback on their sync status in multiple locations.
When I use this provider on a single screen only, every time I enter the screen, the provider is triggered and I get an up to date status.
When I use this provider on multiple screens, the first time I enter a screen where it is used, the provider is triggered. Subsequently, entering any other screen where this provider is used, the provider is no longer triggered and therefore, the results are out of date.
I thought by using autoDispose, the provider would be disposed of when I leave the screen, but that doesn't appear to be the case.
I'm sure that there's a disconnect with my understanding of riverpod and how this works and hopeful someone can shed some light on my misunderstanding.
Thank you!
the provider will not be disposed while the widget in the tree you need to remove it from the back stack to be disposed
in your case, you need to reload it on other screens
by calling
context.refresh(pendingUploadsCounterProvider);
Related
I am writing a cross-platform app in Flutter, with Riverpod for managing state, and Firestore as the back-end database. This question explores one of the alternatives I proposed in this other question.
I'm using Riverpod's StreamProvider to subscribe to a certain Firestore collection, and a provider family to listen to individual documents within that collection.
final collectionProvider = StreamProvider.autoDispose((ref) => Firestore.instance.collection("my_collection").snapshots());
final documentProviderFamily =
StreamProvider.autoDispose.family<DocumentSnapshot, String>((ref, documentId)
=> Firestore.instance.collection("my_collection").doc(documentId).snapshots());
I have a page (a widget) that listens to the collection and shows some info about each document in a ListView:
CollectionPage extends ConsumerWidget {
...
void build(BuildContext context, WidgetRef ref) {
final snapshot = ref.watch(collectionProvider);
...
}
}
If the user clicks on a ListTile, a details page (a widget) is opened, with Navigator.of(context).push(...), showing detailed info about the corresponding document.
Importantly, this details page also needs to update in real-time, so just passing it the current object/document snapshot held by the collection page doesn't work. Instead, this widget watches that individual document from Firestore:
DocumentDetailsPage extends ConsumerWidget {
...
final String documentId;
void build(BuildContext context, WidgetRef ref) {
final snapshot = ref.watch(documentProviderFamily(documentId));
...
}
}
Suppose we have one of these details page open. Since the navigator stack still contains the CollectionPage widget, the subscription to the collection stream is still active. So, whenever the document in question is updated, two reads will occur on Firestore: one for the collection subscription and another one for the single document subscription. (I found this out by using the Firestore emulator and looking at the requests tab.)
Since the CollectionPage is not visible, the former update is wasteful. A single update to the document is causing duplicate reads of the same document (being charged accordingly in Firestore). Ideally I would like to cancel the subscription to the entire collection when the details page is opened, and re-start it when the details page is popped.
In riverpod terms: how do I temporarily un-watch the provider while the widget is not visible and start watching again when it becomes visible again?
I've read Dispose widget when navigating to new route, and it seems this kind of behaviour (cancelling the listener when the screen is not visible) should go into the deactivate() method of the widget, but then again I don't know how to make Riverpod do this, and the Q&A mentions that this method is not reliable anyways.
Imagine a very simple application with two pages, PostList and PostDetail. On the former page, we show a list of posts, and on the latter, the details of a single post.
Now consider the following scenario. Our user clicks on the first PostItem and navigates to the PostDetail page. We fetch the full post data from the server. The likes_count of this post gets increased by one. Now if our user navigates back to the first page, the PostItem should be updated and show the new likes_count as well.
One possible solution to handle this is to create a pool of posts. Now when we fetch some new data from the server, instead of creating a new post object, we can update our corresponding pool instance object. For example, if we navigate to post with id=3, we can do something like this:
Post oldPost = PostPool.get(3)
oldPost.updateFromJson(servers_new_json_for_post_3)
Since the same object is used on the PostDetail page, our PostItem on the PostList page will be updated as well.
Other approaches that do not use a unique "single instance" of our Post objects, across the application, would not be clean to implement and requires tricks to keep the UI sync.
But the ObjectPool approach also has its own problems and leads to memory leaks since the size of the pool gets bigger and bigger over time. For this problem to get solved we need to manually count the number of references for each pool object instance and discard them when this count is equal to zero. This manual calculation is full of bugs and I was wondering if there are any better ways to achieve this.
You can also solve this by using streams and StreamBuilders. First you create the stream and populates it with the initial data fetched from the API:
I like to use BehaviorSubject from rxdart but you can use normal streams too.
final BehaviorSubject<List<Post>> postsStream = BehaviorSubject<List<Post>>()..startWith(<Post>[]);
On the constructor body or initState function you would fetch the data and add it to the stream.
PostPage() {
_fetchPosts().then((posts) => postsStream.add(posts));
}
You can now subscribe to changes on this postsStream in both pages with StreamBuilder. Any update you need to do you would emit a new (updated) List<Post> to the stream triggering a rebuild on any StreamBuilder subscribed to the stream with the new and updated values.
You can latter dispose the StreamController.
I have been using onSnapshot successfully to alert my code to changes in underlying data, as in
// Set up to listen for changes to the "figures" collection, that is,
// someone has created a new figure that we will want to list on the screen.
setFiguresListener: function () {
// `figuresCR` is a collection reference defined elsewhere
return this.figuresCR.onSnapshot((iFigs) => {
iFigs.forEach((fSnap) => {
const aFigure = figureConverter.fromFirestore(fSnap, null);
const dbid = aFigure.guts.dbid; // ID of the "figure" in the database
nos2.theFigures[dbid] = aFigure; // update the local copy of the data
});
nos2.ui.update();
console.log(` Listener gets ${iFigs.size} figures`);
});
But I now read about on in the docs. It explains:
[The on() function] Listens for data changes at a particular location.
This is the primary way to read data from a Database. Your callback
will be triggered for the initial data and again whenever the data
changes. Use off( )to stop receiving updates. See Retrieve Data on
the Web for more details.
The syntax is a bit different, and on() seems to do much the same as onSnapshot().
So what is the real difference? Should we be using on() instead of onSnapshot()?
on() is an operation for reading from Firebase Realtime Database. That's a completely different database with different APIs than Firestore. They have essentially no overlap. There is no on() operation with Firestore.
If you're working with Firestore, ignore all the documentation about Realtime Database, and stick to using onSnapshot() for getting realtime updates.
Other tyros who fall into this tar pit: in the API doc pages, you might think that since firestore is a database under firebase, you could look for help under firebase.database. But no: look only in the next section, firebase.firestore.
I'm currently trying out Stream Providers for noticing changes in Cloud Firestore Database and getting new data on change.
This is how I have created the Stream Provider :
child: StreamProvider.value(value: _firestoreAPI.getProjectsFromStream(_savedUser.user.name),child: PrimaryScene(toggleAnimation)),
As I am not able to call the provider from initstate ....Hence I'm calling is inside build :
_projectListSnapshotFromStream = Provider.of<QuerySnapshot>(context);
I have a doubt that if I rebuild the Widget containing the above call in its build function, will it charge me a read everytime I build that Widget because I called the provider? Or will it only charge a read when there is a change noticed in the database ?
Stream Definition :
Stream<QuerySnapshot> getProjectsFromStream(String username){
return databaseReference.collection("Projects").where("Members",arrayContains: username).snapshots();
}
Thankyou in advance for answering.
will only count as read if you receive a new document from firestore
I am retrieving a lot of documents from Firestore, it can take a lot of time depending on the network connection of the user.
I would like to display a progres bar.
I didn't find any documentation on this
Here is my code :
final databaseReference = Firestore.instance;
databaseReference.collection("XXX").getDocuments().then((QuerySnapshot snapshot) {
Map<String, Transac.Transaction> map = new Map<String, Transac.Transaction>();
snapshot.documents.forEach((f) {
//doing something
});
I would like to have a percentage of the data loaded. For example :
Loading (58%)
Thank you.
Firestore doesn't expose any progress indicator for the documents within a query. By the time your then callback gets called, all documents have been loaded on the client and are present in the QuerySnapshot.
The usual way to deal with this is to show a "spinner" instead of a progress bar, and then hide that spinner as the first code inside your then callback.
What you can do instead is just call getDocument() for each document individually, and track their individual completions. You should track them in the order that you issued them, as the requests are pipelined over a single connection. This would give you a rough sense of progress, if you're requesting lot of documents.