【Android】 Jetpack Composeでドラッグ&ドロップの並び替えを実現する 〜ライブラリに頼って〜


この記事は、and factory.inc Advent Calendar 20215日目 の記事です。
昨日は @y-okudera さんの「 データの暗号化・復号とSwiftでの実装の仕方について改めて調べてみた 」でした!

ドラッグ&ドロップで並び替えしたい

リスト表示において、アイテムを長押しするとドラッグ&ドロップで並び替えができる、というのは結構あるあるな機能ではないでしょうか?

RecyclerViewではItemTouchHelperを使うことで、ドラッグ&ドロップでの並び替えを実装することができました。

しかしながら、Jetpack Composeにおいては、現状ドラッグ&ドロップはサポートされておりません。
一応ロードマップには「ドラッグ&ドロップのサポート」が記載されているので、今後公式にサポートされるものと思われます。

「公式にサポートされるまで待てないよ…!」という欲求に駆られたため、今回はライブラリの力に頼って ドラッグ&ドロップでの並び替えを実現してみたいと思います。

aclassen/ComposeReorderable

LazyListLazyColumnLazyRow )のドラッグ&ドロップでの並び替えを可能にするライブラリです。

こちらのライブラリの使い方を説明していきたいと思います。

Before

シンプルな LazyColumn を使った画面に、 ComposeReorderable を用いたドラッグ&ドロップでの並び替えを追加していくことを想定します。

対応前のコード↓

    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Sample") })
        }
    ) {
        val data = remember {
            mutableStateOf(List(100) { "$it" })
        }

        LazyColumn {
            items(data.value) { item ->
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .clickable {}
                        .padding(40.dp),
                ) {
                    Text(item)
                }
                Divider(thickness = 1.dp)
            }
        }
    }

画面としては↓のような感じです。

1. dependenciesへの追加

dependencies に追加します。記事作成時では 0.7.4 が最新版でした。

+    implementation "org.burnoutcrew.composereorderable:reorderable:0.7.4"

2. LazyColumnへ設定追加

rememberReorderState() を呼び出し ReorderableState を作成します。

② 作成した ReorderableStatelistState を、 LazyColumnstate に指定します。

ComposeReorderable の拡張である reorderable Modifierも設定します。

-        LazyColumn {
+        val reorderState = rememberReorderState() // ①
+
+        LazyColumn(
+            state = reorderState.listState, // ②
+            modifier = Modifier // ③
+                .reorderable(
+                    state = reorderState,
+                    onMove = { from, to -> /* TODO */ },
+                ),
+        ) {

3. リストのアイテムへの設定追加

リストの各アイテムの Modifier へ設定を追加します。

① 並び替え可能なアイテムであることを示すために、detectReorder(state) or detectReorderAfterLongPress(state) を指定します。
detectReorderAfterLongPress(state) の場合は、長押しで並び替えが起きるようになるので、今回はこちらを指定します。

draggedItem の指定も追加します。
引数の offset には reorderState.offsetByKey or reorderState.offsetByIndex を使って指定します。

         ) {
-            items(data.value) { item ->
+            itemsIndexed(data.value) { index, item ->
                 Box(
                     modifier = Modifier
                         .fillMaxWidth()
                         .clickable { /**/ }
+                        .detectReorderAfterLongPress(reorderState) // ①
+                        .draggedItem(reorderState.offsetByIndex(index)) // ②
                         .padding(40.dp),
                 ) {
                     Text(item)

一応この時点で、ドラッグ&ドロップは動くようになっています…!
ただ、データ自体が変わっていないため、ドラッグしているアイテムがおかしなことになっているので、2で /* TODO */ にしていた onMove の処理を実装します。

4. データの更新実装

ドラッグ&ドロップでアイテムを動かし、他のアイテムと切り替わるタイミングで onMove が呼ばれるので、そのタイミングでデータの更新をしてあげる必要があります。

moveComposeReorderable に入っている拡張関数です。

-          onMove = { from, to -> },
+          onMove = { from, to ->
+              data.value = data.value.toMutableList().apply { move(from.index, to.index) }
+          },

以上の工程で、ドラッグ&ドロップによる並び替えを実装することができました🎉

After

Beforeの対比としてAfterのコードも載せておきます。

    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Sample") })
        }
    ) {
        val data = remember {
            mutableStateOf(List(100) { "$it" })
        }

        val reorderState = rememberReorderState()

        LazyColumn(
            state = reorderState.listState,
            modifier = Modifier
                .reorderable(
                    state = reorderState,
                    onMove = { from, to ->
                        data.value = data.value.toMutableList().apply { move(from.index, to.index) }
                    },
                ),
        ) {
            itemsIndexed(data.value) { index, item ->
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .clickable {}
                        .detectReorder(reorderState)
                        .draggedItem(reorderState.offsetByIndex(index))
                        .padding(40.dp),
                ) {
                    Text(item)
                }
                Divider(thickness = 1.dp)
            }
        }
    }

終わりに

(ただのライブラリの使い方の説明記事になってしまいましたが…)
Jetpack Composeでドラッグ&ドロップの並び替えを実現することができました。
「公式にサポートされるまで待てないよ…!」という方の参考になれば幸いです。