Step 2: Describe your environment
- Android device: Samsung S21+5G
- Android OS version: 11
- Google Play Services version: August 1 2021
- Firebase/Play Services SDK version: com.google.firebase:firebase-bom:28.3.0
- FirebaseUI version: 8.0.0
Step 3: Describe the problem:
DatabasePagingSource (and therefore also FirebaseRecyclerPagingAdapter) not working when firebase query contains orderByChild clause.
I have verified that the problem is not in FirebaseUI itself but in method
privateQuerystartAt(Nodenode, Stringkey)
in Query class in package com.google.firebase.database;
according code javadocs for the method:
* Createsaqueryconstrainedtoonlyreturnchildnodeswithavaluegreaterthanorequalto
* thegivenvalue, usingthegiven {@codeorderBy} directiveorpriorityasdefault, and
* additionallyonlychildnodeswithakeygreaterthanorequaltothegivenkey.but I have verified that if the query is using an orderByChild clause, the key parameter in startAt is ignored
Because of this I am getting crashes cause by exception
java.lang.IllegalStateException: The same value, VWGuwwgOg3tg_15vtqkJf32bhICi-zI_yt---, was passed as the nextKey in two
sequential Pages loaded from a PagingSource. Re-using load keys in
PagingSource is often an error, and must be explicitly enabled by
overriding PagingSource.keyReuseSupported.
because, since the key parameter is ignored, we reread each time the same data, each time the method
public Single<LoadResult<String, DataSnapshot>> loadSingle(@NonNull LoadParams<String> params)
is called
Steps to reproduce:
- see above
Proposed Fix
I have written code to fix the issue. note that I am working with a fork of the original FirebaseUI-Android, so I have the original code converted to Kotlin, but you can get the idea if you look at it. In brief what I do is keep not only the key of the last read document but also the value of the childkey, and use them together when I need to query for the next batch of documents
Code With Bug Fix:
classDatabasePagingSource(privatevalmQuery:Query) : PagingSource<Any, DataSnapshot>() {
@SuppressLint("RestrictedApi")
fungetChildValue(snapshot:DataSnapshot, index:PathIndex):Any? {
val keypath=index.queryDefinition
val data=snapshot.child(keypath)
if(!data.exists()) returnnullreturn data.value
}
fun Query.startAt_childvalue(startvalue:Any?,keyvalue:String?):Query {
returnwhen(startvalue) {
isString-> startAt(startvalue,keyvalue)
isBoolean-> startAt(startvalue,keyvalue)
isDouble-> startAt(startvalue,keyvalue)
isLong-> startAt(startvalue.toDouble(),keyvalue)
else->this
}
}
/** * reason of SuppressLint: DatabaseError.fromStatus() is not meant to be public.*/
@SuppressLint("RestrictedApi")
overridesuspendfunload(params:LoadParams<Any>): LoadResult<Any, DataSnapshot> {
//change mQuery.startAt at value if child index//if not null then what we have here is orderByChild queryvar querychildpathindex:PathIndex?= mQuery.spec.index as?PathIndexval pkey=params.key asPair<Any?,String>?val task:Task<DataSnapshot> =if (params.key ==null) {
mQuery.limitToFirst(params.loadSize).get()
} else {
if (querychildpathindex !=null) //orderByChild query mode
mQuery.startAt_childvalue(pkey?.first, pkey?.second).limitToFirst(params.loadSize +1).get()
else
mQuery.startAt(null,pkey?.second).limitToFirst(params.loadSize +1).get()
}
try {
val dataSnapshot = task.await()
if (dataSnapshot.exists()) {
//Make List of DataSnapshotval data:MutableList<DataSnapshot> =ArrayList()
var lastKey:Pair<Any?,String>?=nullif (params.key ==null) {
for (snapshot in dataSnapshot.children) {
data.add(snapshot)
}
} else {
val iterator:Iterator<DataSnapshot> = dataSnapshot.children.iterator()
//Skip First Item that corresponds to lastKey read in previous batchif (iterator.hasNext()) {
iterator.next()
}
while (iterator.hasNext()) {
val snapshot = iterator.next()
data.add(snapshot)
}
}
//Detect End of Dataif (!data.isEmpty()) {
//Get Last Keyval lastkey_c = getLastPageChildKey(data,querychildpathindex)
val lastkey_k = getLastPageKey(data)
lastKey =if (lastkey_c ==null&& lastkey_k ==null)
nullelseif (lastkey_k ==null) Pair(lastkey_c, "") elsePair(lastkey_c, lastkey_k)
}
return toLoadResult(data, lastKey)
} else {
val details =DETAILS_DATABASE_NOT_FOUND+ mQuery.toString()
throwDatabaseError.fromStatus(
STATUS_DATABASE_NOT_FOUND,
MESSAGE_DATABASE_NOT_FOUND,
details
).toException()
}
} catch (e:ExecutionException) {
returnLoadResult.Error<Any, DataSnapshot>(e)
}
}
privatefuntoLoadResult(
snapshots:List<DataSnapshot>,
nextPage:Pair<Any?,String>?
): LoadResult<Any, DataSnapshot> {
returnLoadResult.Page(
snapshots,
null, // Only paging forward.
nextPage,
LoadResult.Page.COUNT_UNDEFINED,
LoadResult.Page.COUNT_UNDEFINED
)
}
privatefungetLastPageChildKey(data:List<DataSnapshot>,index:PathIndex?): Any? {
if(index==null) returnnullreturnif (data.isEmpty()) {
null
} else {
getChildValue(data[data.size -1],index)
}
}
privatefungetLastPageKey(data:List<DataSnapshot>): String? {
returnif (data.isEmpty()) {
null
} else {
data[data.size -1].key
}
}
overridefungetRefreshKey(state:PagingState<Any, DataSnapshot>): Pair<Any?, String>? {
returnnull
}
companionobject {
privateconstvalSTATUS_DATABASE_NOT_FOUND="DATA_NOT_FOUND"privateconstvalMESSAGE_DATABASE_NOT_FOUND="Data not found at given child path!"privateconstvalDETAILS_DATABASE_NOT_FOUND="No data was returned for the given query: "
}
}
Step 2: Describe your environment
Step 3: Describe the problem:
DatabasePagingSource (and therefore also FirebaseRecyclerPagingAdapter) not working when firebase query contains
orderByChildclause.I have verified that the problem is not in FirebaseUI itself but in method
in
Queryclass in packagecom.google.firebase.database;according code javadocs for the method:
but I have verified that if the query is using an
orderByChildclause, thekeyparameter instartAtis ignoredBecause of this I am getting crashes cause by exception
because, since the
keyparameter is ignored, we reread each time the same data, each time the methodis called
Steps to reproduce:
Proposed Fix
I have written code to fix the issue. note that I am working with a fork of the original FirebaseUI-Android, so I have the original code converted to Kotlin, but you can get the idea if you look at it. In brief what I do is keep not only the key of the last read document but also the value of the childkey, and use them together when I need to query for the next batch of documents
Code With Bug Fix: