Jetpack Compose1.2.0で追加されたmovableContentOfを使ってみた

今回は、Jetpack Compose1.2.0で追加された`movableContentOf`について解説していきたいと思います。

1.2.0で追加されたのですが、自分は完全に見落としていました。
1.3.0で追加された`LookaheadLayout`と合わせて使うと、
これまでComposeで実現しづらかった、Shared Element Transitionを実現できるみたいです。

本記事では、まず`movableContentOf`について見ていきたいと思います。

## movableContentOfとは何か

`movableContentOf`とは何かというと、公式サイトによれば

https://developer.android.com/jetpack/androidx/releases/compose-runtime?hl=ja#1.2.0
> コンポーザブル ラムダを別のラムダに変換します。変換されたラムダは、状態および対応するノードを呼び出された新しい場所に移動します。前の呼び出しがコンポジションから出ると状態は一時的に保持され、ラムダへの新しい呼び出しがコンポジションに入った場合、状態および関連付けられたノードが新しい呼び出しの場所に移動されます。

とのことです。

Compose関数の状態を保持して、
状況に応じて、関数の呼び出し場所を変えたりできるみたいですね。

これだけだと、どう使うのかイメージつきづらいので、
実際にmovableContentOfを使った実装をもとに、解説します。

## movableContentOfの使い方

写真のように、Checkboxが縦に並んでおり、
Buttonによって、縦並びと横並びを切り替えられる画面を作りたいと思います。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320402/52d0e172-5b76-a5f0-3800-74ca9e3ef8c0.png" width="300"/>

まずは、movableContentOfを使わない方法だと、こうなります。

↓ movableContentOfを使わないパターン

```kotlin
@Composable
fun MovableContentOfSample() {

    MaterialTheme {
        Surface(
            modifier = Modifier.fillMaxSize()
        ) {
            var isVertical: Boolean by remember {
                mutableStateOf(true)
            }

            Column(
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Button(onClick = {
                    isVertical = isVertical.not()
                }) {
                    Text(
                        text = if (isVertical) {
                            "Horizontal"
                        } else {
                            "Vertical"
                        }
                    )
                }
                Spacer(modifier = Modifier.height(8.dp))
                CheckBoxColumnAndRow(isVertical = isVertical) {
                    (1..3).forEach { _ ->
                        var checked: Boolean by remember {
                            mutableStateOf(false)
                        }
                        Checkbox(checked = checked, onCheckedChange = {
                            checked = it
                        })
                    }
                }
            }
        }
    }
}

@Composable
private fun CheckBoxColumnAndRow(
    isVertical: Boolean,
    content: @Composable () -> Unit
) {
    if (isVertical) {
        Column {
            content()
        }
    } else {
        Row {
            content()
        }
    }
}
```

一見できているように見えますが、
Buttonを押すたびに、Checkboxのチェック状態がクリアされてしまいます。
![az_recorder_20221121_153909_edited.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320402/cdc5d153-ce5b-824f-ea51-bc7f573749ec.gif)

これは、isVerticalの値が変わるたびに、
CheckBoxColumnAndRowのcontentで受け取っている、Composableラムダの状態が保持されないためです。

movableContentOfを使うと解決できます。

↓ movableContentOfを使うパターン

```kotlin
@Composable
fun MovableContentOfSample() {

    MaterialTheme {
        Surface(
            modifier = Modifier.fillMaxSize()
        ) {
            var isVertical: Boolean by remember {
                mutableStateOf(true)
            }

            val content = remember {
                movableContentOf {
                    (1..3).forEach { _ ->
                        var checked: Boolean by remember {
                            mutableStateOf(false)
                        }
                        Checkbox(checked = checked, onCheckedChange = {
                            checked = it
                        })
                    }
                }
            }

            Column(
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Button(onClick = {
                    isVertical = isVertical.not()
                }) {
                    Text(
                        text = if (isVertical) {
                            "Horizontal"
                        } else {
                            "Vertical"
                        }
                    )
                }
                Spacer(modifier = Modifier.height(8.dp))
                CheckBoxColumnAndRow(
                    isVertical = isVertical,
                    content = content
                )
            }
        }
    }
}

@Composable
private fun CheckBoxColumnAndRow(
    isVertical: Boolean,
    content: @Composable () -> Unit
) {
    if (isVertical) {
        Column {
            content()
        }
    } else {
        Row {
            content()
        }
    }
}
```

Buttonの押下前後で、Checkboxのチェック状態が保持されているのが分かります。

![az_recorder_20221121_155722_edited.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320402/6e88c308-a9bb-497a-5898-8eb759c0aa8c.gif)

正直、これだけならmovableContentOfを使わずとも、
ViewModelで状態を保持すれば済む話ですが、

movableContentOfは、
例えば一覧画面と詳細画面で、Composableラムダを共有するといった、
画面をまたいだ共有も実現できます。
なので、Shared Elementのような実装を実現できるということですね。


## おわりに
今回は、movableContentOfについて解説しました。
時間があったら、LookaheadLayoutとmovableContentOfを組み合わせた、
Shared Element Transitionについても解説したいと思います。