# HG changeset patch # User Da Risk # Date 1715059671 14400 # Node ID 9c588eb084e1fc4c414621574f27575fd0032da3 # Parent d272d63737985b22892b652aa183be1eae8d7bb2 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. diff -r d272d6373798 -r 9c588eb084e1 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 QueryPagingSource(query: Query, type: KClass): QueryPagingSource { - return QueryPagingSource(query, documentMapper = { - it.toObject(type.java) - }) +fun QueryPagingSource(query: Query, type: KClass, validatePageKey: (DocumentSnapshot) -> Boolean = { true }): QueryPagingSource { + return QueryPagingSource( + query, + documentMapper = { + it.toObject(type.java) + }, + validatePageKey = validatePageKey + ) } /** @@ -47,7 +64,8 @@ */ class QueryPagingSource( private val query: Query, - private val documentMapper: (DocumentSnapshot) -> T? + private val validatePageKey: (DocumentSnapshot) -> Boolean = { true }, + private val documentMapper: (DocumentSnapshot) -> T?, ) : PagingSource() { 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 = query.asDocumentFlow() .map { @@ -119,7 +161,7 @@ override fun getRefreshKey(state: PagingState): Key? = null sealed class Key { - object InitialKey : Key() + data object InitialKey : Key() data class StartAtDocumentKey(val documentSnapshot: DocumentSnapshot) : Key() } }