おすすめの方法を実践する

Compose でよくある落とし穴が発生することがあります。これらの間違いにより UI パフォーマンスに悪影響を及ぼす可能性があります。フォローのベスト プラクティス ベスト プラクティスをいくつか紹介します。

remember を使用して高コストの計算を最小限に抑える

コンポーズ可能な関数は、すべてのフレームと同じ頻度で非常に頻繁に実行される可能性がある 追加します。このため、コンポーザブルの本文では計算量を可能な限り低減する必要があります。

重要な手法の 1 つは、計算結果を保存することです。 remember。こうすることで、計算が 1 回実行され、 結果を返すことができます

たとえば、以下のコードでは、並べ替えた名前リストが表示されますが、 並べ替えにはコストがかかる:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // DON’T DO THIS
        items(contacts.sortedWith(comparator)) { contact ->
            // ...
        }
    }
}

ContactsList が再コンポーズされるたびに、連絡先リスト全体が並べ替えられます リストが変更されていなくても同じですユーザーがリストをスクロールすると 新しい行が登場するたびに、コンポーザブルが再コンポーズされます。

この問題を解決するには、LazyColumn の外部でリストを並べ替えて、並べ替えられたリストを remember に保存します。

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    val sortedContacts = remember(contacts, comparator) {
        contacts.sortedWith(comparator)
    }

    LazyColumn(modifier) {
        items(sortedContacts) {
            // ...
        }
    }
}

これで、ContactList が最初に作成されたときに、リストが 1 回並べ替えられるようになります。連絡先またはコンパレータが変更されると、並べ替えられたリストが再生成されます。それ以外の場合は、コンポーザブルはキャッシュに保存されている並べ替えられたリストを引き続き使用できます。

遅延レイアウトキーを使用する

Lazy レイアウト: アイテムを効率的に再利用し、アイテムの再生成や再コンポーズのみを行います。 できます。ただし、Lazy レイアウトを最適化するために 必要があります。

ユーザー オペレーションによってアイテムがリスト内に移動するとします。たとえば 変更日時が最も多いメモのリストを表示するとします。 最近変更されたメモが一番上に表示されます。

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes
        ) { note ->
            NoteRow(note)
        }
    }
}

ただし、このコードには問題があります。下のメモが変更されたとします。 これは直近で変更されたメモなのでリストの一番上に移動され、他のすべてのメモは 1 つ下に移動されます。

ユーザーのサポートがなければ、Compose は変更されていないアイテムが単に リスト内で移動しました。代わりに Compose は古い「アイテム 2」を削除され、 アイテム 3、アイテム 4、およびその下位レベルについて、その結果、 Compose はリストのアイテムのうち 1 つしかなくても、リストのすべてのアイテムを再コンポーズする 確認します。

この場合の解決策は、アイテムキーを指定することです。鍵を HSM に 各アイテムにより、Compose は不要な再コンポーズを回避できます。この場合 Compose には スポット 3 の商品アイテムは以前スポット 2 にあった商品アイテムと同じであると判断できます。 そのアイテムのデータは変更されていないため、Compose は 再コンポーズします

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes,
            key = { note ->
                // Return a stable, unique key for the note
                note.id
            }
        ) { note ->
            NoteRow(note)
        }
    }
}

derivedStateOf を使用して再コンポーズを制限する

コンポジションで状態を使用するリスクの一つは、状態が UI が必要以上に再コンポーズされる可能性があります。たとえば スクロール可能なリストを表示しているとしますリストの状態を調べて リスト内の最初に表示されるアイテムはどれか:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

ここでの問題は、ユーザーがリストをスクロールすると、ユーザーが指をドラッグするのに伴って listState が変化し続けることです。つまり、リストが継続的に再コンポーズされます。ただし、実際にはそれほど頻繁に再コンポーズする必要はなく、 新しいアイテムが下部に表示されるまで再コンポーズする必要はありません。これにより、計算量が過剰に増加し、UI のパフォーマンスが低下します。

この問題を解決するには、派生状態を使用します。派生状態を使用して Compose に伝えられる どの状態変化が実際に再コンポーズをトリガーするかを指定します。この例では 最初に表示されるアイテムが変更されたときに重視する点を指定します。そのとき 状態値が変化したら UI を再コンポーズする必要がありますが、 新しいアイテムを一番上までスクロールすると、再コンポーズの必要がなくなります。

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

可能な限り読み取りを延期する

パフォーマンスの問題を特定したら、状態の読み取りを延期することが効果的です。状態の読み取りを延期すると、Compose は再コンポーズ時に可能な限り少ないコードを再実行するようになります。たとえば、コンポーザブル ツリーの高い位置にホイストされる状態が UI にあり、子コンポーザブルで状態を読み取っている場合は、読み取った状態をラムダ関数でラップできます。そうすれば、実際に必要なときにだけ読み取りが発生します。参考として、Jetsnack サンプルアプリJetsnack で折りたたみツールバーのような効果を実装する 表示されます。この手法が機能する理由については、 Jetpack Compose: 再コンポーズのデバッグ

このエフェクトを実現するには、Title コンポーザブルにスクロール オフセットが必要です。 Modifier を使用して自身をオフセットします。こちらは、Vertex AI Pipelines の 最適化を行う前の Jetsnack コード:

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack, scroll.value)
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scroll: Int) {
    // ...
    val offset = with(LocalDensity.current) { scroll.toDp() }

    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

スクロール状態が変わると、Compose は最も近い親を無効にします 再コンポーズのスコープを指定しますこの場合、最も近いスコープは SnackDetail です。 作成します。Box はインライン関数であるため、再コンポーズではありません。 あります。Compose は SnackDetail と内部のコンポーザブルを再コンポーズします。 SnackDetail。実際に実行した状態のみを読み取るようにコードを変更すると、 再コンポーズする必要がある要素の数を減らすことができます。

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack) { scroll.value }
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    val offset = with(LocalDensity.current) { scrollProvider().toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

スクロール パラメータはラムダになりました。つまり、Title はホイストされた状態を引き続き参照できますが、値は実際に必要とされる Title 内でのみ読み取られます。その結果、スクロール値が変化すると、最も近い再コンポーズ スコープが Title コンポーザブルとなり、Compose が Box 全体を再コンポーズする必要はなくなりました。

これは有益な改善ですが、さらに改善していただくことが可能です。コンポーザブルを再レイアウトまたは再描画するためだけに再コンポーズを実施している場合は、注意が必要です。この場合は、Title コンポーザブルのオフセットを変更することのみが必要であり、この操作はレイアウト フェーズで行うことができます。

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    Column(
        modifier = Modifier
            .offset { IntOffset(x = 0, y = scrollProvider()) }
    ) {
        // ...
    }
}

以前のコードでは Modifier.offset(x: Dp, y: Dp) を使用しており、 offset をパラメータとして渡します。修飾子のラムダ バージョンに切り替えると、次のようになります。 関数がレイアウト フェーズでスクロール状態を読み取るようにできます。その結果、スクロール状態が変更されると、Compose はコンポジション フェーズを完全にスキップして、レイアウト フェーズに直接進むことができます。頻繁に変更される State 変数を修飾子に渡す場合は、可能な限りラムダ バージョンの修飾子を使用する必要があります。

以下に示すのは、このアプローチのもう一つの例です。このコードはまだ最適化されていません。

// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

このボックスの背景色は 2 つの色の間で急速に切り替わります。そのため、この状態は非常に頻繁に変化します。コンポーザブルは、この状態をバックグラウンド修飾子で読み取ります。その結果、すべてのフレームごとに色が変化するため、すべてのフレームに対してボックスを再コンポーズする必要があります。

これを改善するには、ラムダベースの修飾子(この場合は drawBehind)を使用します。 つまり、色の状態は描画フェーズでのみ読み取られます。その結果 Compose は、コンポジション フェーズとレイアウト フェーズを完全にスキップできます。 変更があれば、Compose はすぐに描画フェーズに進みます。

val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
    Modifier
        .fillMaxSize()
        .drawBehind {
            drawRect(color)
        }
)

逆方向書き込みを回避する

Compose には、すでに読み取られた状態に書き込むことはないという重要な前提があります。このような書き込みは逆方向書き込みと呼ばれますが、これを行うとフレームごとに無限に再コンポーズが行われる可能性があります。

次のコンポーザブルは、このような誤りの例を示しています。

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

    // Causes recomposition on click
    Button(onClick = { count++ }, Modifier.wrapContentSize()) {
        Text("Recompose")
    }

    Text("$count")
    count++ // Backwards write, writing to state after it has been read</b>
}

このコードは、 使用します。このコードを実行すると、 再コンポーズが発生すると、カウンタが急激に増加します。 Compose がこのコンポーズ可能な関数を再コンポーズすると、無限ループに 別の再コンポーズをスケジュール設定します。

Composition で状態に書き込まないようにすることで、逆方向書き込みを完全に回避できます。可能な限り、前述の onClick の例のように、常にイベントに応答してラムダで状態に書き込みます。

参考情報