Jetpack Compose v1.2.0に気になったAPIのまとめ

Jetpack Compose v1.2.0

Design Division 所属アプリエンジニアのスージです。 良いデザインでクライアントのビジネスを前進させるために開発している、Jetpack Compose と KMM が好きなエンジニアです。

Jetpack Compose の バージョン 1.2.0 が rc になりました。フォントのパディングの改善を含め、たくさんのAPIの追加や更新があります。

medium.com

その中で個人的に気になった API を紹介しようと思います。

movableContentOf

movableContentOf のドキュメンテーションを見てみると、以下のように書かれています。

Convert a lambda into one that moves the remembered state and nodes created in a previous call to the new location it is called.

つまり、movableContentOf を使うとある @Composable lambda が remember(保特)され Compositionツリーの中に再コンポジションをせず移動できます。

分かりにくいと思いますので、サンプルを使って調べてみましょう。

まずは、movableContent を使わずコンテンツを isVertical フラグで ColumnRow に切り替えるようにしましょう。

@Composable
fun Screen() {
    ..
    var isVertical by remember { mutableStateOf(false) }
    
    ScreenContent(isVertical = isVertical, ..) {
        ItemA(modifier = Modifier.size(50.dp))
        ItemB(modifier = Modifier.size(50.dp))
    }

    Button(onClick = { isVertical = !isVertical }) {
        Text(text = "Toggle")
    }
}

@Composable
fun ScreenContent(isVertical: Boolean, ..) {
    ..
    if (isVertical) {
        Column {
            content()
        }
    } else {
        Row {
            content()
        }
    }
}

コンポジションや再コンポジションを確認するために ItemAItemBの中 SideEffect にログを取ってます。再コンポジションのデバッグ方法についてこの記事で詳しく書かれているのでぜひ読んでみてください。

www.jetpackcompose.app

@Composable
fun ItemA(..) {
    SideEffect {
        // Log.e("movableContent", "Compose A")
    }
    ..
}

毎回 ColumnRow に切り替えると @Composable の再コンポジション行います。

movableContentOf を使わない場合
movableContentOf を使わない場合

では、movableContentOfを使ってみるとこのようになります。

@Composable
fun ScreenContent(
    isVertical: Boolean,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit,
) {
    val movableContent by remember(content) { movableContentOf(content) } 
    if (isVertical) {
        Column(..) { movableContent() }
    } else {
        Row(..) { movableContent() }
    }
}

今回は movableContentOf を使うと再コンポジションが行いません。

movableContentOf を使わない場合
movableContentOf を使う場合

以前呼び出された場所で @Composable lambda の状態を保特し、再コンポジションせず次呼び出される場所に持っていくので Shared transitions は実現可能になるかもしれないですね。 すでに Zach Klipp 氏はそれを実装した例を共有してくれました。

LazyGrid にも animateItemPlacement 追加

1.1.0 で LazyColumnLazyRow に追加された animateItemPlacement が、1.2.0 で LazyGrid にも追加されてます。

LazyColumnLazyRow と同じ key を定義し grid アイテムに Modifier.animateItemPlacement() を追加します。

LazyVerticalGrid(columns = Fixed(4)..) {
    items(items, key = { it }) {
        Card(modifier = Modifier..
            .animateItemPlacement(animationSpec = tween(durationMillis = 300)),
        ) {..}
    }
}

LazyGrid の animateItemPlacement 実装
LazyGrid の animateItemPlacement 実装

TextStyle に Brush の追加

TextTextStyle にやっと Brush 対応が入りました。グラディエーション色設定できるようなりましたね。

movableContentOf を使わない場合
TextStyle のグラディエーション色 Brush 設定

val transition = rememberInfiniteTransition()

val color1 by transition.animateColor(..)
val color2 by transition.animateColor(..)
val color3 by transition.animateColor(..)
val color4 by transition.animateColor(..)

Text(
    text = "いいね!",
    style = TextStyle(
        brush = Brush.linearGradient(
            colorStops = arrayOf(
                Pair(0f, color1),
                Pair(.3f, color2),
                Pair(.6f, color3),
                Pair(1f, color4),
            ),
            start = Offset.Zero,
            end = Offset.Infinite
            ),
        ),
        ..
    )
)

新しい @Preview デバイス追加

今年、タブレットとフォルダブルのための12Lの発表があると Material Design 3 Adaptive design のガイドラインもちゃんと書かれています。

そして 1.2.0 から @Preview には Devices.DESKTOPDevices.TABLETDevices.FOLDABLE 3つの新しい reference デバイスが追加されてます。

@Preview(device = Devices.TABLET, name = "Tablet")
@Preview(device = Devices.DESKTOP, name = "Desktop")
@Preview(device = Devices.FOLDABLE, name = "Foldable")
@Composable
fun PreviewDevice() {..}

追加された Desktop, Tablet, Foldable @Preview デバイス
追加された Desktop, Tablet, Foldable @Preview デバイス

kotlinx.collections の @Stable 推論

kotlinx.collections の一部が @Stable と推論されるようになってます。

今までは List<T> を読み込んでる @Composable 変数は再コンポションの時もスキップされないですね。つまり、リストされてない時も再コンポジション行われます。

サンプルを見てみましょう。ここではisBlue のみが更新されて list は変わってないですね。でもリストを読み込んでる ListReadingComponent のコンポジションを管理すると ListReadingComponent の再コンポジションが行われてることがわかりますね。

var isBlue by remember { mutableStateOf(false) }
val list = remember { listOf("A", "B", "C", "D") }

Column {
    Row {
        Button(onClick = { isBlue = !isBlue }) {
            Text(text = "Toggle color")
        }

        Box(
            modifier = Modifier
                .size(48.dp)
                .background(
                    color = if (isBlue) Color.Blue else Color.Red,
                    shape = RoundedCornerShape(8.dp)
                )
        )
    }
    
    ListReadingComponent(list = list)
}

@Composable
fun ListReadingComponent(list: List<String>..) {
    SideEffect {
        // Log.e("stableList", "Compose")
    }

    LazyColumn(..) {
        items(list) {
            Text(text = it)
        }
    }
}

List 変更しなくても再コンポジション行う
List 変更しなくても再コンポジション行う

ではListReadingComponent に渡すリストを kotlinx.ImmutableList に変更してみましょう。1.2.0 から ImmutableList@Stable と推論されてますので、更新されてない場合は再コンポジションが行われない。

kotlinx.ImmutableList の場合再コンポジション行わない
kotlinx.ImmutableList の場合再コンポジション行わない

これについてKotlinlang Slackでは「1.2.0 からパフォーマンスを改善するために全部のリストを kotlinx 化して方がいいのでは?」という質問がありました。 Ben Trengrove 氏はこのように回答しました。「全部を kotlinx 化しないと思いますが、リストを読み込んでパフォーマンスが落ちてるかを確認しながら一つずつ回数に kotlinx に切り替えるすると思います。」

LazyLayout

I/O 2022 でも発表がありましたが、LazyRowLazyColumn LazyGrid 以外もカスタムUIのため LazyLayout 追加されてます。

Lazy layouts in Compose セッションと事例のため wear の ScalingLazyColumn を調べてみてください。

www.youtube.com

終わりに

ここで書いてない 1.2.0 のAPI追加と更新盛り沢山です。1.3.0 には多分 Doris Liu 氏が紹介してくれた LookAheadLayout が追加される予定なので Compose にも Shared transitions が実現しやすくなることは楽しみですね!


Goodpatch には、デザインと技術の両方を追求できる環境があります。 少しでもご興味を持ってくださった方、ぜひ一度カジュアルにお話ししましょう!