geekdroid-firebase: allow to validate QueryPagingSource boundary key
authorDa Risk <da_risk@geekorum.com>
Tue, 07 May 2024 01:27:51 -0400
changeset 74 9c588eb084e1
parent 73 d272d6373798
child 75 534a19e25217
geekdroid-firebase: allow to validate QueryPagingSource boundary key This is useful when ordering by @ServerTimestamp field. When the document hasn't been sent to server it can't be used as a boundary key has the server timestamp is null. This change allows client to make the check and avoid an IllegalArgumentException.
geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/firestore/Paging.kt
--- a/geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/firestore/Paging.kt	Sat Mar 30 10:55:33 2024 -0400
+++ b/geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/firestore/Paging.kt	Tue May 07 01:27:51 2024 -0400
@@ -27,19 +27,36 @@
 import com.google.firebase.firestore.DocumentReference
 import com.google.firebase.firestore.DocumentSnapshot
 import com.google.firebase.firestore.Query
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.channels.onFailure
 import kotlinx.coroutines.channels.trySendBlocking
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.take
 import timber.log.Timber
 import kotlin.coroutines.coroutineContext
 import kotlin.reflect.KClass
 
-fun <T: Any> QueryPagingSource(query: Query, type: KClass<T>): QueryPagingSource<T> {
-    return QueryPagingSource(query,  documentMapper = {
-        it.toObject(type.java)
-    })
+fun <T: Any> QueryPagingSource(query: Query, type: KClass<T>, validatePageKey: (DocumentSnapshot) -> Boolean = { true }): QueryPagingSource<T> {
+    return QueryPagingSource(
+        query,
+        documentMapper = {
+            it.toObject(type.java)
+        },
+        validatePageKey = validatePageKey
+    )
 }
 
 /**
@@ -47,7 +64,8 @@
  */
 class QueryPagingSource<T: Any>(
     private val query: Query,
-    private val documentMapper: (DocumentSnapshot) -> T?
+    private val validatePageKey: (DocumentSnapshot) -> Boolean = { true },
+    private val documentMapper: (DocumentSnapshot) -> T?,
 ) : PagingSource<QueryPagingSource.Key, T>() {
     private val sourceScope = CoroutineScope(Job())
 
@@ -69,14 +87,30 @@
         var query = this.query
             .limit(params.loadSize.toLong())
 
-        params.key?.let {
+        var hasInvalidPreviousKey = false
+        var hasInvalidNextKey = false
+        params.key?.let { key ->
             query = when (params) {
                 is LoadParams.Prepend -> {
-                    (params.key as? Key.StartAtDocumentKey)?.documentSnapshot?.let {
-                        query.startAfter(it)
+                    (key as? Key.StartAtDocumentKey)?.documentSnapshot?.let {
+                        if (!validatePageKey(it)) {
+                            hasInvalidPreviousKey = true
+                            query
+                        } else {
+                            query.startAfter(it)
+                        }
                     } ?: query
                 }
-                is LoadParams.Append -> query.startAfter((params.key as Key.StartAtDocumentKey).documentSnapshot)
+                is LoadParams.Append -> {
+                    (key as Key.StartAtDocumentKey).documentSnapshot.let {
+                        if (!validatePageKey(it)) {
+                            hasInvalidNextKey = true
+                            query
+                        } else {
+                            query.startAfter(it)
+                        }
+                    }
+                }
                 is LoadParams.Refresh -> query
             }
         }
@@ -84,6 +118,14 @@
             lastNextKey = Key.InitialKey
         }
 
+        if (hasInvalidPreviousKey || hasInvalidNextKey) {
+            Timber.w("Query has invalid boundary key, return empty results")
+            val prevKey = lastNextKey.takeIf { hasInvalidNextKey && it !is Key.InitialKey }
+            return LoadResult.Page(
+                data = emptyList(),
+                prevKey = prevKey,
+                nextKey = null)
+        }
         // share the query between 2 coroutines
         val dataChannel: SharedFlow<SnapshotsOrError> = query.asDocumentFlow()
             .map {
@@ -119,7 +161,7 @@
     override fun getRefreshKey(state: PagingState<Key, T>): Key? = null
 
     sealed class Key {
-        object InitialKey : Key()
+        data object InitialKey : Key()
         data class StartAtDocumentKey(val documentSnapshot: DocumentSnapshot) : Key()
     }
 }