Jetpack Compose:再Composeを超えて状態の保持(remenber)

投稿日:  更新日:

状態の保持(remenber)はアプリの画面(UI)を制御・管理するために必要な動作です。

再Composeのスケジューリング(mutableStateOf)に並び、Jetpack Composeの重要な技術の一つです。

今回は「再Composeを超えて状態の保持」について、まとめます。

※環境:Android Studio Giraffe | 2022.3.1 Patch 1
    Kotlin 1.8.10
    Compose Compiler 1.4.3

スポンサーリンク

UI要素の役割

UI要素の役割は、時々刻々と変化する「状態」を表示して、ユーザーへ伝えることです。

「状態」とは、UI要素が主題とするコンテンツや、UI要素の形状・装飾などです。UI要素ごとに、表示すべき状態は異なります。

例えばTextは図のような状態を持ちます。※状態の一部を紹介

@Preview(showBackground = true)
@Composable
fun ClockPreview() {
    MyApplicationTheme {
        val _hour = 10              // 状態(コンテンツ)
        val _min  = 15
        val _sec  = 22
        val _color = Color.Red      // 状態(文字色)
        val _fontSize = 24.sp       // 状態(文字サイズ)
        ClockPanel(
            hour = _hour, min = _min, sec = _sec,
            color = _color, fontSize = _fontSize
        )
    }
}

@Composable
fun ClockPanel(
    hour: Int, min: Int, sec: Int,
    color: Color, fontSize: TextUnit
) {
    Text(
        text = "%02d:%02d:%02d".format(hour, min, sec),
        color = color, fontSize = fontSize
    )
}

Textが持つ状態

スポンサーリンク

UI要素は関数

UI要素はComposable関数で表現されます。そして、UI要素が子UI要素を呼び出し、ツリー構造を作ることで、アプリの画面が構成されます。

UIツリー

Composable関数はKotlinの一般的な関数です。

関数は実行の枠を超えて、変数にデータを保持することが出来ません。関数内で宣言した変数の有効範囲は関数の中のみであり、有効期間は実行中のみです。実行の終了とともに、変数は破棄されます。

従って、UI要素が状態を表示するには、Composable関数が実行される毎に、状態を入力しなければなりません。なぜなら、状態を変数へ保持できないからです。入力は引数で行われます。

UI要素で構成されるUIツリー(最上位は関数)も同様です。

UI要素は関数

スポンサーリンク

状態の保持

UI要素の数が増えて複雑なUIツリーになると、それに合わせて状態の数も多くなり、管理が難しくなります。

状態の分離

rememberは「Composable関数内の変数へ状態を疑似的に保持する仕組み」です。

これにより、状態の一部をUIツリー内で制御・管理できます。

スポンサーリンク

rememberの動作

rememberの動作を、次のようなサンプルを用いて説明します。

@Composable
fun Sample(...) {
    var value = remember { 100 }		// Int型の状態を保存
	//
	// valueを使って表示を行う
	//
}

初回Compose

rememberが実行される前は、Compositionに何もありません。

 Compose実行前  初回Composeの前

rememberが実行されると、Compositionに指定されたおオブジェクト(Int)を保存します。初期値はラムダ式の結果(最後の評価値)です。

そして、その参照をComposable関数内の変数(value)へ代入します。

 Compose実行後  初回Composeの後

以降、この変数(value)を通して、オブジェクト(Int)を参照できます。

再Compose

rememberが実行される前は、Compositionに初回で保存されたオブジェクトが残っています。

 Compose実行前  再Composeの前

rememberが実行されると、Compositionのオブジェクト(Int)の参照をComposable関数内の変数(value)へ代入します。

 Compose実行後  再Composeの後

以降、この変数(value)を通して、オブジェクト(Int)を参照できます。

再Composeを超えて

rememberの動作で、もう一つ重要なポイントがあります。

単純に状態を保持するだけではありません。

rememberが実現する「状態を疑似的に保持する仕組み」は、再Composeを超えて状態を保持できます。

NGケースとOKケース

基本データ型の状態を保持する場合は、注意が必要です。

※サンプルは定期的な再Composeを行っています。定期的な再Composeについては「Jetpack Compose:mutableStateOfはComposable関数ではない」を参照

NGケース(プリミティブ型)

基本データ型の状態を保持する例です。変数counterへ保持対象のInt型オブジェクト(参照値)が代入されます。

        var _compose = mutableStateOf(0)

        setContent {
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Column {
                        var _counter = remember { 0 }			// counterを保持
                        Text(text = "Compose count = %3d".format(_compose.value))
                        Text(text = "Click count = %3d".format(_counter))
                        Button(onClick = { _counter++ }) {		// counterを更新
                            Text(text = "Click")
                        }
                    }
                }
            }
        }

        // 別スレッドで状態を更新
        lifecycleScope.launch(Dispatchers.Default) {
            repeat(100) { delay(5000); _compose.value++ }
        }

クリックタイミングの例

NGケースの結果

この記述は、図にあるようなクリック操作を行ったとしても、表示が変化しません。「Click count = 0」のままです。つまり、状態が期待通りに保持できていないことを意味します。

原因は保持対象が基本データ型だからです。

基本データ型は関数内で値を更新(例はインクリメント)する際に、演算をJVM上で行います。ですので、Compositionのオブジェクトへ更新後の値は反映されません。

プリミティブ型ークリック時の動作

再ComposeはCompositionのオブジェクトの参照値を変数counterへ代入します。

Compositionのオブジェクトは最初に保存された値のままです。

プリミティブ型ー再Compose時の動作

以上により、表示が変化しません。

※基本データ型の演算の様子については「Kotlin:基本データ型はオブジェクト」を参照

OKケース(データクラス型)

データクラス型のオブジェクトを保持する例です。変数counterへ保持対象のDataCounter型オブジェクト(参照値)が代入されます。

        var _compose = mutableStateOf(0)

        setContent {
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Column {
                        val _counter = remember { DataCounter(0) }
                        Text(text = "Compose count = %3d".format(_compose.value))
                        Text(text = "Click count = %3d".format(_counter.value))
                        Button(onClick = { _counter.value++ }) {
                            Text(text = "Click")
                        }
                    }
                }
            }
        }

        // 別スレッドでアプリの状態を更新
        lifecycleScope.launch(Dispatchers.Default) {
            repeat(100) { delay(5000); _compose.value++ }
        }
data class DataCounter(var value: Int)

OKケースの結果

この記述は、図にあるようなクリック操作を行うことにより、表示が変化します。結果は「Click count = 3」になりました。つまり、状態が期待通りに保持できていることを意味します。

データクラス型は値を更新(例はインクリメント)する際に、内部のプロパティが更新されます。ですので、Compositionのオブジェクトへ更新後の値は反映されます。

DataCounter型ークリック時の動作

再ComposeはCompositionのオブジェクトの参照値を変数counterへ代入します。

Compositionのオブジェクトは内部のプロパティが更新されたものです。

DataCounter型ー再Compose時の動作

以上により、表示が変化します。

スポンサーリンク

実用的な使い方

rememberの最も実用的な使い方は、再Composeのスケジューリングを行うmutableStateOfと合わせて使うことです。

        var _compose = mutableStateOf(0)

        setContent {
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Column {
                        val _counter = remember { mutableStateOf(0) }
                        Text(text = "Compose count = %3d".format(_compose.value))
                        Text(text = "Click count = %3d".format(_counter.value))
                        Button(onClick = { _counter.value++ }) {
                            Text(text = "Click")
                        }
                    }
                }
            }
        }

        // 別スレッドでアプリの状態を更新
        lifecycleScope.launch(Dispatchers.Default) {
            repeat(100) { delay(5000); _compose.value++ }
        }

mutableStateOfの働きにより、状態の更新に合わせて直ちに再Composeが行われ、表示が更新できます。

先に挙げたOKケースのようにデータクラス型を用いる必要はありません。同様な役割をmutableStateOfが返すMutableState型が担ってくれます。

※mutableStateOfの働きは「再Composeのスケジューリング(mutableStateOf)」を参照

スポンサーリンク

関連記事:

プロジェクトのビルドで「Something went wrong while checking for version compatibility between the Compose Compiler and the Kotlin Compiler.」とメッセージを吐き、エラーになる場合があります。 既存のプロジェクトを新しくリリースされたAndroid Studioでビルドした場合に頻発します。 先日、「Giraffe|2022.3.1」がリリース(2023.07)されて、早速、ビルドをしたら発生しました。 その対処方法を説明します。 ※環境:Android Studio Giraffe | 2022.3.1 ...
表示の変わらないUI要素(Composable関数)に対して行われる再Composeは無駄な処理です。 ですので、極力排除したいところですが、表示が変わらないため、画面上からの判断が難しくなっています。 このようなとき、Layout Inspectorを利用すると、無駄な再Composeを見つけ出すことが出来ます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1 ...
Jetpack ComposeはAndroidシステムの新たなUIフレームワークです。従来のViewシステムと、アプリ画面の描画の仕組みが異なります。 このJetpack Composeによるアプリ画面の描画について、仕組みの大枠をまとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
Jetpack Composeは描画処理の軽量化(消費リソース量の削減)をするために、表示の変更されたUI要素のみを再Composeし、表示の変わらないUI要素をスキップします。これにより、高い表示パフォーマンスを維持しています。 しかし、スキップが正常に行われないとしても、アプリの画面に現れて来ません。なぜなら、同じ表示を無駄に繰り返すことになるからです。 アプリは動くけれど動作が鈍いならば、真っ先に疑うポイントです。不要な再Composeが行われている可能性が考えられます。 これは気付かないうちに蓄積し易い不具合です。ですので、再Composeとスキップについて理解し、予防に努めることをお勧めします。 今回は「再Composeとスキップ」について、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
サンプルアプリを作成して、描画処理(再Compose)の周期を観測してみました。 その結果を紹介します。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
mutableStateOfはComposable関数ではありません。 ですので、Composable関数内にある必要はなく、どこでも記述できます。 Activityから表示の更新を発行する方法として使えそうです。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
再Composeのスケジューリング(mutableStateOf)はアプリのパフォーマンスに直結する動作です。 状態の保持(remember)に並び、Jetpack Composeの重要な技術の一つです。 今回は「再Composeのスケジューリング」について、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
CompositionLocalはUIツリーで発生する状態のバケツリレーを解決してくれます。 また、広く共有したい状態の定義にも適しています。例えば、「ツリーのある階層以下に対して」などと言った場合です。 CompositionLocalについて、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
CompositionLocalはUIツリーにローカルな変数を確保します。 その変数の参照キーはcompositionLocalOf関数(以降、Dynamic側と呼ぶ)によって返されるCompositionLocalインスタンスです。 このインスタンスを返す方法に、もう一つ、staticCompositionLocalOf関数(以降、Static側と呼ぶ)があります。 この両者の違いをまとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
CompositionLocalはシステムにより提供されているものがあります。 プログラミングに有益で利用頻度の高いものが用意されています。例えば、LocalContextやLocalConfigurationなどです。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
「Android Studio Giraffe」の作成するプロジェクトは、Jetpack Composeの利用が推奨されます。 今後、Viewシステムに代わり、Jetpack Composeが主流になるようです。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
「Android Studio Giraffe」の作成するプロジェクトは、Jetpack Composeの利用が推奨されます。 そして、作成されたプロジェクトは、Material Designeに準拠したテーマが指定されます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
Android StudioにおけるJetpack Composeプロジェクトは、エディタ上でUIのプレビューが行えます。 Kotlinで記述した画面構成(UIツリー)が視覚的に確認できるので、とても便利です。 さらに、色々な表示条件の設定が行えるので、使いこなせば更に利便性が向上します。 ※この記事の執筆中にドキュメント「コンポーザブルのプレビューで UI をプレビューする」を見つけました。記事はこのドキュメントと重複する部分が多いです。ドキュメントも参考にして下さい。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 3     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
Jetpack Composeが提供する既存のUI要素(Compose UI)は、必ずModifierを引数に持ちます。 このModifierの役割はUI要素へ装飾や機能拡張を追加することですが、裏でアプリ画面の描画処理と密接に関連しており、UI要素よりもシステム側に近い存在です。 理解せずに誤った使い方をすれば、装飾や機能拡張の域を脱してUI要素が表示されないこともあり、思ったようなアプリ画面は望めません。 Modifierについて、まとめます。 ※環境:Android Studio Hedgehog | 2023.1.1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
スポンサーリンク