1、問題如下
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 19(offset:19).state:20 androidx.recyclerview.widget.RecyclerView{2a4d50e VFED..... .......D 0,0-1080,1979 #7f080160 app:id/rvNewsHome}, adapter:com.zj.architecture.mainscreen.TestNewsRvAdapter@fed2b2f, layout:androidx.recyclerview.widget.LinearLayoutManager@7de53c, context:com.zj.architecture.testrv.TestRvActivity@f4dbc62
2噪珊、模擬源碼如下
package com.zj.architecture.testrv
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.zj.architecture.R
import com.zj.architecture.mainscreen.TestNewsRvAdapter
import com.zj.architecture.repository.NewsItem
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
class TestRvActivity : AppCompatActivity() {
private var dataItem = mutableListOf<NewsItem>()
private val newsRvAdapter by lazy {
TestNewsRvAdapter(
{
}, dataItem
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_rv)
fabStar.setOnClickListener {
dataItem.removeAt(0)
GlobalScope.launch {
delay(3000)
withContext(Dispatchers.Main) {
newsRvAdapter?.notifyItemRangeRemoved(0,dataItem.size)
newsRvAdapter?.notifyDataSetChanged()
}
}
}
newsRvAdapter.setHasStableIds(false)
rvNewsHome.adapter = newsRvAdapter
initData()
srlNewsHome.setOnRefreshListener {
initData()
srlNewsHome.isRefreshing = false
}
}
private fun initData() {
dataItem.clear()
for (i in 0 until 20) {
var imageUrl = "https://t7.baidu.com/it/u=4162611394,4275913936&fm=193&f=GIF"
if (i % 2 == 0) {
imageUrl = "https://t7.baidu.com/it/u=1951548898,3927145&fm=193&f=GIF"
}
dataItem.add(NewsItem("title$i", "descriptioni$i", imageUrl))
}
rvNewsHome.adapter?.notifyDataSetChanged()
}
}
3辨泳、問題拆解
- 以上問題主要是由于內部數(shù)據(jù)源、與外部數(shù)據(jù)源長度不一致導致的要出。
- 內部數(shù)據(jù)源如下:
class TestNewsRvAdapter(private val listener: (View) -> Unit, private var data: List<NewsItem>) :
RecyclerView.Adapter<TestNewsRvAdapter.MyViewHolder>() {
val TAG = "TestNewsRvAdapter"
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
Log.d(TAG, "onCreateViewHolder: ")
return MyViewHolder(inflate(parent.context, R.layout.item_view, parent), listener)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
Log.d(TAG, "onBindViewHolder: ")
holder.bind(data[position])
}
override fun getItemCount() = data.size
override fun getItemId(position: Int): Long {
return data[position].title.hashCode().toLong()
}
inner class MyViewHolder(override val containerView: View, listener: (View) -> Unit) :
RecyclerView.ViewHolder(containerView),
LayoutContainer {
init {
itemView.setOnClickListener(listener)
}
fun bind(newsItem: NewsItem) =
with(itemView) {
itemView.tag = newsItem
tvTitle.text = newsItem.title
tvDescription.text = newsItem.description
ivThumbnail.load(newsItem.imageUrl) {
crossfade(true)
placeholder(R.mipmap.ic_launcher)
}
}
}
}
- 可以知道adapter內部設置的getItemCount,正是我們外部初始化傳入的dataItem
- 在Activity里面,我們對dataItem做了刪除的操作出革。但是沒有立馬執(zhí)行notify等操作
- 通過源碼RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount() + exceptionLabel());
}
- mAdapter.getItemCount()我們可以知道,內部數(shù)據(jù)源初始化的長度是20渡讼、由于我在Activity里面操作了刪除骂束,外部數(shù)據(jù)源dataItem長度變成了19.等待3秒后耳璧,執(zhí)行notify操作。這個時候展箱。我們執(zhí)行滑動操作旨枯。基于RecycleView的復用原理混驰∨矢簦可以知道,會執(zhí)行到以上源碼處栖榨,進行offsetPosition判斷竞慢。由于offsetPosition獲取的位置是根據(jù)外部數(shù)據(jù)源決定的。所以導致了治泥,內部跟外部數(shù)據(jù)源長度不一致筹煮。外部數(shù)據(jù)源長度19,內部數(shù)據(jù)源認為還是20.這個時候就出現(xiàn)了角標越界問題居夹。
解決方案:
1败潦、使用DiffUtils替代notify等操作,原理后續(xù)分析
2准脂、對外部數(shù)據(jù)源操作后劫扒,要及時執(zhí)行notify等操作。切勿類似demo中有延遲狸膏。很多業(yè)務其實都會忽略這一點沟饥,進行了很多耗時操作后,再執(zhí)行notify操作湾戳。這個會導致內部部數(shù)據(jù)源不一致的角標越界崩潰
3贤旷、網上很多說法,自定義LinerLayoutManager砾脑,個人認為這個無效幼驶。可能因為執(zhí)行順序的原因吧韧衣。
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
super.onLayoutChildren(recycler, state);
} catch (IndexOutOfBoundsException e) {
e.printStackTrace();
}
}
4盅藻、網上還有很多說法,去除動畫,其實畅铭,個人認為也沒什么必要氏淑。角標越界,從源碼分析來看硕噩,基本都是內外部數(shù)據(jù)源長度不一致導致的假残。動畫的執(zhí)行,耗時很短榴徐,類似我上面做的延遲守问。其實在這一點上匀归,如果不是很花里胡哨的寫了一堆動畫坑资,正常不用去掉
rvNewsHome.animation = null
5耗帕、以上4點我認為還有優(yōu)化空間,基于舊業(yè)務袱贮,不太可能大改仿便。所以,還在思考怎么處理更加合適攒巍。暫且先記錄