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?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With