Firestore: Android query.addSnapshotListener should return cached results immediately

962 views
Skip to first unread message

Bartholomew Furrow

unread,
Nov 16, 2018, 6:45:14 PM11/16/18
to fireba...@googlegroups.com
If I issue a Firestore query with query.addSnapshotListener, it doesn't immediately return what's in the cache. Instead, it first tries to get an up-to-date version from the server. But if the Internet connection is slow or non-functional, the user could end up waiting as much as 10 seconds for the request to time out and retrieve it from the local cache instead.

For motivation, on the home page of my app, I would like to display information to the user. The information has previously been retrieved from Firestore. There's nothing else in my app for the user to look at, so it's important that the information loads right away. Instead, the user waits while the client tries to talk to the server.

It's possible to work around this by using query.get(Source.CACHE), then query.addSnapshotListener; but then you don't have access to DocumentChanges, and any complex logic needs to become more complex.

I would guess I'm not alone in wanting immediate feedback, then streamed updates based on what the server says. After all, any snapshot listener has to be able to handle streaming updates. Has much thought been put into this lately? I'd imagine it's more practical now than it originally would have been, since we can ask to be updated when metadata changes.

Best,
Bartholomew

Michael Lehenbauer

unread,
Nov 16, 2018, 7:11:16 PM11/16/18
to fireba...@googlegroups.com
Hey Bartholomew,

What you describe is exactly how it is supposed to work.  You should get a snapshot with metadata.isFromCache() => true that comes from our cached data and then once we've synced with the server, you'll get another snapshot with isFromCache() => false  (though you'll only see this second snapshot if there are other changes in the snapshot or if you pass MatadataChanges.INCLUDE).

If you're seeing something different, please let me know. 

Thanks,
Michael

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-tal...@googlegroups.com.
To post to this group, send email to fireba...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/firebase-talk/CAHaiWHPorVB5Rsvk%3DMRXQpOha%2BM%3DLT1QAAcUJcX0ojVtLkKssw%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.

Bartholomew Furrow

unread,
Nov 17, 2018, 12:32:42 PM11/17/18
to Firebase Google Group
You're right, of course. I'm sorry for assuming you guys had made such a strange design choice, and clearly I should have investigated a little further before mailing it in.

There actually are cases where a snapshot listener doesn't get an immediate cached result, even when the client should have one. Those are:
1. addSnapshotListener on a document reference for a document that doesn't exist, even if the client has previously tried to retrieve that document.
2. addSnapshotListener on a query that has no results, even if the client has previously issued that exact query.

I can understand it as a design decision -- it feels like there's a difference between getting a stale "nothing was there last time I checked" vs. a stale "here's what was there last time I checked" -- but it's a frustrating special case to deal with when the user has a bad connection.

For reference, here's what I'm doing, inside a ViewModel:

1. Watch all photoAccess documents with sharedWith == currentUserId, using query.addSnapshotListener.
2. For each document, get its "photoId" field and watch /photos/{photoId}, using documentReference.addSnapshotListener.
3. Publish the result to the ViewModel's MutableLiveData only once all snapshot listeners have returned at least once.
4. Until the result has been published, I display "loading." Once it has, I display the loaded photos.

So what goes wrong?

- If a user doesn't have any photoAccess objects, then rather than displaying "You have no photos" right away, the Activity will display "loading" until it hears from the server.
- If a photo has been deleted, but the corresponding photoAccess still exists, the Activity will display "loading" until it hears from the server. I try to prevent this from happening, of course.
- The logic of having to choose what to watch (step 2) based on what you've retrieved from a query (step 1) makes all this just a little bit trickier.

Again, this is in a situation where the client should know that there aren't any photoAccesses, or should know that there isn't a photo.

I intend to work around these issues by adding a .get(Source.Cached) before the addSnapshotListeners, though it isn't quite that simple, and it's frustrating to have to work around not getting offline cached results.

Cheers!
Bartholomew

Bartholomew Furrow

unread,
Nov 19, 2018, 1:18:07 AM11/19/18
to fireba...@googlegroups.com
A couple more thoughts:

1. This appears to be a deliberate decision. Retrieving something with ref.get(Source.CACHE) returns a null snapshot if the object exists in the cache, but an Exception if it doesn't. (It's late and I'm tired, so correct me if I'm wrong.)

2. This issue is more pervasive than I realized. I have two Fragments that almost always flash "loading" for longer than they should, and I've always wondered why. I hope they're illustrative.

- My Friends page: Checks four Firestore queries for Friends, FriendRequests from the user, and FriendRequests to the user. If nobody has asked you to be friends lately, then we're doing a round-trip to the server. That's even though I'm already watching those queries elsewhere in the app, to make sure their results are immediately available locally. But if any of the queries has no results, it effectively isn't available locally, even though I'm actively watching it.
- My "choose who you want to share with" page: Same deal.


There's always an instantaneous cache response for stuff the user has done before, except in this edge case that might or might not be common in people's apps. Turns out it is common in mine, and the behaviour really took me by surprise -- I'm guessing it's a surprise to other people, too. I hope you'll consider making this configurable.

Cheers,
Bartholomew


[0] Since immediate display of data is important to me, I'm planning to add the following workaround to most of my calls to ref.addSnapshotListener:

// Before:
ref.addSnapshotListener(new MySnapshotListener());

// After:
ref.get(Source.Cached).addOnCompleteListener(task -> {
    if (task.isSuccessful()) {
        new MySnapshotListener().onEvent(task.getResult(), null);
    }
});
ref.addSnapshotListener(new MySnapshotListener());

Reply all
Reply to author
Forward
0 new messages