問題背景
前不久我們項目中由用戶反饋說遇到筆記重複的問題,而且不只一次遇到類似的反饋。
這種重複筆記總是出現的feed流的中間位置,如下示意圖所示:
這個圖畫的有點醜,湊合看,意思大概就是這樣的。
接下來,我就得追蹤下這個問題了,開始時我幾乎就一口咬定是接口返回的有問題,由於前幾次後端沒有日誌,好像之前的反饋就那麼過去了,直到後面又出現一次重複筆記的問題,這次是公司內部員工出現的,於是後端也通過這個抓到了相應的日誌,發現返回的筆記的確沒有重複的,這下跑不掉了,就是前端的問題。
問題排查
於是,我又重新梳理了下代碼流程,發現有一處比較有嫌疑:
...
if (...) {
mItems[0] = noteItem
} else {
mItems.add(0, noteItem)
}
mAdapter.items = mItems
mAdapter.notifyItemChanged(0)
...
鑑於是公司項目,我就省略掉業務邏輯了,這裏的代碼按照開發者的意圖是當RecyclerView第一個item如果已經是noteItem這種類型的時候,我們就將這個位置item替換成最新的,如果這個位置的item不是noteItem這個數據的話,我們需要手動把它添加到第一個位置去,到這裏實際上都沒有什麼問題。
但是,當我看到mAdapter.notifyItemChanged(0)
這個方法,直覺告訴我這裏好像有點問題,當上面的邏輯走到else這裏的時候,會往list裏add一個新的item,但是這時候調用的刷新方法卻是notifyItemChanged(0)。
這個notifyItemChanged明顯是刷新某個item的方法,即當這個item裏的數據有變化時,調用這個方法去刷新這個item區域的UI,但是如果我們在adapter中add了一個新的item,再調用這個方法明顯是不行的,這裏是導致重複的原因嘛,我其實也不太確定。
問題復現
於是我寫了一個Demo試了下。
class RecyclerViewActivity : AppCompatActivity() {
private val mDataList = ArrayList<Any>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recycler_view)
val layoutManager = LinearLayoutManager(this)
layoutManager.orientation = RecyclerView.VERTICAL
recyclerView.layoutManager = layoutManager
val customAdapter = CustomAdapter(mDataList)
recyclerView.adapter = customAdapter
for (i in 0..5) {
mDataList.add("text: $i")
}
recyclerView.adapter?.notifyDataSetChanged()
}
//刷新方法
fun refresh(view: View) {
mDataList.add(0, "add item")
recyclerView.adapter?.notifyItemChanged(0)
}
}
構造一個普通的feed列表,每次點擊刷新按鈕,就會調用刷新方法,調用刷新方法的時候往index爲0的位置再add一個item,然後再調用notifyItemChanged(0)方法,上下滑動後,發現數據是重複了。
下面放個gif圖展示下效果。
從gif圖中可以看到,點擊刷新按鈕,添加了”add item“,往下滑動後,出現了兩個”text:5“的item,這個就是重複的item。
問題原因
我們看到notifyItemChanged的文檔說明:
notifyItemChanged(int position)
This is an item change event, not a structural change event.
RecyclerView中有兩種不同數據改變事件,一種叫(item changes)項目改變,另一種叫(structual changes)結構改變。項目改變指的是某個單個item的數據發生變化,這個時候沒有位置的改變。而structual changes則是有位置的變化發生,主要是數據源的變化會導致RecyclerView item位置發生變化。
我們這個問題是我們往數據源前面加了一個item,這個時候應該需要調用具有structual changes 的方法來刷新,而不是採用notifyItemChanged來刷新,因爲notifyItemChanged是一個item changes 的方法。