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が用いられています。
: : 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の様子は異なります。
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動作は、そのような仕様です。
関連記事: