問題になっていること
ItemTouchHelper.SimpleCallback
を用いてRecyclerViewの並び替えの実装をしています。
並び替えをする際にDBと同期するようにしているのですが、DBの更新によるコールバックを受けると、
並び替えが期待するものと違った動作をしてしまいます。
kotlin
1// DBからの更新を受け取り、リストを更新 2Repository.items.observe(viewLifecycleOwner) { 3 adapter.submitList(it) 4}
- 初期状態
- 並び替え直後
- DBのコールバックによる更新
DBから並び替えが行われたデータの通知が来ます。
このデータは(1,2,3,4,0)と正しく並び替えられていて、リストの更新は起きないことが期待されます。
しかし、実装によって以下のようにリストが勝手に更新されてしまいます。
並び替えの実装
この実装の方法によって問題となっている並び替えが発生しないことがあります。
まず、問題が発生してしまう実装から提示します。
ItemListでItemの順番を管理しています。
ややこしいですが、私はDBにFirestoreを利用していて、どうしてもこのような実装になってしまいます。
kotlin
1// Itemの順番をIDにより管理する 2data class ItemList( 3 val itemIds: List<Int> 4) 5// データの中身 6data class Item( 7 val id: Int, 8 val value: Int 9)
kotlin
1object Repository { 2 private val _items = List(5) { Item(id = it, value = it) } 3 private val _list = ItemList(itemIds = List(5) { it }) 4 5 private val list = MutableLiveData(_list) 6 val items: LiveData<List<Item>> = list.map { list -> 7 // データをlist.itemIdsの順番に並び替える 8 list.itemIds.map { id -> 9 _items.first { it.id == id } 10 } 11 } 12 13 suspend fun reorder(start: Int, end: Int) { 14 delay(500) 15 if (start < end) { 16 for (i in start until end) { 17 Collections.swap(_list.itemIds, i, i + 1) 18 } 19 } else { 20 for (i in start downTo end + 1) { 21 Collections.swap(_list.itemIds, i, i - 1) 22 } 23 } 24 list.value = _list 25 } 26}
次に、問題になっている並び替えが発生しない実装です。
素直にItemをListで保持し、それを並び替えています。
kotlin
1object Repository { 2 private val _items = List(5) { Item(id = it, value = it) } 3 val items: MutableLiveData<List<Item>> = MutableLiveData(_items) 4 5 suspend fun reorder(start: Int, end: Int) { 6 delay(500) 7 if (start < end) { 8 for (i in start until end) { 9 Collections.swap(_items, i, i + 1) 10 } 11 } else { 12 for (i in start downTo end + 1) { 13 Collections.swap(_items, i, i - 1) 14 } 15 } 16 items.value = _items 17 } 18}
RecyclerView周りの実装
kotlin
1class MyDiffUtil : DiffUtil.ItemCallback<Item>() { 2 override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean { 3 return oldItem.id == newItem.id 4 } 5 6 override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { 7 return oldItem == newItem 8 } 9}
class MyViewHolder(private val binding: FragmentFirstBinding) : RecyclerView.ViewHolder(binding.root) { companion object { fun from(parent: ViewGroup): MyViewHolder { val inflater = LayoutInflater.from(parent.context) val binding = FragmentFirstBinding.inflate(inflater, parent, false) return MyViewHolder(binding) } } fun bind(num: Int) { binding.myText.text = num.toString() } }
class MyAdapter : ListAdapter<Item, MyViewHolder>(MyDiffUtil()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { return MyViewHolder.from(parent) } override fun onBindViewHolder(holder: MyViewHolder, position: Int) { return holder.bind(getItem(position).value) } }
ItemTouchHelper
DBの更新頻度を減らすために、並び替え終了時にDBを更新しています
kotlin
1class MyItemTouchHelperCallback( 2 private val callback: Callback 3) : SimpleCallback( 4 UP or DOWN or LEFT or RIGHT, 5 0 6) { 7 private var start: Int? = null 8 private var end: Int? = null 9 10 override fun onMove( 11 recyclerView: RecyclerView, 12 viewHolder: RecyclerView.ViewHolder, 13 target: RecyclerView.ViewHolder 14 ): Boolean { 15 val from = viewHolder.adapterPosition 16 val to = target.adapterPosition 17 18 start = start ?: from 19 end = to 20 21 recyclerView.adapter?.notifyItemMoved(from, to) 22 return true 23 } 24 25 override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { 26 super.clearView(recyclerView, viewHolder) 27 if (start != null && end != null) 28 callback.reorder(start!!, end!!) 29 30 start = null 31 end = null 32 } 33 34 override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { 35 } 36 37 interface Callback { 38 fun reorder(start: Int, end: Int) 39 } 40}
kotlin
1class MainFragment : Fragment(), MyItemTouchHelperCallback.Callback { 2 override fun onCreateView( 3 inflater: LayoutInflater, 4 container: ViewGroup?, 5 savedInstanceState: Bundle? 6 ): View? { 7 // ... 8 Repository.items.observe(viewLifecycleOwner) { 9 adapter.submitList(it) 10 } 11 val itemTouchHelper = ItemTouchHelper(MyItemTouchHelperCallback(this)) 12 itemTouchHelper.attachToRecyclerView(binding.listView) 13 14 return binding.root 15 } 16 17 override fun reorder(start: Int, end: Int) { 18 lifecycleScope.launch { 19 Repository.reorder(start, end) 20 } 21 } 22}
回答していただきたいこと
データの本体とデータの順番を分けた実装で、どのようにすれば正しく並び替えができるのかを教えていただきたいです。
また、何故この方法ではうまく行かないのかを教えていただけると助かります。
あなたの回答
tips
プレビュー