Jetpack Composeを使ってみたい② 再コンポーズ

Jetpack Composeを使ってみたい!と思ってCodelabを元に勉強してみた記録。
Jetpack Compose  |  Android デベロッパー  |  Android Developers

JetpackComposeの思想より、再コンポーズについてまとめる。
Compose の思想  |  Jetpack Compose  |  Android Developers

再コンポーズの呼び出しとスキップ

カウント数を表示するTextと、クリックでカウントを増やすButtonを配置したCountScreenと、
カウント数を保持し、CountScreenを呼び出すCounterを以下のように記述した。

@Composable
fun Counter(){
    var count by remember { mutableStateOf(0) }

    CountScreen(
        text = count.toString(),
        onClicked = { count ++ }
    )
}

@Composable
fun CountScreen(count:String, onClicked:() -> Unit){
    Column(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = count)
        Button(onClick =  onClicked ) {
            Text(text = "count up")
        }
    }
}

ボタンをクリックするとonClickedが実行されてCounter関数内のcountの値が更新される。
countに変更があったため、CountScreenが再度呼び出される。
CountScreen内ではcountの値を使用しているTextコンポーザブルは再コンポーズされ、
Buttonコンポーザブルはパラメータの更新がないため再コンポーズはスキップされる。

副作用と再コンポーズ

副作用とはアプリの他の部分に反映される変更のことで、
例えば

  • 共有オブジェクトのプロパティへの書き込み
  • ViewModel で監視可能なデータの更新
  • 共有設定の更新

再コンポーズはスキップされたり、アニメーションのレンダリングで何度も実行されたり、
並列実行によりバックグラウンドで実行されることがある。
このとき、コンポーズ可能な関数内に直接状態の変更(副作用)が記述されていると、
想定しない状態の変化が起こることがあるため、コンポーズ可能な関数は副作用を持ってはいけない。

副作用のある例

@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
    var items = 0
    //リストの要素数を数えたい

    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
                items++ 
                //関数内に値の変更があり、再コンポーズによって意図しないタイミングで変更が起こる可能性がある
            }
        }
        Text("Count: $items")
        //この場合myList.sizeで要素数を取得すれば、itemsを用意して副作用を持たせる必要はない
    }
}

副作用を持たせる場合はコールバックにする。

//最初のサンプルコードより
@Composable
fun CountScreen(count:String, onClicked:() -> Unit){
    Column(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = count)
        Button(onClick =  onClicked ) {
            //値の変更はonClickコールバックが持っているため、再コンポーズに依存しない
            Text(text = "count up")
        }
    }
}

任意の順序で実行される

3 つの画面を 1 つのタブレイアウトに描画する次のサンプルコードでは、
コンポーザブル可能な関数StartScreen、MiddleScreen、EndScreenは上から順に実行されるとは限らない。
例えば、StartScreenで値の変更を行い、変更した値をMiddleScreenでも使うというコードを書くと、
StartScreenが先に行われる保証はないので想定した表示にならない可能性がある。
3つのコンポーザブル可能な関数は自己完結している必要がある。

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}


前の記事
mtnmr.hatenablog.com
次の記事
mtnmr.hatenablog.com