Jetpack Compose:staticCompositionLocalOfの違い

投稿日:  更新日:

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の継承関係

CompositionLocalは次に示すような継承関係を持ちます。

CompositionLocal#provided関数の実装に見ると、Dynamic側はremember+mutableStateOfが用いられているのに対し、Static側はdata classが用いられています。

staticCompositionLocalOf(dynamic)CompositionLocalOf
    :
	:
fun <T> staticCompositionLocalOf(
    defaultFactory: () -> T
): ProvidableCompositionLocal<T> = StaticProvidableCompositionLocal(
        defaultFactory
	)
	:
	:
	:
internal class StaticProvidableCompositionLocal<T>(
    defaultFactory: () -> T
) : ProvidableCompositionLocal<T>(defaultFactory) {

    @Composable
    override fun provided(value: T): State<T> = StaticValueHolder(value)
}
	:
	:
	:
	:
	:
	:
	:
@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)
}
    :
	:
@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)
}
    :
	:
    :
	:
fun <T> compositionLocalOf(
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy(),
    defaultFactory: () -> T
): ProvidableCompositionLocal<T> = DynamicProvidableCompositionLocal(
        policy, defaultFactory
	)
    :
	:
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
    }
}
    :
	:
@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)
}
    :
	:
@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)
}
    :
	:
internal data class StaticValueHolder<T>(override val value: T) : State<T>

Static側は状態(ローカルな変数)の監視を行いません。

これについて、ドキュメントは以下のように説明しています。

Unlike compositionLocalOf, reads of a staticCompositionLocalOf are not tracked by the composer and changing the value provided in the CompositionLocalProvider call will cause the entirety of the content to be recomposed instead of just the places where in the composition the local value is used. This lack of tracking, however, makes a staticCompositionLocalOf more efficient when the value provided is highly unlikely to or will never change. 
-----
CompositionLocalOf とは異なり、staticCompositionLocalOf の読み取りはコンポーザーによって追跡されず、CompositionLocalProvider 呼び出しで指定された値を変更すると、コンポジション内のローカル値が使用されている場所だけでなく、コンテンツ全体が再構成されます。 ただし、この追跡がないため、提供された値が変更される可能性が非常に低い場合、または変更されない場合には、staticCompositionLocalOf がより効率的になります。

状態の変更される可能性が低いならば、監視を行わないstatic側を使用することで効率のよい処理になります。

スポンサーリンク

再Composeの様子

Static側とDynamic側で再Composeの様子は異なります。

サンプルは、ボタンの押下でローカルな変数へ保持された状態(_moji)が更新され、”123″と”ABC”の間をトグルします。

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

class MainActivity : ComponentActivity() {
    @SuppressLint("CoroutineCreationDuringComposition", "UnrememberedMutableState")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MyApplicationTheme {
                var _toggle by remember { mutableStateOf(false) }
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    val _moji = if(_toggle) "ABC" else "123"
                    Column {
                        CompositionLocalProvider(state provides _moji) {
                            Element1()
                            ElementA()
                        }
                        Button(onClick = { _toggle = (! _toggle) }) {
                            Text(text = "Recompose")
                        }
                    }
                }
            }
        }
    }
}

// -----
@Composable
fun Element1() {
    Log.i(TAG, "Recompose Element1 !")
    Element2()
}
@Composable
fun Element2() {
    Log.i(TAG, "Recompose Element2 !")
    Element3()
}
@Composable
fun Element3() {
    Log.i(TAG, "Recompose Element3 !")
    Text(text = state.current)				// ローカルな変数の参照
}
// -----
@Composable
fun ElementA() {
    Log.i(TAG, "Recompose ElementA !")
    ElementB()
}
@Composable
fun ElementB() {
    Log.i(TAG, "Recompose ElementB !")
    ElementC()
}
@Composable
fun ElementC() {
    Log.i(TAG, "Recompose ElementC !")
}

Static側もDynamic側も、問題なく表示は更新されます。

ローカルな変数による再Compose

しかし、再Composeの様子は異なります。

【Static側】

Static側の再Compose
【Dynamic側】

Dynamic側の再Compose

Dynamic側は更新された表示のみを再Composeしますが、Static側はコンテンツの全てを再Composeします。

これについて、ドキュメントは以下のように説明しています。

Unlike compositionLocalOf, reads of a staticCompositionLocalOf are not tracked by the composer and changing the value provided in the CompositionLocalProvider call will cause the entirety of the content to be recomposed instead of just the places where in the composition the local value is used. This lack of tracking, however, makes a staticCompositionLocalOf more efficient when the value provided is highly unlikely to or will never change. 
-----
CompositionLocalOf とは異なり、staticCompositionLocalOf の読み取りはコンポーザーによって追跡されず、CompositionLocalProvider 呼び出しで指定された値を変更すると、コンポジション内のローカル値が使用されている場所だけでなく、コンテンツ全体が再構成されます。ただし、この追跡がないため、提供された値が変更される可能性が非常に低い場合、または変更されない場合には、staticCompositionLocalOf がより効率的になります。

つまり、Static側の再Compose動作は、そのような仕様です。

スポンサーリンク

関連記事:

プロジェクトのビルドで「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ツリーで発生する状態のバケツリレーを解決してくれます。 また、広く共有したい状態の定義にも適しています。例えば、「ツリーのある階層以下に対して」などと言った場合です。 CompositionLocalについて、まとめます。 ※環境: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 ...
スポンサーリンク