0Pricing
Android Academy · レッスン

再コンポジションを制御する

安定したパラメーターで再コンポジションを減らします。

「再コンポジションを制御する」はCoddyKit上の無料Android Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAndroid Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Android Academyコースには全4レッスンが含まれています。

再コンポジション:Composeのパフォーマンスを左右するつまみ

Jetpack Composeでは、UIを関数で記述します。状態が変化すると、Composeは再コンポジションを行います。つまり、その状態を読み取るコンポーザブルを再実行して、画面を更新します。

適切な範囲に限定されていれば、再コンポジションは正常でコストも低い処理です。しかし、頻繁すぎる、またはツリーの広い範囲にわたって発生すると、Composeのjankの最大の原因になります。このレッスンでは、再コンポジションを小さく、まれに保つ方法を学びます。

再コンポジション回数の確認

修正する前に測定しましょう。Android StudioのLayout Inspectorでは、コンポーザブルごとの再コンポジション回数をリアルタイムで確認できます。Composeコンパイラーのメトリクスや、簡単なデバッグ用カウンターを使うこともできます。

簡単な方法として、SideEffectで参照値をインクリメントすると、コンポーザブルが再コンポジションされるたびに、開発中に意外な動作をログに記録できます。

@Composable
fun RecompositionCounter(tag: String) {
    val count = remember { mutableStateOf(0) }
    SideEffect { count.value++ }
    Log.d("Recompose", "$tag recomposed ${count.value} times")
}

// Drop RecompositionCounter("PriceLabel") inside a composable
// to watch how often it re-runs while you interact.

可能な限り遅く状態を読み取る

Composeは、状態の値を読み取るコンポーザブルだけを再コンポジションします。親が状態を読み取ると親全体が再コンポジションされますが、小さな子だけが読み取る場合は、その子だけが再コンポジションされます。

そのため、状態の読み取りをツリーの下位に移してください。ここでは、悪い例ではティックごとにColumn全体が再コンポジションされ、良い例ではラベルだけに限定されています。

// BAD: Column reads `seconds`, so everything recomposes each second
@Composable
fun TimerBad(seconds: Int) {
    Column {
        ExpensiveHeader()
        Text("Elapsed: $seconds")
    }
}

// GOOD: only the Text reads the value via a lambda
@Composable
fun TimerGood(seconds: () -> Int) {
    Column {
        ExpensiveHeader()
        Text("Elapsed: ${seconds()}")
    }
}

ラムダで読み取りを遅延する

強力なパターンの1つは、頻繁に変化する値を渡す代わりに、その値を返すラムダを渡すことです。最終的にラムダを呼び出すコンポーザブルだけが再コンポジションされます。

これがModifier.offset { ... }やgraphicsLayer { ... }がラムダを受け取る理由です。スクロールやアニメーションの値は毎フレーム変化しますが、ラムダによってレイアウトフェーズから再コンポジションを完全に排除できます。

// Passing the value: parent recomposes every frame of scroll
Box(Modifier.offset(y = scrollOffset.dp))

// Passing a lambda: skips recomposition, updates in the layout phase
Box(Modifier.offset { IntOffset(x = 0, y = scrollOffset.roundToInt()) })

// Same idea for alpha/scale during animation:
Image(
    painter = painter,
    contentDescription = null,
    modifier = Modifier.graphicsLayer { alpha = animatedAlpha() }
)

安定性:Composeがスキップする理由

すべてのパラメーターが安定していて変更されていない場合、Composeはコンポーザブルの再コンポジションをスキップできます。Composeがequalsによって実際の変更を判断でき、公開フィールドが気付かないうちに変更されないと信頼できる場合、その型は安定しています。

  • 安定:プリミティブ型、String、不変のデータクラス、Stateです。
  • 不安定:List/Mapインターフェース、varフィールドを持つクラス、コンパイラーが適用されていないモジュールの型です。

不安定なパラメーターがあると、何も変わっていなくても再コンポジションが強制されます。

// UNSTABLE: List is an interface; Compose cannot assume immutability,
// so UserList recomposes even if the contents are identical.
@Composable
fun UserList(users: List<User>) { /* ... */ }

data class User(val id: Long, val name: String) // stable: all vals

パラメーターを安定させる

不安定なパラメーターを修正する一般的な方法は2つあります。

  • kotlinx.collections.immutableの不変コレクション(例:ImmutableList)を使用します。Composeはこれらを安定したものとして扱います。
  • 自分で管理しているクラスに@Immutableまたは@Stableを付け、変更されないことをComposeに保証します。

これで、同じインスタンスが再び渡されたとき、Composeは安全に再コンポジションをスキップできます。

import kotlinx.collections.immutable.ImmutableList
import androidx.compose.runtime.Immutable

@Immutable
data class UiState(
    val title: String,
    val users: ImmutableList<User>
)

// Stable parameter -> Compose can skip this when state is unchanged
@Composable
fun UserList(users: ImmutableList<User>) { /* ... */ }

remember:毎フレーム再計算しない

コンポーザブルは何度も実行されることがあります。本文に直接書いた複雑な計算は、再コンポジションのたびに実行されます。rememberで囲むと、キーが変化したときだけ再計算されます。

他の状態から導出される値には、derivedStateOfを優先してください。計算結果が実際に変化したときだけ再通知されます。

// Recomputes the sorted list only when `items` changes
val sorted = remember(items) { items.sortedBy { it.name } }

// derivedStateOf: only triggers readers when the BOOLEAN flips,
// not on every scroll pixel
val showButton by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
if (showButton) ScrollToTopButton()

Lazyリストで安定したキーを使う

LazyColumn/LazyRowでは、各アイテムに安定したキーを指定してください。キーがないと、Composeはアイテムを位置で追跡するため、挿入や並べ替えの際に、実際には変化していないアイテムまで再コンポジションと再測定を行います。

安定したキーがあれば、Composeは更新前後でアイテムを対応付け、変更されていないアイテムをスキップできます。

LazyColumn {
    items(
        items = users,
        key = { user -> user.id }   // stable identity
    ) { user ->
        UserRow(user)
    }
}
// Now adding a user at the top reuses existing rows
// instead of recomposing the whole list.

状態をホイスティングし、イベントを上に渡す

不安定なラムダパラメーターも、再コンポジションのたびに新しいラムダインスタンスが作られると、スキップを妨げることがあります。ラムダをrememberするか、安定した関数を参照してコールバックを安定させてください。

状態のホイスティング(状態は呼び出し元に置き、イベントは上方向に流す)と組み合わせることで、リーフコンポーザブルを低コストで、スキップ可能な状態に保てます。

@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
    TextField(value = query, onValueChange = onQueryChange)
}

// In the caller, a remembered lambda keeps the reference stable:
val onChange = remember { { newValue: String -> viewModel.setQuery(newValue) } }
SearchBar(query = query, onQueryChange = onChange)

スクロールやアニメーションの状態を高い階層で読み取らない

よくある間違いは、変化の速い状態(スクロール位置やアニメーションの進行度)を高い階層のコンポーザブルで読み取ることです。これにより、サブツリー全体が毎フレーム再コンポジションされます。

このような読み取りはModifierのラムダ(offset {}、graphicsLayer {}、drawBehind {})の内部に置き、再コンポジションではなくレイアウトフェーズまたは描画フェーズで処理されるようにしてください。これが、滑らかな60fpsのスクロールとカクつくスクロールの違いです。

// Animated color used only for drawing -> stay in the draw phase
Box(
    Modifier.drawBehind {
        drawRect(color = animatedColor())  // lambda read, no recomposition
    }
)

再コンポジションのチェックリスト

操作中に画面がカクつくと感じたら、次の項目を確認してください。

  • パラメーターは安定していますか(不変データ、ImmutableList)?
  • 変化の速い状態を、できるだけ低い階層で、理想的にはモディファイアのラムダ内で読み取っていますか?
  • 遅延リストに安定したキーがありますか?
  • 高コストな計算をremember / derivedStateOfの背後に置いていますか?
  • Layout Inspectorで、回数が実際に減ったことを確認しましたか?

各項目をチェックするたびに、毎フレームの無駄な処理が取り除かれます。

クイックチェック

List<User>をコンポーザブルに渡していて、データが変わっていないのに再コンポジションされることに気付きました。最も直接的な修正方法は何でしょうか。

まとめ:より小さく、よりまれな再コンポジション

Composeで最大のパフォーマンス問題となる再コンポジションを制御する方法を学びました。

  • Layout Inspectorまたはデバッグ用カウンターで回数を測定します。
  • 状態はできるだけ低い階層で読み取り、ラムダで読み取りを遅延します。
  • 不変データと@Immutable/ImmutableListでパラメーターを安定させます。
  • rememberとderivedStateOfを使い、毎フレームの再計算を避けます。
  • 遅延アイテムには安定したキーを指定し、変化の速い状態の読み取りをモディファイアのラムダ内に置きます。

次はCPUと再コンポジションからメモリへ移り、リークの発見と修正を扱います。

よくある質問

「再コンポジションを制御する」レッスンは無料ですか?

はい。「再コンポジションを制御する」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Android Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Android Academyコースには全4レッスンが含まれています。

「再コンポジションを制御する」で何を学びますか?

安定したパラメーターで再コンポジションを減らします。 ブラウザで直接実行するハンズオンコードでAndroid Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Android Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAndroid Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「再コンポジションを制御する」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAndroid Academyレッスンでコードを書いて実行できますか?

はい。すべてのAndroid Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. パフォーマンスを測定する
  2. 再コンポジションを制御する
  3. メモリリークと修正方法
  4. 起動処理とBaseline Profiles
← Android Academyに戻る