Why get local cache first?
DocumentReference.get(Source.DEFAULT) will always get the latest data (assuming there is network), but the response is slower as it needs to check the server if latest data exist.
DocumentReference.get(Source.CACHE)
is fast if local cache exist. If it doesn't, we need fetch from server with DocumentReference.get(Source.SERVER)
. This is the reverse of Source.DEFAULT
.
If local cache already exist, there is no guarantee DocumentReference.get(Source.CACHE)
will return the latest data. To solve this issue, create DocumentReference.addSnapshotListener to make sure the latest data is always saved to local cache.
val firestore = FirebaseFirestore.getInstance()val docRef = firestore.collection("test").document("test_local_cache")docRef.addSnapshotListener { documentSnapshot, firebaseFirestoreException -> // do nothing, just to make sure server will update local cache}
Kotlin Extension
fun DocumentReference.getCacheFirst(listener: (DocumentSnapshot?, Exception?) -> Unit) { get(Source.CACHE).addOnCompleteListener { task -> if (task.isSuccessful) { listener(task.result, null) } else { get(Source.SERVER).addOnCompleteListener { task -> if (task.isSuccessful) { listener(task.result, null) } else { listener(null, task.exception) } } } }}
Usage
val firestore = FirebaseFirestore.getInstance()val docRef = firestore.collection("test").document("test_local_cache")docRef.getCacheFirst { documentSnapshot, exception -> if (documentSnapshot != null) { Timber.d("data=${documentSnapshot.data}") } else { Timber.e(exception) }}
Kotlin Coroutines Suspend Function
Using kotlinx-coroutines-play-services.
Include dependencies
dependencies {
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-play-services
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.2.1'
}
Kotlin Extension
suspend fun DocumentReference.getCacheFirstSuspend(): DocumentSnapshot { return try { // com.google.firebase.firestore.FirebaseFirestoreException: Failed to get document from cache. (However, this document may exist on the server. Run again without setting source to CACHE to attempt to retrieve the document from the server.) get(Source.CACHE).await() } catch (e: FirebaseFirestoreException) { // com.google.firebase.firestore.FirebaseFirestoreException: Failed to get document because the client is offline. get(Source.SERVER).await() }}
Usage
val handler = CoroutineExceptionHandler { _, exception -> Timber.e(exception, "handler")}GlobalScope.launch(Dispatchers.Main + handler) { val firestore = FirebaseFirestore.getInstance() val docRef = firestore.collection("test").document("test_local_cache") val doc = docRef.getCacheFirstSuspend() Timber.d("data=${doc.data}")}
Use suspendCoroutine
suspend fun DocumentReference.getCacheFirstSuspend() = suspendCoroutine<DocumentSnapshot> { cont -> getCacheFirst { documentSnapshot, exception -> if (documentSnapshot != null) { cont.resume(documentSnapshot) } else { cont.resumeWithException(exception!!) } }}
Usage same as Kotlin Coroutines Suspend Function
.
Complete solution using Kotlin Coroutines
- Send in
lifecycleOwner
to listen toaddSnapshotListener
, where it will be removed whenlifecycleOwner
is destroyed. Refer to Firestore addSnapshotListener with LifecycleOwner. - Only first time will try to check server for latest data
- Subsequent call will will always read from cache (assuming
addSnapshotListener
is doing it's job in updating the local cache with latest data) docRef.get().await()
is usingkotlinx-coroutines-play-services
private val userQuoteHistoryRegistration = AtomicReference<ListenerRegistration>()suspend fun getUserQuoteHistorySuspend(lifecycleOwner: LifecycleOwner? = null): DocumentSnapshot { val docRef = ... // only run on the 1st time, when addSnapshotListener not called yet if (lifecycleOwner != null) { if (userQuoteHistoryRegistration.compareAndSet(null, docRef.addSnapshotListener(lifecycleOwner) { documentSnapshot, firebaseFirestoreException -> Timber.d("userQuoteHistory updated") } ) ) { Timber.d("userQuoteHistory.addSnapshotListener") // sadly, I can't return result from addSnapshotListener // , as there is no way to tell if the result is cache or latest from server // get from server if available, else return cache return docRef.get().await() } } // get cache 1st, if not available try server return docRef.getCacheFirstSuspend()}
Usage
val lifecycleOnwer = ... // Actiivty or Fragmentlaunch(Dispatchers.Default) { val doc = getUserQuoteHistorySuspend(lifecycleOnwer)}
NOTE: Alternatively, you can addSnapshotListener
for the lifetime of the application (at Application.onStart), and getCacheFirst
will always return cache with the latest data.