DatabasePagingSource not working when firebase query contains orderByChild clause. #2000

Description

@beyondeye

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:

  1. 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: "
}
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
    Skip to content

    DatabasePagingSource not working when firebase query contains orderByChild clause. #2000

    Description

    @beyondeye

    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:

    1. 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: "
    }
    }

    Activity

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      DatabasePagingSource not working when firebase query contains orderByChild clause. #2000

      Description

      @beyondeye

      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:

      1. 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: "
      }
      }

      Activity

      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

      Metadata

      Metadata

      Assignees

      Type

      No type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        DatabasePagingSource not working when firebase query contains orderByChild clause. #2000

        Description

        @beyondeye

        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:

        1. 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: "
        }
        }

        Activity

        Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

        Metadata

        Metadata

        Assignees

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          DatabasePagingSource not working when firebase query contains orderByChild clause. #2000

          Description

          @beyondeye

          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:

          1. 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: "
          }
          }

          Activity

          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

          Metadata

          Metadata

          Assignees

          Type

          No type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            DatabasePagingSource not working when firebase query contains orderByChild clause. #2000

            Description

            @beyondeye

            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:

            1. 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: "
            }
            }

            Activity

            Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

            Metadata

            Metadata

            Assignees

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              DatabasePagingSource not working when firebase query contains orderByChild clause. #2000

              Description

              @beyondeye

              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:

              1. 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: "
              }
              }

              Activity

              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

              Metadata

              Metadata

              Assignees

              Type

              No type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                DatabasePagingSource not working when firebase query contains orderByChild clause. #2000

                Description

                @beyondeye

                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:

                1. 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: "
                }
                }

                Activity

                Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                Metadata

                Metadata

                Assignees

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions