Jetpack Compose:UIツリーにローカルな変数の確保(CompositionLocal)

投稿日:  更新日:

CompositionLocalはUIツリーで発生する状態のバケツリレーを解決してくれます。

また、広く共有したい状態の定義にも適しています。例えば、「ツリーのある階層以下に対して」などと言った場合です。

CompositionLocalについて、まとめます。

※環境: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ツリーになると、それに合わせてツリーの階層も深くなり、状態のバケツリレーが多くなります。

このバケツリレーは、対象のUI要素(UI要素CとText)にとって新し状態を得るために必要です。

しかし、中間のUI要素(UI要素AとB)にとって全く関係のない状態であるために不要です。ただ、対象のUI要素のためにバケツリレーへ協力しているだけです。

ローカルな変数の確保

CompositionLocalは「UIツリーをスコープに持つローカルな変数を確保する仕組み」です。

これにより、対象のUI要素は、ツリー階層を飛び越えて、ローカルな変数から状態を取得できます。

スポンサーリンク

CompositionLocalの動作

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

// ↓↓ キーを定義 ↓↓
private val testA = compositionLocalOf<String> { error("TestA not found !") }
private val testB = compositionLocalOf<String> { error("TestB not found !") }
private val state = compositionLocalOf<String> { error("State not found !") }
private val testD = compositionLocalOf<String> { error("TestD not found !") }
private val testE = compositionLocalOf<String> { error("TestE not found !") }

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
		
        val _testA = "AAA"
        val _testB = "BBB"
        val _state = "123"
        val _testD = "DDD"
        val _testE = "EEE"
		
        setContent {
			// ↓↓ 状態を保持 ↓↓
            CompositionLocalProvider(
                testA provides _testA,  // ProvideValue<String>(testA, _testA, true)
                testB provides _testB,  // ProvideValue<String>(testB, _testB, true)
                state provides _state,  // ProvideValue<String>(state, _state, true)
                testD provides _testD,  // ProvideValue<String>(testD, _testD, true)
                testE provides _testE   // ProvideValue<String>(testE, _testE, true)
			) {
                Element1()
            }
		}
    }
}

@Composable
fun Element1() {
    Element2()
}

@Composable
fun Element2() {
    Element3()
}

@Composable
fun Element3() {
    Text(text = State.current)		// 状態を参照
}

CompositionLocalによりローカルな変数が作られて、Element3はそこから状態を取得できるようになります。つまり、状態をバイパスする経路です。

CompositionLocalの動作

CompositionLocal実装の要点は4つです。

  • (0)状態の管理
  • (1)キーを定義
  • (2)状態を保持
  • (3)状態を参照
参考:バケツリレーで状態を転送する例

バケツリレーで状態を転送する例です。

UIツリーに入力された状態:”ABC”を必要とするのは、最下段のTextです。ツリートップからTextまで、状態を順繰りに受け渡しています。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
		
        val _testZ = "ABC"

        setContent {
            ElementA(_testZ)
        }
    }
}

@Composable
fun ElementA(state: String) {
    ElementB(state = state)
}

@Composable
fun ElementB(state: String) {
    ElementC(state = state)
}

@Composable
fun ElementC(state: String) {
    Text(text = state)
}

状態のバケツリレー

先に述べましたが、このバケツリレーの欠点は次のとおりです。

  • ツリーの階層が深くなり、バケツリレーが多くなる
  • 中間に位置するUI要素(A,B)にバケツリレーを強要する

(0)状態の管理

CompositionLocalは状態(value)をマップに似たデータ構成で管理します。

CompositonLocalの状態の管理

マップの参照キー(key)はCompositionLocalのインスタンス自身です。

状態はremember+mutableStateOfで宣言されているので、Composition内に状態のオブジェクトが保持され、状態の更新により参照先の関数が再Composeされます。

(1)キーを定義

compositionLocalOf関数はキーを定義します。キーとは関数が返すCompositionLocalのインタンスです。

    :
// ↓↓ キーを定義 ↓↓
private val testA = compositionLocalOf<String> { error("TestA not found !") }
private val testB = compositionLocalOf<String> { error("TestB not found !") }
private val state = compositionLocalOf<String> { error("State not found !") }
private val testD = compositionLocalOf<String> { error("TestD not found !") }
private val testE = compositionLocalOf<String> { error("TestE not found !") }
    :

CompositionLocalは次に示すような継承関係を持ちます。各々の継承階層で与えたれる定義を、ソースコードのコメントへ追記しました。

    :
	:
fun <T> compositionLocalOf(
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy(),
    defaultFactory: () -> T
): ProvidableCompositionLocal<T> = DynamicProvidableCompositionLocal(
        policy, defaultFactory
	)
    :
	:
// ↓↓ Composition内に状態を保持する関数providedを定義 ↓↓
internal class DynamicProvidableCompositionLocal<T> constructor(
    private val policy: SnapshotMutationPolicy<T>,
    defaultFactory: () -> T
) : ProvidableCompositionLocal<T>(defaultFactory) {

    @Composable
    override fun provided(value: T): State<T> = remember { 
	    mutableStateOf(value, policy) 
	}.apply {
        this.value = value
    }
}
    :
	:
// ↓↓ ProvideValueを準備する関数provides(infix:中値記法)を定義 ↓↓
//  (ProvideValue:状態を保持する際に必要な情報)
@Stable
abstract class ProvidableCompositionLocal<T> internal constructor(
    defaultFactory: () -> T
) : CompositionLocal<T> (defaultFactory) {

    @Suppress("UNCHECKED_CAST")
    infix fun provides(value: T) = ProvidedValue(this, value, true)

    @Suppress("UNCHECKED_CAST")
    infix fun providesDefault(value: T) = ProvidedValue(this, value, false)
}
    :
	:
// ↓↓ 状態の参照キー、ComposableとComposition間のI/F(current)を定義 ↓↓
@Stable
sealed class CompositionLocal<T> constructor(defaultFactory: () -> T) {
    @Suppress("UNCHECKED_CAST")
    internal val defaultValueHolder = LazyValueHolder(defaultFactory)

    @Composable
    internal abstract fun provided(value: T): State<T>

    @OptIn(InternalComposeApi::class)
    inline val current: T
        @ReadOnlyComposable
        @Composable
        get() = currentComposer.consume(this)
}
    :
	:

(2)状態を保持

CompositionLocalProvider関数は状態を保持します。具体的には「(0)状態の管理」で説明したマップを作成します。

保持する際にProvidedValueオブジェクトを指定します。一度に複数の指定が可能です。

                    :
				// ↓↓ 状態を保持 ↓↓
                CompositionLocalProvider(
                    testA provides "AAA",  // ProvidedValue<String>(testA, "AAA", true)と同じ
                    testB provides "BBB",  // ProvidedValue<String>(testB, "BBB", true)と同じ
                    state provides "123",  // ProvidedValue<String>(state, "123", true)と同じ
                    testD provides "DDD",  // ProvidedValue<String>(testD, "DDD", true)と同じ
                    testE provides "EEE"   // ProvidedValue<String>(testE, "EEE", true)と同じ
                ) {
                    Element1()
                }
				    :
class ProvidedValue<T> internal constructor(
    val compositionLocal: CompositionLocal<T>,
    val value: T,
    val canOverride: Boolean
)

状態を保持

※「state provides “123”」は「state.provides(“123”)」のinfix(中値記法)です。

(3)状態を参照

状態はCompositionLocal#currentプロパティから参照できます。

CompositionLocal#currentプロパティがComposableとComposition間のI/Fの役割を果たします。

    :
@Composable
fun Element3() {
    Text(text = state.current)
}

状態を参照

ローカルな変数のスコープ

「(2)状態を保持」はCompositonLocalProvider関数によって行われます。

@Composable
@OptIn(InternalComposeApi::class)
fun CompositionLocalProvider(vararg values: ProvidedValue<*>, content: @Composable () -> Unit) {
    currentComposer.startProviders(values)	// 新しい状態へ更新(開始)
    content()
    currentComposer.endProviders()			// 以前の状態へ復帰(終了)
}

startProviders関数は新しい状態へ更新(無い場合は作成)します。

endProviders関数は以前の状態へ復帰(無い場合は未定義)します。

この両者に挟まれたcontent()内が、ローカルな変数のスコープとなり、最新の状態が取り出せます。

また、状態の保持は入れ子にできます。例えば、内側(Element2)にCompositonLocalProvider関数を設けて、状態を再保持するとします。

@Composable
fun Element2() {
    Log.i(TAG, "Element2(ローカル外:Start) State = ${state.current}")
    CompositionLocalProvider(state provides "456") {
        Log.i(TAG, "Element2(ローカル内)       State = ${state.current}")
        Element3()
    }
    Log.i(TAG, "Element2(ローカル外:End)   State = ${state.current}")
}
Element2(ローカル外:Start) State = 123
Element2(ローカル内)       State = 456
Element2(ローカル外:End)   State = 123

状態の保持の入れ子

内側のCompositonLocalProvider関数により状態は更新されますが、スコープは限定的です。

スポンサーリンク

ローカルな変数のデフォルト

「(1)キーを定義」はcompositionLocalOf関数によって行われます。

fun <T> compositionLocalOf(
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy(),
    defaultFactory: () -> T
): ProvidableCompositionLocal<T> = DynamicProvidableCompositionLocal(
        policy, defaultFactory
	)

この関数の引数defaultFactoryに与えるラムダ式の答え(最後に評価した値)は、「(2)状態を保持」する前に参照した際のデフォルト値です。

デフォルト:エラーの場合

private val state = compositionLocalOf<String> { error("State not found !") }

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Log.i(TAG, "State = ${state.current}")		// 状態の参照
            CompositionLocalProvider(state provides "123") {
                Element1()
            }
        }
    }
}
private val state = compositionLocalOf<String> { error("State not found !") }

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CompositionLocalProvider(state provides "123") {
                Element1()
            }
            Log.i(TAG, "State = ${state.current}")		// 状態の参照
        }
    }
}
FATAL EXCEPTION: main
Process: com.example.app, PID: 7576
java.lang.IllegalStateException: State not found !

デフォルト:値の場合

private val state = compositionLocalOf<String> { "Default state !!" }

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Log.i(TAG, "State = ${state.current}")		// 状態の参照
            CompositionLocalProvider(state provides "123") {
                Element1()
            }
        }
    }
}
private val state = compositionLocalOf<String> { "Default state !!" }

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CompositionLocalProvider(state provides "123") {
                Element1()
            }
            Log.i(TAG, "State = ${state.current}")		// 状態の参照
        }
    }
}
State = Default state !!
スポンサーリンク

関連記事:

プロジェクトのビルドで「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 ...
状態の保持(remenber)はアプリの画面(UI)を制御・管理するために必要な動作です。 再Composeのスケジューリング(mutableStateOf)に並び、Jetpack Composeの重要な技術の一つです。 今回は「再Composeを超えて状態の保持」について、まとめます。 ※環境: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ツリーにローカルな変数を確保します。 その変数の参照キーは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 ...
スポンサーリンク