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 ) }
UI要素は関数
UI要素はComposable関数で表現されます。そして、UI要素が子UI要素を呼び出し、ツリー構造を作ることで、アプリの画面が構成されます。
Composable関数はKotlinの一般的な関数です。
関数は自身の枠を超えて、変数にデータを保持することが出来ません。関数内で宣言した変数の有効範囲は関数の中のみであり、有効期間は実行中のみです。実行の終了とともに、変数は破棄されます。
従って、UI要素が状態を表示するには、Composable関数が実行される毎に、状態を入力しなければなりません。なぜなら、状態を変数へ保持できないからです。入力は引数で行われます。
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実装の要点は4つです。
- (0)状態の管理
- (1)キーを定義
- (2)状態を保持
- (3)状態を参照
(0)状態の管理
CompositionLocalは状態(value)をマップに似たデータ構成で管理します。
マップの参照キー(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 !!
関連記事: