Compose学习笔记(六):CompositionLocal的应用场景

3,517 阅读4分钟

这是一个Compose学习系列,记录笔者在学习Compose过程中的核心知识点:

Compose学习笔记(一):基本控件

Compose学习笔记(二):Composable和MutableState

Compose学习笔记(三)重组和无状态

Compose学习笔记(四):状态机制和重组优化

Compose学习笔记(五):derivedStateOf和remember的使用

本篇主要介绍的知识点

  • CompositionLocal的使用场景
  • CompositionLocalOf()和staticCompositionLocalOf()
  • Material中CompositonLocal的应用

CompositionLocal的用法

CompositionLocal的使用场景

在Compose函数里,如果要用到函数外提供的值,一般都要通过函数定义好参数,外部调用时传入进去。

这种方法表面看没啥问题,但在一些场景下就不是很方便。比如一个函数A嵌套一个函数B,函数B嵌套函数C,并且这A、B、C的函数都用到这个参数,那对三个函数来说都需要定义这个参数,就不是那么方便。

那用个全局变量不就可以解决需要重复定义参数,确实可以。但全局变量有个副作用就是影响的范围比较大。本身我们定义的参数只会影响到函数内部。这个时候我们就可以使用CompositionLocal:它具有穿透函数功能的局部变量

CompositionLocal适用场景:用来提供上下文数据,不扩大影响范围。

一个简单的例子

举个例子:一个Text文本根据提供的颜色设置背景

使用方法:

1.使用compositionLocalOf先定义一个LocalColor变量,提供一个默认值

val LocalColor = compositionLocalOf { Color.Black }

如果无法提供默认值,可以使用error抛出一个异常:

val LocalColor = compositionLocalOf { error("LocalColor 没有提供值!") }

2.使用CompositionLocalProvider提供一个蓝色值:

CompositionLocalProvider(LocalColor provides Color.Blue) {
    LocalColorText()
}

3.函数中使用LocalColor.current获取提供的颜色值

@Composable
fun LocalColorText() {
    Text("我是什么颜色的", Modifier.background(LocalColor.current))
}

完整代码示例如下:

// 定义好本地颜色,默认是颜色是Black
val LocalColor = compositionLocalOf { Color.Black }
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        Column {
          // 该Provider下提供蓝色
          CompositionLocalProvider(LocalColor provides Color.Blue) {
              LocalColorText()
          }
          // 该Provider下提供绿色
          CompositionLocalProvider(LocalColor provides Color.Green) {
              LocalColorText()
          }
        }
    }
  
    @Composable
    fun LocalColorText() {
        Text("我是什么颜色的", Modifier.background(LocalColor.current))
    }
}

上述代码中,两次使用CompositionLocalProvider,分别提供不同的颜色值,它们不互相影响,只影响它括号包住的内容,对本代码来说也就是它们各自的LocalColorText函数

CompositionLocalProvider使用运行结果.png

CompositionLocalOf()和staticCompositionLocalOf()

除了compositionLocalOf定义,还可以使用staticCompositionLocalOf(),用法类似:

val LocalColor = staticCompositionLocalOf { Color.Black }

compositionLocalOf()staticCompositionLocalOf() 的区别:

  • compositionLocalOf() 会精准订阅、值改变时精准Recompose,所以性能的消耗在于订阅阶段。
  • staticCompositionLocalOf()不支持订阅、值改变时全量Recompose,所以性能的消耗在于更新阶段。

所以基于性能消耗的阶段,要根据不同场景选择:

  • compositionLocalOf() 适合值可能会经常改变的场景。
  • staticCompositionLocalOf()适合值不会改变或⼏乎不会改变的场景。比如compose源码里提供的LocalView
val LocalView = staticCompositionLocalOf<View> {
    noLocalProvidedFor("LocalView")
}

Material中CompositonLocal的应用

CompositonLocal很适用的场景之一是主题,比如我们看Material库中MaterialTheme的源码对它就有很好的应用:

@Composable
fun MaterialTheme(
    colors: Colors = MaterialTheme.colors,  // 默认使用MaterialTheme.colors
    typography: Typography = MaterialTheme.typography,
    shapes: Shapes = MaterialTheme.shapes,
    content: @Composable () -> Unit
) {
    val rememberedColors = remember {
        colors.copy()
    }.apply { updateColorsFrom(colors) }
    val rippleIndication = rememberRipple()
    val selectionColors = rememberTextSelectionColors(rememberedColors)
    // 关注这个LocalColors,提供rememberedColors值
    CompositionLocalProvider(
        LocalColors provides rememberedColors,
        LocalContentAlpha provides ContentAlpha.high,
        LocalIndication provides rippleIndication,
        LocalRippleTheme provides MaterialRippleTheme,
        LocalShapes provides shapes,
        LocalTextSelectionColors provides selectionColors,
        LocalTypography provides typography
    ) {
        ProvideTextStyle(value = typography.body1) {
            PlatformMaterialTheme(content)
        }
    }
}

Materialcolors就用到了LocalColors

val colors: Colors
    @Composable
    @ReadOnlyComposable
    get() = LocalColors.current

// 默认值是lightColors方法提供的
internal val LocalColors = staticCompositionLocalOf { lightColors() }

如果我们在MaterialTheme里,使用MaterialButton控件,它就会使用lightColors()提供的主题属性

MaterialTheme() {
    Button(onClick = { }) {
        Text("对比按钮的颜色和colors里的background")
    }
}

比如Button的背景就会使用MaterialTheme.colors.primary

fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    elevation: ButtonElevation? = ButtonDefaults.elevation(),
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),   // 背景颜色在这里面
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit
){ ...}
​
fun buttonColors(
        backgroundColor: Color = MaterialTheme.colors.primary,  // 背景颜色在这里
        contentColor: Color = contentColorFor(backgroundColor),
        disabledBackgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
            .compositeOver(MaterialTheme.colors.surface),
        disabledContentColor: Color = MaterialTheme.colors.onSurface
            .copy(alpha = ContentAlpha.disabled)
    ){...}

所以,如果没有为LocalColors提供值的话,Button的背景就会使用默认值lightColors()提供的primary颜色

fun lightColors(
    primary: Color = Color(0xFF6200EE),
    primaryVariant: Color = Color(0xFF3700B3),
    secondary: Color = Color(0xFF03DAC6),
    secondaryVariant: Color = Color(0xFF018786),
    background: Color = Color.White,
    surface: Color = Color.White,
    error: Color = Color(0xFFB00020),
    onPrimary: Color = Color.White,
    onSecondary: Color = Color.Black,
    onBackground: Color = Color.Black,
    onSurface: Color = Color.Black,
    onError: Color = Color.White
): Colors = Colors(...)

当我们想改个主题也很简单:

val colors = darkColors()
MaterialTheme(colors = colors) {
    Button(onClick = { }) {
        Text("对比按钮的颜色和colors里的background")
    }
}