2021年4月15日木曜日

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

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

バケツリレー問題

例えば深いところの Composable で Context が必要だとします。
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.   
  6.         val context: Context = this  
  7.   
  8.         setContent {  
  9.             MyAppTheme {  
  10.                 MyApp(context)  
  11.             }  
  12.         }  
  13.     }  
  14. }  
  15.   
  16. @Composable  
  17. fun MyApp(context:Context) {  
  18.     TopScreen(context)  
  19. }  
  20.   
  21. @Composable  
  22. fun TopScreen(context:Context) {  
  23.     MyList(context)  
  24. }  
  25.   
  26. @Composable  
  27. fun MyList(context:Context) {  
  28.     // use context  
  29. }  
この場合 Context を必要としているのは MyList ですが、MyList に Context を渡すために

MyApp -> TopScreen -> MyList

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

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

Composition Local

Composition Local を使うと、深いところの Composable にデータを渡すことができます。
上のコードを Composition Local を使って書き換えるとこのようになります。
後ほどそれぞれを詳しく説明します。
  1. private val MyLocalContext = staticCompositionLocalOf<Context> {   
  2.     error("No current Context")   
  3. }  
  4.   
  5. class MainActivity : AppCompatActivity() {  
  6.   
  7.     override fun onCreate(savedInstanceState: Bundle?) {  
  8.         super.onCreate(savedInstanceState)  
  9.   
  10.         val context: Context = this  
  11.   
  12.         setContent {  
  13.             MyAppTheme {  
  14.                 CompositionLocalProvider(MyLocalContext provides context) {  
  15.                     MyApp()  
  16.                 }  
  17.             }  
  18.         }  
  19.     }  
  20. }  
  21.   
  22. @Composable  
  23. fun MyApp() {  
  24.     TopScreen()  
  25. }  
  26.   
  27. @Composable  
  28. fun TopScreen() {  
  29.     MyList()  
  30. }  
  31.   
  32. @Composable  
  33. fun MyList() {  
  34.     val context:Context = MyLocalContext.current  
  35.     // use context  
  36. }  
実は Context を受け渡す Composition Local は Jetpack Compose の方で用意されています。それが LocalContext です。
LocalContext を使って書き換えるとこのようになります。
Context が欲しい Composable で LocalContext.current から Context のインスタンスが取得できます。
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.   
  6.         setContent {  
  7.             MyAppTheme {  
  8.                 MyApp()  
  9.             }  
  10.         }  
  11.     }  
  12. }  
  13.   
  14. @Composable  
  15. fun MyApp() {  
  16.     TopScreen()  
  17. }  
  18.   
  19. @Composable  
  20. fun TopScreen() {  
  21.     MyList()  
  22. }  
  23.   
  24. @Composable  
  25. fun MyList() {  
  26.     val context:Context = LocalContext.current  
  27.     // use context  
  28. }  


用意されている Composition Local

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


独自の Composition Local を作る

まず、受け渡したい値のホルダーになる ProvidableCompositionLocal を作ります。作るには staticCompositionLocalOf() か compositionLocalOf() を使います。
  1. private val MyLocalColor: ProvidableCompositionLocal<Color> = compositionLocalOf<Color> {  
  2.     error("No current color")  
  3. }  
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<*> なので、複数渡すこともできます。
  1. CompositionLocalProvider(MyLocalColor provides Color.Black) {  
  2.     MyApp()  
  3. }  
こうすると、CompositionLocalProvider の content lambda 内の Composable では ProvidableCompositionLocal.current で指定された値を取得することができます。
  1. @Composable  
  2. fun MyApp() {  
  3.     TopScreen()  
  4. }  
  5.   
  6. @Composable  
  7. fun TopScreen() {  
  8.     MyList()  
  9. }  
  10.   
  11. @Composable  
  12. fun MyList() {  
  13.     val color:Color = MyLocalColor.current // Color.Black  
  14. }  


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


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

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

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


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


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

これにより Surface の color を指定することで、内部に置かれた LocalContentColor を使っている Text などの色が自動で変わるようになっています。
  1. @Composable  
  2. fun Surface(  
  3.     ...  
  4.     color: Color = MaterialTheme.colors.surface,  
  5.     contentColor: Color = contentColorFor(color),  
  6.     ...  
  7.     content: @Composable () -> Unit  
  8. ) {  
  9.     ...  
  10.     CompositionLocalProvider(  
  11.         LocalContentColor provides contentColor,  
  12.         LocalAbsoluteElevation provides absoluteElevation  
  13.     ) {  
  14.         Box(  
  15.             ...  
  16.         ) {  
  17.             content()  
  18.         }  
  19.     }  
  20. }  



0 件のコメント:

コメントを投稿