teratail header banner
teratail header banner
質問するログイン新規登録

質問編集履歴

2

問題となる部分が違うことに気がついたので、問題の根本的な部分にフォーカスするように修正しました。

2020/12/26 05:17

投稿

iMASAKI
iMASAKI

スコア12

title CHANGED
@@ -1,1 +1,1 @@
1
- LiveDataの更新によるDiffUtilの挙動
1
+ [RecyclerView] DBの更新による並び替えの挙動
body CHANGED
@@ -1,97 +1,205 @@
1
1
  ### 問題になっていること
2
- `ItemTouchHelper.SimpleCallback`を用いてリストの並び替えを行っているのでが、
2
+ `ItemTouchHelper.SimpleCallback`を用いてRecyclerViewの並び替えの実装てい
3
- 並び替えの方法よって`RecyclerView`がた更新になっしま
3
+ 並び替えをする際DBと同するようにしているのでが、DBの更新によるコールバックを受けると、
4
+ 並び替えが期待するものと違った動作をしてしまいます。
4
5
 
5
- ### 並び替えの対象となるクラス
6
6
  ```kotlin
7
+ // DBからの更新を受け取り、リストを更新
8
+ Repository.items.observe(viewLifecycleOwner) {
7
- data class Task(
9
+ adapter.submitList(it)
8
- val id: TaskId = TaskId(),
9
- val title: TaskTitle,
10
- val isDone: Boolean = false
11
- )
10
+ }
12
11
  ```
12
+ - 初期状態
13
+ ![初期状態](9f793c0b33255ef5334cb3b455f01480.png)
13
14
 
14
- ### 並び替えの方法
15
+ - 並び替え直後
15
- メソッド戻り値としてはどちらの方法とっても同じ値を取るのでが、
16
+ 最初タイル最後へ移動しま
16
- 1番の方法をとると`RecyclerView`の更新が正常に行われません。
17
+ ![並び替え直後](09106009101dbab97fd080e1121a71f2.png)
17
18
 
19
+ - DBのコールバックによる更新
20
+ DBから並び替えが行われたデータの通知が来ます。
21
+ このデータは(1,2,3,4,0)と正しく並び替えられていて、リストの更新は起きないことが期待されます。
18
- 1 `remove()`と`add()`メソッドを利用して更新
22
+  かし、実装によっ以下のようにリストが勝手に更新されてしまいます。
23
+ ![DBのコールバックによる更新](4c804506933d829bd7bde578182cc706.png)
24
+
25
+
26
+ ### 並び替えの実装
27
+ この実装の方法によって問題となっている並び替えが発生しないことがあります。
28
+ まず、問題が発生してしまう実装から提示します。
29
+ ItemListでItemの順番を管理しています。
30
+ ややこしいですが、私はDBにFirestoreを利用していて、どうしてもこのような実装になってしまいます。
19
31
  ```kotlin
32
+ // Itemの順番をIDにより管理する
33
+ data class ItemList(
20
- fun reorder(start: Int, end: Int): List<Task> {
34
+ val itemIds: List<Int>
35
+ )
36
+ // データの中身
37
+ data class Item(
38
+ val id: Int,
39
+ val value: Int
40
+ )
41
+ ```
42
+ ```kotlin
43
+ object Repository {
44
+ private val _items = List(5) { Item(id = it, value = it) }
21
- if (start < 0 || start > this.tasks.size) throw RuntimeException()
45
+ private val _list = ItemList(itemIds = List(5) { it })
22
- if (end < 0 || end > this.tasks.size) throw RuntimeException()
23
- if (start == end) throw RuntimeException()
24
46
 
25
- val reordered = tasks.toMutableList()
47
+ private val list = MutableLiveData(_list)
48
+ val items: LiveData<List<Item>> = list.map { list ->
26
- val target = reordered[start]
49
+ // データをlist.itemIdsの順番に並び替える
27
- reordered.removeAt(start)
28
- reordered.add(end, target)
50
+ list.itemIds.map { id ->
29
- return reordered
51
+ _items.first { it.id == id }
52
+ }
30
53
  }
54
+
55
+ suspend fun reorder(start: Int, end: Int) {
56
+ delay(500)
57
+ if (start < end) {
58
+ for (i in start until end) {
59
+ Collections.swap(_list.itemIds, i, i + 1)
60
+ }
61
+ } else {
62
+ for (i in start downTo end + 1) {
63
+ Collections.swap(_list.itemIds, i, i - 1)
64
+ }
65
+ }
66
+ list.value = _list
67
+ }
68
+ }
31
69
  ```
70
+ 次に、問題になっている並び替えが発生しない実装です。
71
+ 素直にItemをListで保持し、それを並び替えています。
72
+ ```kotlin
32
- 2 `Collections.swap`を利用して更新
73
+ object Repository {
33
- ```
74
+ private val _items = List(5) { Item(id = it, value = it) }
75
+ val items: MutableLiveData<List<Item>> = MutableLiveData(_items)
76
+
34
- fun reorder(start: Int, end: Int): List<Task> {
77
+ suspend fun reorder(start: Int, end: Int) {
78
+ delay(500)
35
79
  if (start < end) {
36
80
  for (i in start until end) {
37
- Collections.swap(this.tasks, i, i + 1)
81
+ Collections.swap(_items, i, i + 1)
38
82
  }
39
83
  } else {
40
84
  for (i in start downTo end + 1) {
41
- Collections.swap(this.tasks, i, i - 1)
85
+ Collections.swap(_items, i, i - 1)
42
86
  }
43
87
  }
44
-
45
- return tasks
88
+ items.value = _items
46
89
  }
90
+ }
47
91
  ```
48
- ### 実際に発生する問題
49
- 並び替えの方法の一番目の方法をとると、以下のように2回更新がかけられてしまいます。
50
- ###### 例) 1 -> 2に並び替え
51
- 1. 初期状態
52
- ![初期状態](62e0e101112e1ee5356b324656f89028.png)
53
- 1. onMove内の`notifyItemMoved`による並び替え(期待する状態)
54
- ![並び替え](79092f8d6c7031d7ced766937c3cadbf.png)
55
- 1. LiveData更新にともなうDiffUtilによる並び替え(再び並び替えが発生)
56
- ![LiveData](62e0e101112e1ee5356b324656f89028.png)
57
92
 
58
- ### 関連するコード
93
+ ### RecyclerView周りの実装
59
94
  ```kotlin
60
- // ListAdapter内のデータの更新処理
95
+ class MyDiffUtil : DiffUtil.ItemCallback<Item>() {
61
- viewModel.tasks.observe(lifecycleOwner, { tasks ->
96
+ override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
62
- adapter.submitList(tasks)
97
+ return oldItem.id == newItem.id
63
- })
98
+ }
99
+
100
+ override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
101
+ return oldItem == newItem
102
+ }
103
+ }
64
104
  ```
105
+ ```
106
+ class MyViewHolder(private val binding: FragmentFirstBinding) :
107
+ RecyclerView.ViewHolder(binding.root) {
65
108
 
66
- ```kotlin
67
- // Adapter
109
+ companion object {
68
- class EditTaskListAdapter(private val viewModel: TaskViewModel) :
110
+ fun from(parent: ViewGroup): MyViewHolder {
111
+ val inflater = LayoutInflater.from(parent.context)
69
- ListAdapter<Task, EditTaskViewHolder>(EditTaskListDiffItemCallback()) {
112
+ val binding = FragmentFirstBinding.inflate(inflater, parent, false)
113
+ return MyViewHolder(binding)
114
+ }
115
+ }
70
116
 
71
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EditTaskViewHolder {
117
+ fun bind(num: Int) {
72
- return EditTaskViewHolder.from(parent)
118
+ binding.myText.text = num.toString()
73
119
  }
120
+ }
121
+ ```
122
+ ```
123
+ class MyAdapter : ListAdapter<Item, MyViewHolder>(MyDiffUtil()) {
124
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
125
+ return MyViewHolder.from(parent)
126
+ }
74
127
 
75
- override fun onBindViewHolder(holder: EditTaskViewHolder, position: Int) {
128
+ override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
76
- val task = getItem(position)
129
+ return holder.bind(getItem(position).value)
77
- holder.bind(viewModel, task)
78
130
  }
79
131
  }
132
+ ```
80
133
 
134
+ ### ItemTouchHelper
135
+ DBの更新頻度を減らすために、並び替え終了時にDBを更新しています
136
+ ```kotlin
137
+ class MyItemTouchHelperCallback(
138
+ private val callback: Callback
139
+ ) : SimpleCallback(
140
+ UP or DOWN or LEFT or RIGHT,
141
+ 0
142
+ ) {
143
+ private var start: Int? = null
144
+ private var end: Int? = null
81
145
 
146
+ override fun onMove(
147
+ recyclerView: RecyclerView,
148
+ viewHolder: RecyclerView.ViewHolder,
149
+ target: RecyclerView.ViewHolder
150
+ ): Boolean {
151
+ val from = viewHolder.adapterPosition
152
+ val to = target.adapterPosition
153
+
154
+ start = start ?: from
155
+ end = to
156
+
157
+ recyclerView.adapter?.notifyItemMoved(from, to)
158
+ return true
159
+ }
160
+
161
+ override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
162
+ super.clearView(recyclerView, viewHolder)
163
+ if (start != null && end != null)
164
+ callback.reorder(start!!, end!!)
165
+
166
+ start = null
167
+ end = null
168
+ }
169
+
170
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
171
+ }
172
+
173
+ interface Callback {
174
+ fun reorder(start: Int, end: Int)
175
+ }
176
+ }
82
177
  ```
83
178
  ```kotlin
179
+ class MainFragment : Fragment(), MyItemTouchHelperCallback.Callback {
180
+ override fun onCreateView(
181
+ inflater: LayoutInflater,
182
+ container: ViewGroup?,
183
+ savedInstanceState: Bundle?
184
+ ): View? {
84
- // DiffUtil
185
+ // ...
186
+ Repository.items.observe(viewLifecycleOwner) {
187
+ adapter.submitList(it)
188
+ }
85
- class EditTaskListDiffItemCallback : DiffUtil.ItemCallback<Task>(){
189
+ val itemTouchHelper = ItemTouchHelper(MyItemTouchHelperCallback(this))
86
- override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean {
190
+ itemTouchHelper.attachToRecyclerView(binding.listView)
191
+
87
- return oldItem.id == newItem.id
192
+ return binding.root
88
193
  }
89
194
 
90
- override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean {
195
+ override fun reorder(start: Int, end: Int) {
196
+ lifecycleScope.launch {
91
- return oldItem == newItem
197
+ Repository.reorder(start, end)
198
+ }
92
199
  }
93
-
94
200
  }
95
201
  ```
202
+
96
203
  ### 回答していただきたいこと
97
- なぜ`Collections.swap`利用並び替えを行うと、上記のような問題発生しないのかを教えていただきたいです。
204
+ データの本体とデータの順番分けた実装で、どのようにすれば正並び替えができるのかを教えていただきたいです。
205
+ また、何故この方法ではうまく行かないのかを教えていただけると助かります。

1

問題となるところを回避する方法を見つけたので、根本的な問題と関係ないところを削除しました。

2020/12/26 05:17

投稿

iMASAKI
iMASAKI

スコア12

title CHANGED
@@ -1,1 +1,1 @@
1
- RecyclerViewのItemTouchHelper内での更新とLiveDataの更新の競合
1
+ LiveDataの更新によるDiffUtil挙動
body CHANGED
@@ -1,90 +1,68 @@
1
1
  ### 問題になっていること
2
- 以下、順を追って問題点を共有させていただきますが、主となる問題点はItemTouchHelper内の`notifyItemMoved`と
3
- データの更新時にDiffUtilが発生させる`notify`系のメソッドが競合して、**予期しないリストの更新**が発生するとう点です
2
+ `ItemTouchHelper.SimpleCallback`を用リストの並び替えを行ってるのですが、
4
- とき**データは正常更新されおり**、Viewの更新に問題が発生しています。
3
+ 並び替え方法よっ`RecyclerView`が期待に反した更新になっしまいます。
5
4
 
6
- ###### ItemTouchHelperによトの更新
5
+ ### 並び替えの対象となクラ
7
- RecyclerViewにItemTouchHelperを適用して、リストアイテムの並び替え処理を実装しています。
8
- DBとして`Firestore`を採用しており、更新を頻繁にできないので並び替えが終了した段階でDBを更新しています。
9
6
  ```kotlin
7
+ data class Task(
10
- class EditTaskItemTouchHelperCallback(
8
+ val id: TaskId = TaskId(),
11
- private val action: ItemCallbackAction
12
- ) : SimpleCallback(
9
+ val title: TaskTitle,
13
- UP or DOWN,
14
- 0
15
- ) {
16
- interface ItemCallbackAction{
17
- fun onReorder(start: Int, end: Int)
10
+ val isDone: Boolean = false
18
- }
11
+ )
12
+ ```
19
13
 
20
- // 並び替えの最初と最後だけを保持
14
+ ### 並び替えの方法
15
+ メソッドの戻り値としてはどちらの方法をとっても同じ値を取るのですが、
21
- private var start: Int? = null
16
+ 1番の方法をとると`RecyclerView`の更新が正常に行われません。
22
- private var end: Int? = null
23
17
 
24
- override fun onMove(
25
- recyclerView: RecyclerView,
26
- viewHolder: RecyclerView.ViewHolder,
27
- target: RecyclerView.ViewHolder
18
+ 1 `remove()`と`add()`メソッドを利用して更新
28
- ): Boolean {
19
+ ```kotlin
29
- val positionStart = viewHolder.adapterPosition
20
+ fun reorder(start: Int, end: Int): List<Task> {
21
+ if (start < 0 || start > this.tasks.size) throw RuntimeException()
22
+ if (end < 0 || end > this.tasks.size) throw RuntimeException()
30
- val positionEnd = target.adapterPosition
23
+ if (start == end) throw RuntimeException()
31
24
 
25
+ val reordered = tasks.toMutableList()
32
- start = start ?: positionStart
26
+ val target = reordered[start]
33
- end = positionEnd
27
+ reordered.removeAt(start)
34
-
35
- // (問題となる箇所)
28
+ reordered.add(end, target)
36
- // リストに更新を通知
37
- recyclerView.adapter?.notifyItemMoved(positionStart, positionEnd)
38
-
39
- return true
29
+ return reordered
40
30
  }
31
+ ```
32
+ 2 `Collections.swap`を利用して更新
33
+ ```
34
+ fun reorder(start: Int, end: Int): List<Task> {
35
+ if (start < end) {
36
+ for (i in start until end) {
37
+ Collections.swap(this.tasks, i, i + 1)
38
+ }
39
+ } else {
40
+ for (i in start downTo end + 1) {
41
+ Collections.swap(this.tasks, i, i - 1)
42
+ }
43
+ }
41
44
 
42
- override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
43
- super.clearView(recyclerView, viewHolder)
44
-
45
- if (start != null && end != null)
46
-        // 並び替えが完了後、DBが更新される
47
- action.onReorder(start!!, end!!)
48
-
49
- start = null
45
+ return tasks
50
- end = null
51
46
  }
47
+ ```
48
+ ### 実際に発生する問題
49
+ 並び替えの方法の一番目の方法をとると、以下のように2回更新がかけられてしまいます。
50
+ ###### 例) 1 -> 2に並び替え
51
+ 1. 初期状態
52
+ ![初期状態](62e0e101112e1ee5356b324656f89028.png)
53
+ 1. onMove内の`notifyItemMoved`による並び替え(期待する状態)
54
+ ![並び替え](79092f8d6c7031d7ced766937c3cadbf.png)
55
+ 1. LiveData更新にともなうDiffUtilによる並び替え(再び並び替えが発生)
56
+ ![LiveData](62e0e101112e1ee5356b324656f89028.png)
52
57
 
53
- override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
54
-
55
- }
56
- }
57
- ```
58
- ###### ListAdapter(DiffUtil)によリストの更新
58
+ ### 関連すコード
59
- リストのデータはDBからのLiveDataに従い、更新がかけられます。
60
59
  ```kotlin
61
60
  // ListAdapter内のデータの更新処理
62
61
  viewModel.tasks.observe(lifecycleOwner, { tasks ->
63
62
  adapter.submitList(tasks)
64
63
  })
65
64
  ```
66
- ###### 問題発生
67
- ItemTouchHelperのonMove内で`notifyItemMoved`の処理があるので,並び替えが発生します。
68
- 並び替えが完了するとDBにデータの更新処理をかけます。
69
- 次にDBからLiveDataを受け取りAdapter内で再びリストを更新しようとします。
70
- リストが更新されるとDiffUtilが動作し、内部で`notify`系のメソッドが動作するので、ここでも並び替えが発生します。
71
65
 
72
- 結果、以下のような状況が発生します。
73
- 例) 1 -> 2に並び替え
74
- 1. 初期状態
75
- ![初期状態](62e0e101112e1ee5356b324656f89028.png)
76
- 1. onMove内の`notifyItemMoved`による並び替え
77
- ![並び替え](79092f8d6c7031d7ced766937c3cadbf.png)
78
- 1. LiveData更新にともなうDiffUtilによる並び替え(再び並び替えが発生)
79
- ![LiveData](62e0e101112e1ee5356b324656f89028.png)
80
-
81
- ### 補足
82
- - 並び替え後、DBからのLiveDataは期待通りに並べ替えられています。
83
- - DiffUtilを適用せずにRecyclerViewを用いるとこのような状況は発生しません。
84
- - DBの更新処理をかけなければView自体は正常に並び替えられます。(データを更新することはできませんが...)
85
-
86
- ### 他の関連するコード
87
- RecyclerViewにDiffUtilを適用させた状態で実装しています。
88
66
  ```kotlin
89
67
  // Adapter
90
68
  class EditTaskListAdapter(private val viewModel: TaskViewModel) :
@@ -115,6 +93,5 @@
115
93
 
116
94
  }
117
95
  ```
118
- ### していただきたいこと
96
+ ### 答していただきたいこと
119
- DiffUtilの利用ながらも、並び替え時に発生するDB更新処理最低限にできる方法があれば教えていただきたいです。
97
+ なぜ`Collections.swap`を利用し並び替えを行うと、上記のような問題が発生しないを教えていただきたいです。
120
- 他のデバイスとのデータの同期をしたいので、ページを戻るときにデータを一括更新するなどはしたくないです。