Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Room livedata with paging getting IndexOutOfBoundsException in some devices

I'm using paging library with Room livedata, in some devices sometimes i receive IndexOutOfBoundsException Index out of bounds - passed position = 75, old list size = 75 (always with size 75, none of my databases has a fixed size of 75)

The problem is that the log is shown more like an internal error than my app error

Fatal Exception: java.lang.IndexOutOfBoundsException: Index out of bounds - passed position = 75, old list size = 75

at androidx.recyclerview.widget.DiffUtil$DiffResult.convertOldPositionToNew(DiffUtil.java:672)
at androidx.paging.PagedStorageDiffHelper.transformAnchorIndex(PagedStorageDiffHelper.java:215)
at androidx.paging.AsyncPagedListDiffer.latchPagedList(AsyncPagedListDiffer.java:382)
at androidx.paging.AsyncPagedListDiffer$2$1.run(AsyncPagedListDiffer.java:345)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6543)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:440)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:810)

Is there a workaround for this? or is something related to my app?

like image 335
Jordy Mendoza Avatar asked Apr 06 '19 05:04

Jordy Mendoza


1 Answers

OLD ANSWER (recommended 14/2/2020 ):

After many days investigating the option that causes this error is this :

protected val pagedListConfig: PagedList.Config = PagedList.Config.Builder()
        .setEnablePlaceholders(true) . <------------ set to false 
        .setPrefetchDistance(PREFETCH_DISTANCE_SIZE)
        .setPageSize(DATABASE_PAGE_SIZE)
        .setInitialLoadSizeHint(INITIAL_DATABASE_PAGE_SIZE)
        .build()

so just set setEnablePlaceholders(false) should avoid this case from happing noting that this will affect the scrolling causing it to flash when loading more data , until a fix from android sdk.

UPDATED 1 (not recommended, see update 2):
The issue happens mainly when restoring a destroyed activity / fragment for my case i have 4 fragments, I was refreshing all the lists at once after restoring, meaning i insert new data causing the observable to be called multiple times causing the call of mDiffer.submitList to be called multiple, by right this should not crash as mDiffer do all the work in the background but my guess that there is a synchronisation issue between the mDiffer Runnables

so my solution was to minimizethe mDiffer.submitList calls by the following methods :

  • save/restore items tab fragments:

    public override fun onSaveInstanceState(savedInstanceState: Bundle) { super.onSaveInstanceState(savedInstanceState) supportFragmentManager.putFragment(savedInstanceState, "itemsFragment1", itemsFragment1) } public override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) newsMainFragment = supportFragmentManager.getFragment(savedInstanceState, "itemsFragment1") as ItemsFragment1? }

  • Make sure only one list to be submit at time (ignoring too much changes and keep last one only):

      abstract class MyPagedListAdapter<T, VH : RecyclerView.ViewHolder> 
       (diffCallback: DiffUtil.ItemCallback<T>) : PagedListAdapter<T, VH> 
      .(diffCallback) {
       var submitting = false
       val latestPage: Queue<PagedList<T>?> = LinkedList()
      val runnable = Runnable {
          synchronized(this) {
              submitting = false
              if (latestPage.size > 0) {
                  submitFromLatest()
              }
          }
      }
    
      override fun submitList(pagedList: PagedList<T>?) {
          synchronized(this) {
              if(latestPage.size > 0){
                  Log.d("MyPagedListAdapter","ignored ${latestPage.size}")
              }
              latestPage.clear()
              latestPage.add(pagedList)
              submitFromLatest()
    
          }
      }
    
      fun submitFromLatest() {
          synchronized(this) {
              if (!submitting) {
                  submitting = true
                  if (latestPage.size > 1 || latestPage.size < 1) {
                      Crashlytics.logException(Throwable("latestPage size is ${latestPage.size}"))
                      Log.d("MyPagedListAdapter","latestPage size is ${latestPage.size}")
                  }
                  super.submitList(latestPage.poll(), runnable)
              }
          }
        }
      } 
    

UPDATED 2 (fix but not recommended, see update 3):

This fix will catch the error not prevent the issue from happening as it is related to the Handler Class from the Android SDK when trying to call Handler.createAsync some old devices with old sdk will create synchronised handler which will lead to the crash

In your adapter that exptend PagedListAdapter add the following :

init {
  
    try {
        val mDiffer = PagedListAdapter::class.java.getDeclaredField("mDiffer")
        val excecuter = AsyncPagedListDiffer::class.java.getDeclaredField("mMainThreadExecutor")
        mDiffer.isAccessible = true
        excecuter.isAccessible = true

        val myDiffer = mDiffer.get(this) as AsyncPagedListDiffer<*>
        val foreGround = object : Executor {
            val mHandler = createAsync(Looper.getMainLooper())
            override fun execute(command: Runnable?) {
                try {
                    mHandler.post {
                        try {
                            command?.run()
                        } catch (e: Exception) {
                            e.printStackTrace()
                        }
                    }
                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }
        }


        excecuter.set(myDiffer, foreGround)
    } catch (e: Exception) {
        e.printStackTrace()
    }

}

private fun createAsync(looper: Looper): Handler {
        if (Build.VERSION.SDK_INT >= 28) {
            return Handler.createAsync(looper)
        }
        if (Build.VERSION.SDK_INT >= 16) {
            try {
                return Handler::class.java.getDeclaredConstructor(Looper::class.java, Handler.Callback::class.java,
                        Boolean::class.javaPrimitiveType)
                        .newInstance(looper, null, true)
            } catch (ignored: IllegalAccessException) {
            } catch (ignored: InstantiationException) {
            } catch (ignored: NoSuchMethodException) {
            } catch (e: InvocationTargetException) {
                return Handler(looper)
            }

        }
        return Handler(looper)
    }

Also if you are using pro-guard or R8 add the following rules:

-keep class androidx.paging.PagedListAdapter.** { *; }
-keep class androidx.paging.AsyncPagedListDiffer.** { *; }

ofc this is a work around so when updating the sdk be careful with the naming does not change for the reflection .

UPDATED 3 (14/2/2020):

use :

 setEnablePlaceholders(false)

but after updating paging library to :

 implementation 'androidx.paging:paging-runtime-ktx:2.1.2'

the flash issue got fixed

like image 103
Ahmed na Avatar answered Oct 23 '22 13:10

Ahmed na