アプリ開発備忘録

PlayStationMobile、Android、UWPの開発備忘録

【Jetpack Cpmpose】スキップ/起動可能性と@NonRestartableComposable【パフォーマンス最適化】

Composableの状態

Composable関数には3つの状態があります。

State1 State2 State3
skippable
restartable

https://github.com/androidx/androidx/blob/403b6d0032c289ed6e65a1c75f137c07aedd220f/compose/compiler/design/compiler-metrics.md

skippableは引数を比較し、同じであればrecomposeをskipできます。skippableである場合は必ずrestartableです。
restartableはrecomposeのスコープとなる事ができます。restartableであればそのComposable単体でRecomposeできますが、restartableでなければ、更に上位のスコープでRecomposeを行います。
これだけ聞くと、全てskippableにしておけば良いように思えますが、ドキュメントには必ずしもComposableをSkippableにしなければいけないわけではないと記述があります。

@NonRestartableComposable

restartableであり、skippableでない関数は、以下のようにする事を「検討しても良い」とあります。
今回はNonRestartableにする方を掘り下げていきます。

  • Composableの引数をStableにしてskippableにする
  • NonRestartableにする

挙動確認

@NonRestartableComposable があるComposable関数と、無い関数で動作の違いを見ていきます。
Button1とButton2があり、それぞれカウントアップするボタンがあります。

@Composable
public fun Test(
    modifier: Modifier = Modifier,
) {
    var count1 by remember {
        mutableStateOf(0)
    }
    var count2 by remember {
        mutableStateOf(0)
    }
    Column(modifier = modifier) {
        Button(onClick = {
            count1++
        }) {
            Text(text = "Button1")
        }
        Button(onClick = {
            count2++
        }) {
            Text(text = "Button2")
        }

        NonRestartable("1", count1)
        Restartable("1", count1)

        NonRestartable("2", count2)
        Restartable("2", count2)
    }
}

@Composable
@NonRestartableComposable
public fun NonRestartable(tag: String, count: Int) {
    Log.d("LOG", "Restartable: $tag, count=$count")
    Text(text = "$tag: count=$count")
}

@Composable
public fun Restartable(tag: String, count: Int) {
    Log.d("LOG", "Restartable: $tag, count=$count")
    Text(text = "$tag: count=$count")
}

Button1を押したときの出力

Non-Restartable: 1, count=1
Restartable: 1, count=1
Non-Restartable: 2, count=0

text2の方の値は変わっていないはずですし、StringはStableなので、Skipされるはずですが、 @NonRestartableComposable の方は呼ばれています。
挙動としては inline 関数に近いのかなと思っています。

公式で使用されている部分

ドキュメントには、再構成のRootになる可能性が低く、機能が殆どなく、引数を読み取らずに他のComposableな関数に渡す関数に使うと良いと記述してあります。
1.1.0時点では LaunchedEffectSideEffect に使用されています。

Compose1.2.0では他にも使用されている部分が増えています。
※1.2.0-beta2での情報です

Spacer

内部でPainterが引数のImageを呼び出している、引数がImageVectorの方のImage。

Surfaceを呼び出しているだけのCard

@NonRestartableComposableをどう使っていくべきか

これに関してはパフォーマンスに大きく問題が出るという事は無いと思うので、付けなくても、条件を満たしたら付けても良いと思います。ライブラリ開発者以外はそこあまで気にしなくてもいいかなととも思いますが、小さいパーツのちょっとした共通化に使うのがいいかなと思います。

例えば以下のようなコードです。(Painterはunstable)

@Composable
@NonRestartableComposable
private fun Budge(
    modifier: Modifier,
    text: String,
    color: Color,
    icon: Painter, // unstable
    contentDescription: String,
) {
    Icon(
        modifier = modifier
            .size(32.dp)
            .clip(RoundedCornerShape(2.dp))
            .background(color)
            .padding(2.dp),
        painter = icon,
        contentDescription = contentDescription,
        tint = Color.White,
    )
    Spacer(modifier = Modifier.width(8.dp))
    Text(
        text = text,
        color = colorResource(id = R.color.navy_70),
        fontSize = 18.sp,
    )
}

P.S.

この内容で登壇しました。
https://andpad.connpass.com/event/249842/