2021年4月15日木曜日

Jetpack Compose の Composition Local ってなんなの?

Jetpack Compose には Composition Local という仕組みが用意されています。
これを使うとバケツリレー問題に対応することができます。

バケツリレー問題

例えば深いところの Composable で Context が必要だとします。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val context: Context = this setContent { MyAppTheme { MyApp(context) } } } } @Composable fun MyApp(context:Context) { TopScreen(context) } @Composable fun TopScreen(context:Context) { MyList(context) } @Composable fun MyList(context:Context) { // use context } この場合 Context を必要としているのは MyList ですが、MyList に Context を渡すために

MyApp -> TopScreen -> MyList

と Context をバケツリレーしています。

しかしこの方法では、途中の MyApp と TopScreen では Context を使わないにもかかわらず、受け渡しのためだけに引数に Context が必要になってしまいます。 その Composable で使わないものが引数にあると、何をその Compose で必要としているのかがわかりににくくなってしまいます。

Composition Local

Composition Local を使うと、深いところの Composable にデータを渡すことができます。
上のコードを Composition Local を使って書き換えるとこのようになります。
後ほどそれぞれを詳しく説明します。 private val MyLocalContext = staticCompositionLocalOf<Context> { error("No current Context") } class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val context: Context = this setContent { MyAppTheme { CompositionLocalProvider(MyLocalContext provides context) { MyApp() } } } } } @Composable fun MyApp() { TopScreen() } @Composable fun TopScreen() { MyList() } @Composable fun MyList() { val context:Context = MyLocalContext.current // use context } 実は Context を受け渡す Composition Local は Jetpack Compose の方で用意されています。それが LocalContext です。
LocalContext を使って書き換えるとこのようになります。
Context が欲しい Composable で LocalContext.current から Context のインスタンスが取得できます。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyAppTheme { MyApp() } } } } @Composable fun MyApp() { TopScreen() } @Composable fun TopScreen() { MyList() } @Composable fun MyList() { val context:Context = LocalContext.current // use context }

用意されている Composition Local

LocalContext 以外にも LocalDensity, LocalConfiguration, LocalLifecycleOwner, LocalContentColor などが用意されています。


独自の Composition Local を作る

まず、受け渡したい値のホルダーになる ProvidableCompositionLocal を作ります。作るには staticCompositionLocalOf() か compositionLocalOf() を使います。 private val MyLocalColor: ProvidableCompositionLocal<Color> = compositionLocalOf<Color> { error("No current color") } staticCompositionLocalOf() および compositionLocalOf() に渡す lambda はデフォルト値のファクトリーです。値が与えられる前に取得しようとすると、このファクトリーが呼ばれます。

staticCompositionLocalOf() だと StaticProvidableCompositionLocal が生成され、compositionLocalOf() だと DynamicProvidableCompositionLocal が生成されます。

staticCompositionLocalOf() (StaticProvidableCompositionLocal) は滅多に変わらない値に使います。

例えば LocalContext や LocalLifecycleOwner は staticCompositionLocalOf() を使っていますが、LocalConfiguration は compositionLocalOf() を使っています。



ProvidableCompositionLocal の provides() または providesDefault() で 値を指定して ProvidedValue を取得します。
provides() および providesDefault() には infix がついているので、MyLocalColor provides Color.Black のように記述できます。

次に、取得した ProvidedValue を CompositionLocalProvider Composable に渡します。
CompositionLocalProvider の第1引数は vararg values: ProvidedValue<*> なので、複数渡すこともできます。 CompositionLocalProvider(MyLocalColor provides Color.Black) { MyApp() } こうすると、CompositionLocalProvider の content lambda 内の Composable では ProvidableCompositionLocal.current で指定された値を取得することができます。 @Composable fun MyApp() { TopScreen() } @Composable fun TopScreen() { MyList() } @Composable fun MyList() { val color:Color = MyLocalColor.current // Color.Black }

provides() で指定された値を上書きできます。 CompositionLocalProvider(MyLocalColor provides Color.Black) { Column { Text("Hello", color = MyLocalColor.current) // 文字色は Color.Black CompositionLocalProvider(MyLocalColor provides Color.Red) { Text("Hello", color = MyLocalColor.current) // 文字色は Color.Red } } } providesDefault() ではすでに指定された値がある場合は上書きしません。 CompositionLocalProvider(MyLocalColor provides Color.Black) { Column { Text("Hello", color = MyLocalColor.current) // 文字色は Color.Black CompositionLocalProvider(MyLocalColor providesDefault Color.Red) { Text("Hello", color = MyLocalColor.current) // 文字色は Color.Black のまま } } }

用意されている Composable での Composition Local の利用

Jetpack Compose で用意されている Composable では Composition Local がたくさん使われています。
ここでは一部を紹介します。

例えば string resource id を指定して文字列を取得する stringResource() では内部で Resources インスタンスを取得する resources() を使っています。この resources() では LocalContext から Context を取得しています。 @Composable @ReadOnlyComposable private fun resources(): Resources { LocalConfiguration.current return LocalContext.current.resources }

Text Composable ではデフォルトのスタイルとして LocalTextStyle から TextStyle を取得しています。また、文字色の指定がないときは LocalContentColor と LocalContentAlpha が使われます。 @Composable fun Text( text: AnnotatedString, ... style: TextStyle = LocalTextStyle.current ) { val textColor = color.takeOrElse { style.color.takeOrElse { LocalContentColor.current.copy(alpha = LocalContentAlpha.current) } } ... } よって CompositionLocalProvider で LocalTextStyle を上書きすると、Text のデフォルトスタイルが変わります。 CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.h3) { Text("Hello") // h3 }

Surface では LocalContentColor を contentColor で上書きしています。
contentColor はデフォルトでは color (背景色) から導出 (color が primary なら onPrimary, color が secondary から onSecondary など) しています。

これにより Surface の color を指定することで、内部に置かれた LocalContentColor を使っている Text などの色が自動で変わるようになっています。 @Composable fun Surface( ... color: Color = MaterialTheme.colors.surface, contentColor: Color = contentColorFor(color), ... content: @Composable () -> Unit ) { ... CompositionLocalProvider( LocalContentColor provides contentColor, LocalAbsoluteElevation provides absoluteElevation ) { Box( ... ) { content() } } }


0 件のコメント:

コメントを投稿