CompositionLocal
は、Composition を通じてデータを暗黙的に伝えるためのツールです。このページでは、CompositionLocal
の詳細、独自の CompositionLocal
を作成する方法、CompositionLocal
がユースケースに適した解決手段かどうかを判断する方法について説明します。
CompositionLocal
のご紹介
通常、Compose では、データは UI ツリーを通じて各コンポーズ可能な関数にパラメータとして 流れ落ちます。こうすると、コンポーザブルの依存関係が明示的になります。しかし、色や型スタイルなど、幅広く頻繁に使用されるデータの場合は、大変扱いにくいものになります。次の例をご覧ください。
@Composable fun MyApp() { // Theme information tends to be defined near the root of the application val colors = colors() } // Some composable deep in the hierarchy @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, color = colors.onPrimary // ← need to access colors here ) }
ほとんどのコンポーザブルに明示的なパラメータ依存関係として色を渡す必要がなくなるよう、Compose には CompositionLocal
が用意されています。これを使用して、ツリーをスコープとする名前付きオブジェクトを作成し、データを UI ツリーに流し込むための暗黙の方法としてそれを使用できます。
CompositionLocal
要素は通常、UI ツリーの特定のノードで値が設定されます。その値は、コンポーズ可能な関数のパラメータとして CompositionLocal
を宣言しなくても、コンポーズ可能な子孫で使用できます。
CompositionLocal
は、マテリアル テーマが内部で使用するものです。MaterialTheme
は、3 つの CompositionLocal
インスタンス(colorScheme
、typography
、shapes
)を提供して、後から Composition の子孫部分で取得できるようにするオブジェクトです。具体的には、MaterialTheme
colorScheme
、shapes
、typography
属性からアクセスできる、LocalColorScheme
、LocalShapes
、LocalTypography
プロパティです。
@Composable fun MyApp() { // Provides a Theme whose values are propagated down its `content` MaterialTheme { // New values for colorScheme, typography, and shapes are available // in MaterialTheme's content lambda. // ... content here ... } } // Some composable deep in the hierarchy of MaterialTheme @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, // `primary` is obtained from MaterialTheme's // LocalColors CompositionLocal color = MaterialTheme.colorScheme.primary ) }
CompositionLocal
インスタンスのスコープは Composition の一部であるため、ツリーの各レベルで異なる値を指定できます。CompositionLocal
の current
値は、Composition の該当部分の祖先が設定する最も近い値に対応します。
CompositionLocal
に新しい値を設定するには、CompositionLocalProvider
と、CompositionLocal
キーを value
に関連付ける provides
中置関数を使用します。CompositionLocalProvider
の content
ラムダは、CompositionLocal
の current
プロパティにアクセスすると、設定された値を取得します。新しい値が設定されると、Compose が CompositionLocal
を読み取る Composition の一部を再コンポーズします。
たとえば、LocalContentColor
CompositionLocal
には、テキストやアイコンに使用される推奨のコンテンツ色が含まれています。これにより、現在の背景色と対照的な色になります。次の例では、CompositionLocalProvider
を使用して、Composition の各部分で異なる値を設定しています。
@Composable fun CompositionLocalExample() { MaterialTheme { // Surface provides contentColorFor(MaterialTheme.colorScheme.surface) by default // This is to automatically make text and other content contrast to the background // correctly. Surface { Column { Text("Uses Surface's provided content color") CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) { Text("Primary color provided by LocalContentColor") Text("This Text also uses primary as textColor") CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) { DescendantExample() } } } } } } @Composable fun DescendantExample() { // CompositionLocalProviders also work across composable functions Text("This Text uses the error color now") }
図 1. CompositionLocalExample
コンポーザブルのプレビュー。
最後の例では、CompositionLocal
インスタンスを Material コンポーザブルが内部で使用していました。CompositionLocal
の現在の値にアクセスするには、その current
プロパティを使用します。次の例では、Android アプリで一般的に使用されている LocalContext
CompositionLocal
の現在の Context
値を使ってテキストの書式設定を行っています。
@Composable fun FruitText(fruitSize: Int) { // Get `resources` from the current value of LocalContext val resources = LocalContext.current.resources val fruitText = remember(resources, fruitSize) { resources.getQuantityString(R.plurals.fruit_title, fruitSize) } Text(text = fruitText) }
独自の CompositionLocal
を作成する
CompositionLocal
は、Composition を通じてデータを暗黙的に伝えるためのツールです。
パラメータが横断的であり、実装の中間レイヤがその存在を意識すべきでない場合も、CompositionLocal
を活用できます。中間レイヤに意識させると、コンポーザブルの有用性が限定的になるためです。たとえば、Android 権限のクエリには、内部で CompositionLocal
が使用されます。メディア ピッカー コンポーザブルは、権限で保護されたデバイス上のコンテンツにアクセスするための新しい機能を追加できます。このとき、API を変更して、メディア ピッカーの呼び出し元に、環境から使用される追加されたコンテキストを認識させる必要はありません。
ただし、CompositionLocal
が常に最適な解決手段だとは限りません。CompositionLocal
の乱用はおすすめしません。次のようなデメリットがあるためです。
CompositionLocal
を使用すると、コンポーザブルの動作を理解することが難しくなります。暗黙的な依存関係ができるため、それを使用するコンポーザブルの呼び出し元で、すべての CompositionLocal
の値が満たされていることを確認する必要があります。
さらに、この依存関係は Composition のどの部分でも変更される可能性があるため、この依存関係の信頼できる明確な情報源がなくなります。したがって、問題が起きたときにアプリをデバッグするのが難しくなります。Composition をたどって current
値が設定された場所を確認する必要があるためです。IDE の Find usages や Compose Layout Inspector などのツールを使用すれば、この問題を軽減するのに十分な情報を得ることができます。
CompositionLocal
を使用するかどうかを判断する
CompositionLocal
がユースケースに適した解決手段になる条件には、次のものがあります。
CompositionLocal
に適切なデフォルト値が設定されている。デフォルト値が存在しない場合、デベロッパーが CompositionLocal
の値が設定されていない状況に陥ることが極めて困難になるよう保証する必要があります。デフォルト値を指定しない場合に、テストの作成時に問題やフラストレーションが発生したり、その CompositionLocal
を使用するコンポーザブルのプレビューに、常にそれを明示的に指定することが必要になったりします。
スコープがツリーやサブ階層だとは言えないコンセプトには、CompositionLocal
を使用しないでください。CompositionLocal
は、少数のインスタンスではなく、どの子孫にも使用される可能性がある場合に意味があります。
ユースケースがこういった要件を満たしていない場合は、CompositionLocal
を作成する前に、考慮すべき代替手段を確認してください。
不適切な利用の例として、特定の画面の ViewModel
を保持する CompositionLocal
を作成して、その画面のすべてのコンポーザブルが ViewModel
への参照を取得してロジックを実行できるようにするというものがあります。特定の UI ツリーにあるすべてのコンポーザブルが ViewModel
を認識する必要はないので、この方法は適切ではありません。コンポーザブルには、状態が下に流れ、イベントが登ってくるというパターンに沿って、必要な情報のみを渡すことをおすすめします。このやり方ならば、コンポーザブルは再利用しやすく、テストも容易になります。
CompositionLocal
の作成
CompositionLocal
を作成するための API は次の 2 つです。
compositionLocalOf
: 再コンポーズ中に提供された値を変更すると、current
値を読み取るコンテンツのみが無効になります。staticCompositionLocalOf
:compositionLocalOf
とは異なり、staticCompositionLocalOf
の読み取りを Compose が追跡しません。この値を変更すると、Composition でcurrent
値が読み取られた場所だけではなく、CompositionLocal
が設定された場所で、content
ラムダ全体が再コンポーズされます。
CompositionLocal
に設定された値が変わる可能性が非常に低いか、まったく変更されない場合は、staticCompositionLocalOf
を使用してパフォーマンスを向上させます。
たとえば、アプリの設計システムは、UI コンポーネントにシャドウを使ってコンポーザブルにエレベーションを設定しているという点で独特です。アプリのエレベーションが UI ツリー全体に伝わる必要があるため、CompositionLocal
を使用しています。CompositionLocal
値はシステムテーマに基づいて条件付きで導出されるため、次のように compositionLocalOf
API を使用します。
// LocalElevations.kt file data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp) // Define a CompositionLocal global object with a default // This instance can be accessed by all composables in the app val LocalElevations = compositionLocalOf { Elevations() }
CompositionLocal
に値を設定する
CompositionLocalProvider
コンポーザブルは、特定の階層の CompositionLocal
インスタンスに値をバインドします。CompositionLocal
に新しい値を設定するには、次のように、CompositionLocal
キーを value
に関連付ける provides
中置関数を使用します。
// MyActivity.kt file class MyActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { // Calculate elevations based on the system theme val elevations = if (isSystemInDarkTheme()) { Elevations(card = 1.dp, default = 1.dp) } else { Elevations(card = 0.dp, default = 0.dp) } // Bind elevation as the value for LocalElevations CompositionLocalProvider(LocalElevations provides elevations) { // ... Content goes here ... // This part of Composition will see the `elevations` instance // when accessing LocalElevations.current } } } }
CompositionLocal
の使用
CompositionLocal.current
は、その CompositionLocal
に値を設定する最も近い CompositionLocalProvider
から設定された値を返します。
@Composable fun SomeComposable() { // Access the globally defined LocalElevations variable to get the // current Elevations in this part of the Composition MyCard(elevation = LocalElevations.current.card) { // Content } }
考慮すべき代替手段
CompositionLocal
は、ユースケースによっては過剰な解決手段となる場合があります。ユースケースが、CompositionLocal を使用すべきかどうかの判断で挙げた基準を満たしていない場合は、別の解決手段が適していると考えられます。
明示的なパラメータを渡す
コンポーザブルの依存関係は明確にすることが推奨されます。コンポーザブルは、必要なものにのみ渡すことをおすすめします。コンポーザブルの分離と再利用を促進するには、各コンポーザブルに保持する情報をできるだけ少なくする必要があります。
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel.data) } // Don't pass the whole object! Just what the descendant needs. // Also, don't pass the ViewModel as an implicit dependency using // a CompositionLocal. @Composable fun MyDescendant(myViewModel: MyViewModel) { /* ... */ } // Pass only what the descendant needs @Composable fun MyDescendant(data: DataToDisplay) { // Display data }
制御の反転
コンポーザブルに不要な依存関係を渡さないようにするもう 1 つの方法は、制御の反転を使用することです。なんらかのロジックを実行するために子孫が依存関係を受け取るのではなく、親がそれを行います。
以下の例では、子孫がデータを読み込むためのリクエストをトリガーする必要があります。
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel) } @Composable fun MyDescendant(myViewModel: MyViewModel) { Button(onClick = { myViewModel.loadData() }) { Text("Load data") } }
状況によって異なりますが、MyDescendant
の役割が増えます。また、MyViewModel
を依存関係として渡すと、それらが結びつくため MyDescendant
の再利用性が低下します。依存関係を子孫に渡さず、ロジックの実行を祖先に委ねる制御の反転の原則を利用することを検討してください。
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusableLoadDataButton( onLoadClick = { myViewModel.loadData() } ) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text("Load data") } }
この方法は、子を直接の祖先から切り離すため、ユースケースによっては適している場合があります。祖先コンポーザブルは、下位レベルのコンポーザブルが柔軟になる代わりに、複雑になる傾向があります。
同様に、@Composable
コンテンツ ラムダを同じように使用して、同じメリットを得ることもできます。
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- Compose 内のテーマの構造
- Compose でビューを使用する
- Jetpack Compose で Kotlin を使用する