Compose 工具之 CompositionLocal

188 阅读6分钟

什么是 CompositionLocal

Compose通过可组合函数的参数显式地在组合树中传递数据。这通常是让数据流过树的最简单和最好的方法。 有时,这个模型可能很麻烦,或者对于许多组件需要的数据,或者当组件需要在彼此之间传递数据但保持实现细节私有时,这个模型可能会崩溃。对于这些情况,可以使用CompositionLocals作为一种隐式方式来让数据流过组合。

通俗点来说,就是 Composable 函数获取状态通常都是通过函数的参数显示传递,但有的状态有可能在任何 Composable 函数中都有可能用到,如果还通过参数传递,链路就有可能非常深,不是很方便。这个时候便是 CompositionLocal 该出场的时候了。CompositionLocal 可以让你不需要显示传递参数就能在某个区域内共享制定的状态。

一个例子

@Composable
fun CommonText(text: String, fontSize: TextUnit) {
    Text(text = text, fontSize = fontSize)
}

@Preview
@Composable
fun Greeting(modifier: Modifier = Modifier) {
    Column {
        CommonText("Hello World", 30.sp)
    }
}

比如此示例,通常我们需要将fontSize通过传递到函数内,然后才能获取到这个状态,而 CompositionLocal 提供了不一样的方式,如下:

@Composable
fun CommonText(text: String) {
    Text(text = text, fontSize = LocalTextFontSize.current)
}

@Preview
@Composable
fun Greeting(modifier: Modifier = Modifier) {
    CompositionLocalProvider(LocalTextFontSize provides 50.sp) {
        Column {
            CommonText("Hello World")
        }
    }
}

private val LocalTextFontSize = compositionLocalOf { error("no value") }

此处定义了一个 LocalTextFontSize, 此后可以在任何CompositionLocalProvider第二个参数内的所有函数内通过 LocalTextFontSize.current 读取当前状态。等等,***.current,好像在哪见过?没错你一定用过 LocalContext.current 或者 LocalDensity.current 吧?它们都是 CompositionLocal

CompositionLocal 该怎么使用

@Composable
fun CompositionLocalProvider(value: ProvidedValue<*>, content: @Composable () -> Unit) {
    currentComposer.startProvider(value)
    content()
    currentComposer.endProvider()
}

正如上面的示例,CompositionLocal 需要在 CompositionLocalProvider 函数中使用才能读取最新的值

有两种方式创建 CompositionLocal

  • compositionLocalOf 这种方法在 CompositionLocal更新后,只会重组参数 content 中读取.current 的地方。
  • staticCompositionLocalOf 这种方法影响会大一点,它会重组整个 content,所以这种方式适合变化很少的场合。我们之前提到的LocalContext,LocalDensity 都是这种方式创建

使用 CompositionLocal 分为两步,第一步创建 CompositionLocal 实例,第二步在需要依赖此示例的函数之外用 CompositionLocalProvider包起来,如下:

@Composable
fun Greeting(modifier: Modifier = Modifier) {
    CompositionLocalProvider(LocalTextFontSize provides 50.sp) {// 第一层
        Column {
            CommonText("Hello World",LocalTextFontSize.current) // 50.sp
            CompositionLocalProvider(LocalTextFontSize.provides(80.sp)) { // 第二层
                CommonText("Hello World", LocalTextFontSize.current) // 80.sp
            }
        }
    }
}

没错,CompositionLocalProvider 是可以嵌套的,此处的 LocalTextFontSize.current 第一层之内所有依赖都是 50.sp,第二次则是80.sp。

LocalTextFontSize provides 50.sp就是赋值操作,有些人可能对 LocalTextFontSize provides 50.sp 这种写法比较陌生,这其实是 kotlin 的 infix 函数,其完全等同于 LocalTextFontSize.provides(50.sp),两者没有任何区别。

CompositionLocal 有什么用

从语法来说,在一个区域内如果你想不通过参数共享某些状态时,就可以使用 CompositionLocal,但从实际应用出发它还有有其特定使用的一些场景,不能滥用。通常我们 Composable 函数的参数主要是业务状态和 Modifier,这部分参数其实都不适合用 CompositionLocal。原因是 CompositionLocal 有可能出现在任何地方,如果用在业务上,则开发者可能要阅读源码后才能知道这个函数的作用,这对于开发来说是灾难。

所以 CompositionLocal 最适合的是哪种通用的配置场景,与业务无关。比如系统的LocalContextLocalDensity。比如主题切换、应用/平台配置、导航等等。

应用场景之屏幕适配

由于 Android 端的屏幕规格种类非常非常多,为了在各种尺寸上保持 UI 一致,则需要适配不同的屏幕。要做屏幕适配首先我们得了解Android 屏幕参数的一些概念:

像素(px) :像素(Pixel,缩写为 px)是屏幕显示的最小单位,是构成图像的基本单元

每英寸点数(dpi - Dots Per Inch) 是用来衡量屏幕像素密度的物理量,表示每英寸长度或宽度上所包含的像素点数。例如,一个屏幕的 dpi 为 320,表示在该屏幕的每英寸长度或宽度上,均匀分布着 320 个像素。

设备独立像素(dp - Density - independent Pixels) dp 是 Android 开发中为了解决像素在不同密度屏幕上显示不一致问题而引入的单位。它是一种基于屏幕物理密度的抽象单位,与屏幕的实际像素密度相关。1dp 在中密度(mdpi)屏幕上等于 1px,在其他密度屏幕上,dp 与 px 的换算关系会根据屏幕的 dpi 进行调整。具体换算公式为:px = dp * (dpi / 160)(mdpi 的 dpi 值约定为 160)。

密度(density) 定义:密度是与 dpi 相关的概念,它是屏幕 dpi 与基准 dpi(mdpi 的 160dpi)的比值。例如,一个屏幕的 dpi 为 320,那么它的密度为320/160 = 2。密度通常用于计算 dp 与 px 之间的换算关系。

假设现在有两块屏幕:

  • 屏幕1:宽度 1080px dpi 480 density=480/160=3 1080 / 3 = 360dp
  • 屏幕2:宽度 1440px dpi 560 density=480/160=3.5 1440 / 3.5 = 411dp

那么此时,180dp 在屏幕1的宽度占比是0.5,而在屏幕2 占比为 0.43,此时,两块屏幕便出现了偏差。我们知道 Composable 函数尺寸使用的单位是 dp,但实际绘制时,最终也会转换成 px,此处即是 180dp * density ,那我们让屏幕1 360dp = 屏幕2 360dp 就可以通过调整 density 来控制了。公式为 360 * density = 1440,则 density = 1440/360 = 4。此时计算 180dp = 180 * 4= 720px 便是0.5的比重了。

好了,接下来就是如何调整 density 的属性了,还记得我们上面提到的LocalDensity吗?没错,我们可以通过改变它,来达到我们的目的,上代码。

class MainActivity : ComponentActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeDemoTheme  {
                Column(
                    modifier = Modifier
                        .fillMaxSize(),
                    horizontalAlignment = Alignment.CenterHorizontally,
                    verticalArrangement = Arrangement.Center
                ) {
                    val displayMetrics = LocalContext.current.resources.displayMetrics
                    val fontScale = LocalDensity.current.fontScale
                    val density = displayMetrics.density
                    val widthPixels = displayMetrics.widthPixels
                    val widthDp = widthPixels / density
                    val display =
                        "density: $density\nwidthPixels: $widthPixels\nwidthDp: $widthDp"
                    Text(text = display)
                    Greeting()
                    CompositionLocalProvider(
                        LocalDensity provides Density(
                            density = widthPixels / 372.0f, // 此处的372 是需要适配的基准屏幕dp
                            fontScale = fontScale
                        )
                    ) {
                        Greeting()
                    }
                }
            }
        }
    }
}

@Composable
fun Greeting() {
    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Spacer(
            modifier = Modifier
                .size(
                    width = 186.dp,
                    height = 100.dp
                )
                .background(color = Color.Green)
                .align(alignment = Alignment.Start)
        )
        Spacer(
            modifier = Modifier
                .size(
                    width = 186.dp,
                    height = 100.dp
                )
                .background(color = Color.Cyan)
                .align(alignment = Alignment.End)
        )
    }
}

看实际效果

output.jpg

上下两端代码,上面没有被 CompositionLocalProvider包起来,则表现确实不是居中。

最后

CompositionLocal 常见的应用场景便是配置,配置主题、字体、尺寸,或平台配置,登录session 等等,这些操作通常都是针对整个应用或全局都需要配置。

大家可以尝试体验下,有任何问题欢迎留言沟通!