質問編集履歴

2

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

2020/12/26 05:17

投稿

iMASAKI
iMASAKI

スコア12

test CHANGED
@@ -1 +1 @@
1
- LiveDataの更新によるDiffUtilの挙動
1
+ [RecyclerView] DBの更新による並び替えの挙動
test CHANGED
@@ -1,76 +1,120 @@
1
1
  ### 問題になっていること
2
2
 
3
- `ItemTouchHelper.SimpleCallback`を用いてリストの並び替えを行っているのでが、
4
-
5
- 並び替えの方法によ`RecyclerView`期待に反した更新になってしまいます。
6
-
7
-
8
-
9
- ### 並び替えの対象となるクラス
10
-
11
- ```kotlin
12
-
13
- data class Task(
14
-
15
- val id: TaskId = TaskId(),
16
-
17
- val title: TaskTitle,
18
-
19
- val isDone: Boolean = false
20
-
21
- )
22
-
23
- ```
24
-
25
-
26
-
27
- ### 並び替えの方法
28
-
29
- メソッドの戻り値としてはどちらの方法をとっても同じ値を取るのですが、
30
-
31
- 1番方法とると`RecyclerView`の更新が正常に行われせん
32
-
33
-
34
-
35
- 1 `remove()`と`add()`メソッドを利用して更新
36
-
37
- ```kotlin
38
-
39
- fun reorder(start: Int, end: Int): List<Task> {
40
-
41
- if (start < 0 || start > this.tasks.size) throw RuntimeException()
42
-
43
- if (end < 0 || end > this.tasks.size) throw RuntimeException()
44
-
45
- if (start == end) throw RuntimeException()
46
-
47
-
48
-
49
- val reordered = tasks.toMutableList()
50
-
51
- val target = reordered[start]
52
-
53
- reordered.removeAt(start)
54
-
55
- reordered.add(end, target)
56
-
57
- return reordered
58
-
59
- }
60
-
61
- ```
62
-
63
- 2 `Collections.swap`利用して更新
64
-
65
- ```
66
-
67
- fun reorder(start: Int, end: Int): List<Task> {
3
+ `ItemTouchHelper.SimpleCallback`を用いてRecyclerViewの並び替えの実装てい
4
+
5
+ 並び替えをする際DBと同期するうにしいるのです、DBの更新によるコールバックを受けると、
6
+
7
+ 並び替えが期待するものと違った動作をしてしまいます。
8
+
9
+
10
+
11
+ ```kotlin
12
+
13
+ // DBからの更新を受け取り、リストを更新
14
+
15
+ Repository.items.observe(viewLifecycleOwner) {
16
+
17
+ adapter.submitList(it)
18
+
19
+ }
20
+
21
+ ```
22
+
23
+ - 初期状態
24
+
25
+ ![初期状態](9f793c0b33255ef5334cb3b455f01480.png)
26
+
27
+
28
+
29
+ - 並び替え直後
30
+
31
+ 最初タイル最後へ移動し
32
+
33
+ ![並び替え直後](09106009101dbab97fd080e1121a71f2.png)
34
+
35
+
36
+
37
+ - DBのコールバックによる更新
38
+
39
+ DBから並び替えが行われたデータの通知が来ます。
40
+
41
+ このデータは(1,2,3,4,0)と正しく並び替えられていて、リストの更新は起きないことが期待されます。
42
+
43
+  しかし、実装によって以下のようにリストが勝手に更新されてしまいます。
44
+
45
+ ![DBのコールバックによる更新](4c804506933d829bd7bde578182cc706.png)
46
+
47
+
48
+
49
+
50
+
51
+ ### 並び替えの実装
52
+
53
+ この実装の方法によって問題となっている並び替えが発生しないことがあります。
54
+
55
+ まず、問題が発生してしまう実装から提示します。
56
+
57
+ ItemListでItemの順番を管理しています。
58
+
59
+ ややこしいですが、私はDBにFirestoreを利用していて、どうしてもこのような実装になってしまいます。
60
+
61
+ ```kotlin
62
+
63
+ // Itemの順番IDにより管理する
64
+
65
+ data class ItemList(
66
+
67
+ val itemIds: List<Int>
68
+
69
+ )
70
+
71
+ // データの中身
72
+
73
+ data class Item(
74
+
75
+ val id: Int,
76
+
77
+ val value: Int
78
+
79
+ )
80
+
81
+ ```
82
+
83
+ ```kotlin
84
+
85
+ object Repository {
86
+
87
+ private val _items = List(5) { Item(id = it, value = it) }
88
+
89
+ private val _list = ItemList(itemIds = List(5) { it })
90
+
91
+
92
+
93
+ private val list = MutableLiveData(_list)
94
+
95
+ val items: LiveData<List<Item>> = list.map { list ->
96
+
97
+ // データをlist.itemIdsの順番に並び替える
98
+
99
+ list.itemIds.map { id ->
100
+
101
+ _items.first { it.id == id }
102
+
103
+ }
104
+
105
+ }
106
+
107
+
108
+
109
+ suspend fun reorder(start: Int, end: Int) {
110
+
111
+ delay(500)
68
112
 
69
113
  if (start < end) {
70
114
 
71
115
  for (i in start until end) {
72
116
 
73
- Collections.swap(this.tasks, i, i + 1)
117
+ Collections.swap(_list.itemIds, i, i + 1)
74
118
 
75
119
  }
76
120
 
@@ -78,97 +122,73 @@
78
122
 
79
123
  for (i in start downTo end + 1) {
80
124
 
81
- Collections.swap(this.tasks, i, i - 1)
125
+ Collections.swap(_list.itemIds, i, i - 1)
82
126
 
83
127
  }
84
128
 
85
129
  }
86
130
 
87
-
88
-
89
- return tasks
90
-
91
- }
92
-
93
- ```
94
-
95
- ### 実際に発生する問題
96
-
97
- 並び替えの方法の一番目の方法をとると、以下のように2回更新がかけられしまいます。
98
-
99
- ###### 例) 1 -> 2に並び替え
100
-
101
- 1. 初期状態
102
-
103
- ![初期状態](62e0e101112e1ee5356b324656f89028.png)
104
-
105
- 1. onMove内の`notifyItemMoved`による並び替え(期待する状態)
106
-
107
- ![並び替え](79092f8d6c7031d7ced766937c3cadbf.png)
108
-
109
- 1. LiveData更新にともなうDiffUtilによる並び替え(再び並び替えが発生)
110
-
111
- ![LiveData](62e0e101112e1ee5356b324656f89028.png)
112
-
113
-
114
-
115
- ### 関連するコード
116
-
117
- ```kotlin
118
-
119
- // ListAdapter内のデータの更新処理
120
-
121
- viewModel.tasks.observe(lifecycleOwner, { tasks ->
122
-
123
- adapter.submitList(tasks)
124
-
125
- })
126
-
127
- ```
128
-
129
-
130
-
131
- ```kotlin
132
-
133
- // Adapter
134
-
135
- class EditTaskListAdapter(private val viewModel: TaskViewModel) :
136
-
137
- ListAdapter<Task, EditTaskViewHolder>(EditTaskListDiffItemCallback()) {
138
-
139
-
140
-
141
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EditTaskViewHolder {
142
-
143
- return EditTaskViewHolder.from(parent)
144
-
145
- }
146
-
147
-
148
-
149
- override fun onBindViewHolder(holder: EditTaskViewHolder, position: Int) {
150
-
151
- val task = getItem(position)
152
-
153
- holder.bind(viewModel, task)
154
-
155
- }
156
-
157
- }
158
-
159
-
160
-
161
-
162
-
163
- ```
164
-
165
- ```kotlin
166
-
167
- // DiffUtil
168
-
169
- class EditTaskListDiffItemCallback : DiffUtil.ItemCallback<Task>(){
170
-
171
- override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean {
131
+ list.value = _list
132
+
133
+ }
134
+
135
+ }
136
+
137
+ ```
138
+
139
+ 、問題になっている並び替えが発生しない実装で
140
+
141
+ 素直にItemをListで保持し、それを並び替えています。
142
+
143
+ ```kotlin
144
+
145
+ object Repository {
146
+
147
+ private val _items = List(5) { Item(id = it, value = it) }
148
+
149
+ val items: MutableLiveData<List<Item>> = MutableLiveData(_items)
150
+
151
+
152
+
153
+ suspend fun reorder(start: Int, end: Int) {
154
+
155
+ delay(500)
156
+
157
+ if (start < end) {
158
+
159
+ for (i in start until end) {
160
+
161
+ Collections.swap(_items, i, i + 1)
162
+
163
+ }
164
+
165
+ } else {
166
+
167
+ for (i in start downTo end + 1) {
168
+
169
+ Collections.swap(_items, i, i - 1)
170
+
171
+ }
172
+
173
+ }
174
+
175
+ items.value = _items
176
+
177
+ }
178
+
179
+ }
180
+
181
+ ```
182
+
183
+
184
+
185
+ ### RecyclerView周りの実装
186
+
187
+ ```kotlin
188
+
189
+ class MyDiffUtil : DiffUtil.ItemCallback<Item>() {
190
+
191
+ override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
172
192
 
173
193
  return oldItem.id == newItem.id
174
194
 
@@ -176,18 +196,214 @@
176
196
 
177
197
 
178
198
 
179
- override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean {
199
+ override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
180
200
 
181
201
  return oldItem == newItem
182
202
 
183
203
  }
184
204
 
185
-
186
-
187
- }
205
+ }
188
-
206
+
189
- ```
207
+ ```
208
+
209
+ ```
210
+
211
+ class MyViewHolder(private val binding: FragmentFirstBinding) :
212
+
213
+ RecyclerView.ViewHolder(binding.root) {
214
+
215
+
216
+
217
+ companion object {
218
+
219
+ fun from(parent: ViewGroup): MyViewHolder {
220
+
221
+ val inflater = LayoutInflater.from(parent.context)
222
+
223
+ val binding = FragmentFirstBinding.inflate(inflater, parent, false)
224
+
225
+ return MyViewHolder(binding)
226
+
227
+ }
228
+
229
+ }
230
+
231
+
232
+
233
+ fun bind(num: Int) {
234
+
235
+ binding.myText.text = num.toString()
236
+
237
+ }
238
+
239
+ }
240
+
241
+ ```
242
+
243
+ ```
244
+
245
+ class MyAdapter : ListAdapter<Item, MyViewHolder>(MyDiffUtil()) {
246
+
247
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
248
+
249
+ return MyViewHolder.from(parent)
250
+
251
+ }
252
+
253
+
254
+
255
+ override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
256
+
257
+ return holder.bind(getItem(position).value)
258
+
259
+ }
260
+
261
+ }
262
+
263
+ ```
264
+
265
+
266
+
267
+ ### ItemTouchHelper
268
+
269
+ DBの更新頻度を減らすために、並び替え終了時にDBを更新しています
270
+
271
+ ```kotlin
272
+
273
+ class MyItemTouchHelperCallback(
274
+
275
+ private val callback: Callback
276
+
277
+ ) : SimpleCallback(
278
+
279
+ UP or DOWN or LEFT or RIGHT,
280
+
281
+ 0
282
+
283
+ ) {
284
+
285
+ private var start: Int? = null
286
+
287
+ private var end: Int? = null
288
+
289
+
290
+
291
+ override fun onMove(
292
+
293
+ recyclerView: RecyclerView,
294
+
295
+ viewHolder: RecyclerView.ViewHolder,
296
+
297
+ target: RecyclerView.ViewHolder
298
+
299
+ ): Boolean {
300
+
301
+ val from = viewHolder.adapterPosition
302
+
303
+ val to = target.adapterPosition
304
+
305
+
306
+
307
+ start = start ?: from
308
+
309
+ end = to
310
+
311
+
312
+
313
+ recyclerView.adapter?.notifyItemMoved(from, to)
314
+
315
+ return true
316
+
317
+ }
318
+
319
+
320
+
321
+ override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
322
+
323
+ super.clearView(recyclerView, viewHolder)
324
+
325
+ if (start != null && end != null)
326
+
327
+ callback.reorder(start!!, end!!)
328
+
329
+
330
+
331
+ start = null
332
+
333
+ end = null
334
+
335
+ }
336
+
337
+
338
+
339
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
340
+
341
+ }
342
+
343
+
344
+
345
+ interface Callback {
346
+
347
+ fun reorder(start: Int, end: Int)
348
+
349
+ }
350
+
351
+ }
352
+
353
+ ```
354
+
355
+ ```kotlin
356
+
357
+ class MainFragment : Fragment(), MyItemTouchHelperCallback.Callback {
358
+
359
+ override fun onCreateView(
360
+
361
+ inflater: LayoutInflater,
362
+
363
+ container: ViewGroup?,
364
+
365
+ savedInstanceState: Bundle?
366
+
367
+ ): View? {
368
+
369
+ // ...
370
+
371
+ Repository.items.observe(viewLifecycleOwner) {
372
+
373
+ adapter.submitList(it)
374
+
375
+ }
376
+
377
+ val itemTouchHelper = ItemTouchHelper(MyItemTouchHelperCallback(this))
378
+
379
+ itemTouchHelper.attachToRecyclerView(binding.listView)
380
+
381
+
382
+
383
+ return binding.root
384
+
385
+ }
386
+
387
+
388
+
389
+ override fun reorder(start: Int, end: Int) {
390
+
391
+ lifecycleScope.launch {
392
+
393
+ Repository.reorder(start, end)
394
+
395
+ }
396
+
397
+ }
398
+
399
+ }
400
+
401
+ ```
402
+
403
+
190
404
 
191
405
  ### 回答していただきたいこと
192
406
 
193
- なぜ`Collections.swap`利用並び替えを行うと、上記のような問題発生しないのかを教えていただきたいです。
407
+ データの本体とデータの順番分けた実装で、どのようにすれば正並び替えができるのかを教えていただきたいです。
408
+
409
+ また、何故この方法ではうまく行かないのかを教えていただけると助かります。

1

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

2020/12/26 05:17

投稿

iMASAKI
iMASAKI

スコア12

test CHANGED
@@ -1 +1 @@
1
- RecyclerViewのItemTouchHelper内での更新とLiveDataの更新の競合
1
+ LiveDataの更新によるDiffUtil挙動
test CHANGED
@@ -1,120 +1,118 @@
1
1
  ### 問題になっていること
2
2
 
3
- 以下、順を追って問題点を共有させていただきますが、主となる問題点はItemTouchHelper内の`notifyItemMoved`
3
+ `ItemTouchHelper.SimpleCallback`を用いてリストの並び替えを行っているのですが、
4
4
 
5
- データの更新時にDiffUtilが発生させる`notify`系のメソッドが競合して、**予期しないリストの更新**が発生するという点です。
6
-
7
- とき**データは正常更新されおり**、View更新に問題が発生います。
5
+ 並び替え方法よっ`RecyclerView`が期待に反した更新になっています。
8
6
 
9
7
 
10
8
 
11
- ###### ItemTouchHelperによトの更新
9
+ ### 並び替えの対象となクラ
12
-
13
- RecyclerViewにItemTouchHelperを適用して、リストアイテムの並び替え処理を実装しています。
14
-
15
- DBとして`Firestore`を採用しており、更新を頻繁にできないので並び替えが終了した段階でDBを更新しています。
16
10
 
17
11
  ```kotlin
18
12
 
19
- class EditTaskItemTouchHelperCallback(
13
+ data class Task(
20
14
 
21
- private val action: ItemCallbackAction
15
+ val id: TaskId = TaskId(),
22
16
 
23
- ) : SimpleCallback(
17
+ val title: TaskTitle,
24
18
 
25
- UP or DOWN,
19
+ val isDone: Boolean = false
26
20
 
27
- 0
21
+ )
28
22
 
29
- ) {
23
+ ```
30
24
 
31
- interface ItemCallbackAction{
32
25
 
26
+
27
+ ### 並び替えの方法
28
+
29
+ メソッドの戻り値としてはどちらの方法をとっても同じ値を取るのですが、
30
+
31
+ 1番の方法をとると`RecyclerView`の更新が正常に行われません。
32
+
33
+
34
+
35
+ 1 `remove()`と`add()`メソッドを利用して更新
36
+
37
+ ```kotlin
38
+
33
- fun onReorder(start: Int, end: Int)
39
+ fun reorder(start: Int, end: Int): List<Task> {
40
+
41
+ if (start < 0 || start > this.tasks.size) throw RuntimeException()
42
+
43
+ if (end < 0 || end > this.tasks.size) throw RuntimeException()
44
+
45
+ if (start == end) throw RuntimeException()
46
+
47
+
48
+
49
+ val reordered = tasks.toMutableList()
50
+
51
+ val target = reordered[start]
52
+
53
+ reordered.removeAt(start)
54
+
55
+ reordered.add(end, target)
56
+
57
+ return reordered
34
58
 
35
59
  }
36
60
 
61
+ ```
37
62
 
63
+ 2 `Collections.swap`を利用して更新
38
64
 
39
- // 並び替えの最初と最後だけを保持
65
+ ```
40
66
 
41
- private var start: Int? = null
67
+ fun reorder(start: Int, end: Int): List<Task> {
42
68
 
69
+ if (start < end) {
70
+
71
+ for (i in start until end) {
72
+
73
+ Collections.swap(this.tasks, i, i + 1)
74
+
75
+ }
76
+
77
+ } else {
78
+
43
- private var end: Int? = null
79
+ for (i in start downTo end + 1) {
80
+
81
+ Collections.swap(this.tasks, i, i - 1)
82
+
83
+ }
84
+
85
+ }
44
86
 
45
87
 
46
88
 
47
- override fun onMove(
89
+ return tasks
48
90
 
49
- recyclerView: RecyclerView,
91
+ }
50
92
 
51
- viewHolder: RecyclerView.ViewHolder,
93
+ ```
52
94
 
53
- target: RecyclerView.ViewHolder
95
+ ### 実際に発生する問題
54
96
 
55
- ): Boolean {
97
+ 並び替えの方法の一番目の方法をとると、以下のように2回更新がかけられてしまいます。
56
98
 
57
- val positionStart = viewHolder.adapterPosition
99
+ ###### 例) 1 -> 2に並び替え
58
100
 
101
+ 1. 初期状態
102
+
103
+ ![初期状態](62e0e101112e1ee5356b324656f89028.png)
104
+
59
- val positionEnd = target.adapterPosition
105
+ 1. onMove内の`notifyItemMoved`による並び替え(期待する状態)
106
+
107
+ ![並び替え](79092f8d6c7031d7ced766937c3cadbf.png)
108
+
109
+ 1. LiveData更新にともなうDiffUtilによる並び替え(再び並び替えが発生)
110
+
111
+ ![LiveData](62e0e101112e1ee5356b324656f89028.png)
60
112
 
61
113
 
62
114
 
63
- start = start ?: positionStart
64
-
65
- end = positionEnd
66
-
67
-
68
-
69
- // (問題となる箇所)
70
-
71
- // リストに更新を通知
72
-
73
- recyclerView.adapter?.notifyItemMoved(positionStart, positionEnd)
74
-
75
-
76
-
77
- return true
78
-
79
- }
80
-
81
-
82
-
83
- override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
84
-
85
- super.clearView(recyclerView, viewHolder)
86
-
87
-
88
-
89
- if (start != null && end != null)
90
-
91
-        // 並び替えが完了後、DBが更新される
92
-
93
- action.onReorder(start!!, end!!)
94
-
95
-
96
-
97
- start = null
98
-
99
- end = null
115
+ ### 関連するコード
100
-
101
- }
102
-
103
-
104
-
105
- override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
106
-
107
-
108
-
109
- }
110
-
111
- }
112
-
113
- ```
114
-
115
- ###### ListAdapter(DiffUtil)によるリストの更新
116
-
117
- リストのデータはDBからのLiveDataに従い、更新がかけられます。
118
116
 
119
117
  ```kotlin
120
118
 
@@ -128,49 +126,7 @@
128
126
 
129
127
  ```
130
128
 
131
- ###### 問題発生
132
129
 
133
- ItemTouchHelperのonMove内で`notifyItemMoved`の処理があるので,並び替えが発生します。
134
-
135
- 並び替えが完了するとDBにデータの更新処理をかけます。
136
-
137
- 次にDBからLiveDataを受け取りAdapter内で再びリストを更新しようとします。
138
-
139
- リストが更新されるとDiffUtilが動作し、内部で`notify`系のメソッドが動作するので、ここでも並び替えが発生します。
140
-
141
-
142
-
143
- 結果、以下のような状況が発生します。
144
-
145
- 例) 1 -> 2に並び替え
146
-
147
- 1. 初期状態
148
-
149
- ![初期状態](62e0e101112e1ee5356b324656f89028.png)
150
-
151
- 1. onMove内の`notifyItemMoved`による並び替え
152
-
153
- ![並び替え](79092f8d6c7031d7ced766937c3cadbf.png)
154
-
155
- 1. LiveData更新にともなうDiffUtilによる並び替え(再び並び替えが発生)
156
-
157
- ![LiveData](62e0e101112e1ee5356b324656f89028.png)
158
-
159
-
160
-
161
- ### 補足
162
-
163
- - 並び替え後、DBからのLiveDataは期待通りに並べ替えられています。
164
-
165
- - DiffUtilを適用せずにRecyclerViewを用いるとこのような状況は発生しません。
166
-
167
- - DBの更新処理をかけなければView自体は正常に並び替えられます。(データを更新することはできませんが...)
168
-
169
-
170
-
171
- ### 他の関連するコード
172
-
173
- RecyclerViewにDiffUtilを適用させた状態で実装しています。
174
130
 
175
131
  ```kotlin
176
132
 
@@ -232,8 +188,6 @@
232
188
 
233
189
  ```
234
190
 
235
- ### していただきたいこと
191
+ ### 答していただきたいこと
236
192
 
237
- DiffUtilの利用ながらも、並び替え時に発生するDB更新処理最低限にできる方法があれば教えていただきたいです。
193
+ なぜ`Collections.swap`を利用し並び替えを行うと、上記のような問題が発生しないを教えていただきたいです。
238
-
239
- 他のデバイスとのデータの同期をしたいので、ページを戻るときにデータを一括更新するなどはしたくないです。