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 !!
関連記事:

