Jetpack Compose 中的 CompositionLocal

Posted 川峰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack Compose 中的 CompositionLocal相关的知识,希望对你有一定的参考价值。

要在可组合函数之间共享数据时,可以通过参数传递显式地调用,这通常是最简单和最好的方式。

但随着参数越来越多,组件也越来越多,并且有些数据还需要保持私有性,这时这种方式就会显得很繁琐臃肿,难以维护。 对于这些情况,Compose 提供了CompositionLocals来作为一种隐式方式在组合函数之间传递数据。说白了就是在 Composable 树中提供的一种共享数据的方式(例如主题配置)。

MaterialTheme 是如何实现的

需要注意的是,此时传入的 content 参数其实是声明在 Theme 中的自定义布局系统,其类型是一个带有 Composable 注解的 lambda

我们所关注的 colorsremember 修饰后赋值为 rememberedColors。如果 MaterialTheme 这个 Composable 发生 recompose 时便会检查 colors 是否发生了改变从而决定更新。

接下来使用 CompositionLocalProvider 方法,通过中缀表达式 providersrememberedColors 提供给了 LocalColors。让我们回到自己的 Composable 中,看看我们是如何通过 MaterialTheme 获取到当前主题配色的。

@Composable
fun MyCard(text: String) 
    Box(
        modifier = Modifier
        .fillMaxWidth()
        .height(100.dp)
    ) 
        Text(text = text, color = MaterialTheme.colors.primary)
    

这里使用的 MaterialTheme.colors 实际上被定义在 MaterialTheme object 单例对象中:

object MaterialTheme  

    val colors: Colors
        @Composable
        @ReadOnlyComposable
        get() = LocalColors.current 
        
    val typography: Typography
        @Composable
        @ReadOnlyComposable
        get() = LocalTypography.current 
        
    val shapes: Shapes
        @Composable
        @ReadOnlyComposable
        get() = LocalShapes.current

MaterialTheme 类单例的 colors 属性,间接使用了 LocalColors

总的来说,我们在自定义 Theme 使用的是 MaterialTheme 函数为 LocalColors 赋值,而在获取时使用的是 MaterialTheme 类单例,间接从 LocalColors 中获取到值。那 LocalColors 又是什么呢?

internal val LocalColors = staticCompositionLocalOf  lightColors() 

实际上它是一个 CompositionLocal,其初始值是 lightColors() 返回的 colors 配置。

MaterialTheme 方法中通过 CompositionLocalProvider 方法为我们的自定义视图 Composable 提供了一些 CompositionLocal,包含了所有的主题配置信息。

CompositionLocal 介绍

CompositionLocals本质上是分层的。当CompositionLocal的值需要被限定于组合的特定子层次结构时,它们是有意义的。

CompositionLocals 可以被限定在以某个 Composable 作为根结点的子树中,其默认会向下传递的,当然当前子树中的某个 Composable 可以对该 CompositionLocals 进行覆盖,从而使得新值会在这个 Composable 中继续向下传递。

总的来说,CompositionLocal 它有以下特性:

  • 具备函数穿透功能的局部变量,不需要显示的传递的函数参数,
  • 多用于提供:上下文/主题 等,方便透传
  • 它的着重点在于提供某种上下文,如果是某个函数需要某个参数多数情况应该使用函数参数直接传

要使用 CompositionLocal 必须创建一个 CompositionLocal 实例,消费者可以静态地引用该实例CompositionLocal 实例本身不保存任何数据,可以将其视为在组合树中向下传递的数据的类型安全标识符。

要创建一个 CompositionLocal 的实例通过调用 compositionLocalOf 方法来实现:

import androidx.compose.runtime.compositionLocalOf

var LocalString = compositionLocalOf  "Jetpack Compose" 

compositionLocalOf 后面的 lambda 中返回的是一个默认值,这个 lambda 其实是一个工厂函数,其中还可以配置没有提供值的情况下的警告信息,以提醒使用者:

val LocalBackground = compositionLocalOf<Color>  error("LocalBackground没有提供值") 

compositionLocalOf的返回值是一个 ProvidableCompositionLocal 对象(它继承了 CompositionLocal 类)。

然后,在 Composable 树的某个地方,我们可以使用 CompositionLocalProvider 方法为 CompositionLocal 提供一个值。通常情况下位于 Composable 树的根部,但也可以位于任何位置,还可以在多个位置使用,以覆盖子树能够获取到的值。

val LocalUserName = compositionLocalOf<String>  "" 

class CompositionLocalProviderActivity : ComponentActivity() 

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContent 
            CompositionLocalProvider(LocalUserName provides "Jetpack Compose") 
                User()
            
        
    


@Composable
fun User() 
    Column 
        Text(text = LocalUserName.current) // 通过current当前用户名
    

比如可以通过这种方式来为 Composable 提供当前 Activity 的实例:

val LocalActivity = compositionLocalOf<Activity>  error("LocalActivity没有提供值") 

class CompositionLocalProviderActivity : ComponentActivity() 

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContent 
            CompositionLocalProvider(LocalActivity provides this) 
                User() // 范围内都可以使用LocalActivity变量访问当前Activity对象
            
        
    

其中 providesProvidableCompositionLocal 抽象类中定义的一个 infix 中缀表达式:

abstract class ProvidableCompositionLocal<T> internal constructor(defaultFactory: () -> T) : CompositionLocal<T> (defaultFactory)  
    infix fun provides(value: T) = ProvidedValue(this, value, true)

以下示例是在任意子 Composable 中嵌套使用 CompositionLocalProvider

// Top Level   
val LocalBackground = compositionLocalOf  Color.Magenta 

class CompositionLocalProviderActivity : ComponentActivity() 

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContent   
             Column 
                SomethingtWithBackground() // 默认背景色
                CompositionLocalProvider(LocalBackground provides Color.Red ) 
                    SomethingtWithBackground() // 红色
                    CompositionLocalProvider(LocalBackground provides Color.Green) 
                        SomethingtWithBackground() // 绿色
                        CompositionLocalProvider(LocalBackground provides Color.Blue) 
                            SomethingtWithBackground() // 蓝色
                        
                        SomethingtWithBackground() // 绿色
                    
                    SomethingtWithBackground() // 红色
                
                SomethingtWithBackground() // 默认背景色
            
        
    

@Composable
fun SomethingtWithBackground() 
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .background(LocalBackground.current)
            .padding(20.dp)
    ) 
        Text(text = "我是有背景的人!", color = Color.White, fontSize = 22.sp)
    

可以看到在不同嵌套层级范围内相同的 CompositionLocal 获取到的值可以不一样,也就是说可以在某个子树范围中被覆盖,但是出了这个子树范围后,还是原来的值。

注意,CompositionLocalProvider 中的第一个参数是一个可变参数,也就是可以提供多个值,例如:

// Top Level   
val LocalUserName = compositionLocalOf  "张三" 

Column 
     SomethingtWithBackground()
     CompositionLocalProvider(
         LocalBackground provides Color.Red,
         LocalUserName provides "李四"
     ) 
         SomethingtWithBackground()
         CompositionLocalProvider(LocalBackground provides Color.Green) 
             SomethingtWithBackground()
             CompositionLocalProvider(
                 LocalBackground provides Color.Blue,
                 LocalUserName provides "小明"
             ) 
                 SomethingtWithBackground()
             
             SomethingtWithBackground()
         
         SomethingtWithBackground()
     
     SomethingtWithBackground()
 
 
@Composable
fun SomethingtWithBackground() 
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .background(LocalBackground.current)
            .padding(20.dp)
    ) 
        Text(text = LocalUserName.current, color = Color.White, fontSize = 22.sp)
    
   

compositionLocalOf 与 staticCompositionLocalOf 区别

当需要创建 CompositionLocal 时,除了可以使用 compositionLocalOf 方法,在 Compose 中还有一个 staticCompositionLocalOf 方法,那么这两者有什么区别呢?

  • 当我们选择使用 staticCompositionLocalOf 时,实际上创建了个StaticProvidableCompositionLocal 实例,与compositionLocalOf不同,编译器不会跟踪staticCompositionLocalOf的读取,一旦它的值改变时,它所提供范围内的所有内容都会重组,而不仅仅是组合中使用局部值的地方。也就是说它不会进行智能重组,每次改变值时都是强制所有人重组。

  • 如果我们选择使用 compositionLocalOf,实际上创建了个 DynamicProvidableCompositionLocal 实例,当其所提供的值改变时,仅会导致它所提供范围内的依赖当前 CompositionLocal 的那个 Composable 触发重组。

下面是一个分别使用 staticCompositionLocalOfcompositionLocalOf 创建 CompositionLocal 的对照效果示例:

val LocalCounter = compositionLocalOf  0 
val LocalCounterStatic = staticCompositionLocalOf  0 

@Composable
fun CompositionLocalExample() 
    var counter by remember  mutableStateOf(0) 
    Column(horizontalAlignment = Alignment.CenterHorizontally) 
        Button(onClick =  counter++ ) 
            Text(text = "change LocalCounter")
        
        CompositionLocalProvider(LocalCounter provides counter) 
            ThreeBox("CompositionLocal") 
                Text("counter: $LocalCounter.current", color = Color.White,
                    modifier = Modifier.background(getRandomColor()).padding(5.dp))
            
        
        Spacer(Modifier.height(10.dp))
        CompositionLocalProvider(LocalCounterStatic provides counter) 
            ThreeBox("StaticCompositionLocal") 
                Text("counter: $LocalCounterStatic.current", color = Color.White,
                    modifier = Modifier.background(getRandomColor()).padding(5.dp))
            
        
    


@Composable
fun ThreeBox(type: String, content: @Composable () -> Unit) 
    Box(
        modifier = Modifier
            .size(300.dp)
            .background(getRandomColor())
    ) 
        Box(modifier = Modifier
            .padding(50.dp)
            .fillMaxSize()
            .background(getRandomColor())
        ) 
            Box(modifier = Modifier
                .padding(50.dp)
                .fillMaxSize()
                .background(getRandomColor()),
                contentAlignment = Alignment.Center
            ) 
                content()
            
        
        Text(type, color = Color.White, fontSize = 18.sp,
            modifier = Modifier.padding(top=15.dp).align(Alignment.TopCenter))
    

运行效果:

在上面代码中,三个 Box 及最里面的 Text 组件都设置了随机背景色,这样一旦它们发生重组,我们就能观察到。可以看到,点击修改 counter 状态值时,staticCompositionLocalOfcompositionLocalOf 创建的本地共享变量有着明显不同的表现:前者强制其范围内的所有组件重组(不管是否从CompositionLocal中读取值),后者仅重组了从其读取值的组件。

Composable 中的常见 CompositionLocals 创建流程分析

通过前面部分的对比示例可见,使用 staticCompositionLocalOf 主要目的是为了某种全局性的配置,例如开头分析的 MaterialTheme 中的主题颜色都是通过 staticCompositionLocalOf 实现的,因为这些主题是整个应用中共有的属性,需要做到一改全改的效果。此外,我们常用的 LocalContext 等带Localxxx前缀的实现方式都是 staticCompositionLocalOf


当我们在 ActivityFragmentComposeView 调用 setContent 的时候,会创建 Composition (组合)对象,在创建该对象时会先创建一个 AndroidComposeView 来作为 LayoutNode 组合树的跟节点的 Owner,然后这个 AndroidComposeView 被附加到 Android 的 View 视图层次结构中,以便可以根据需要执行 invalidate 操作(作为与Compose 视图树的集成点)。

然后会创建一个 WrappedComposition 对象,WrappedComposition 是一个装饰器,它知道如何将 Composition 链接到一个 AndroidComposeView,以便将其直接连接到 Android View 系统。它启动受控效果来跟踪诸如键盘可见性更改或 accessibility 之类的内容,并将关于 Android Context 的信息以 CompositionLocals 的形式传输暴露给 Composition

WrappedComposition 设置这些 CompositionLocals 具体是通过调用 ProvideAndroidCompositionLocals 方法:

然后在 ProvideAndroidCompositionLocals 方法中,我们就能够看到例如:Context本身、配置、当前LifecycleOwner、当前savedStateRegistryOwnerOwnerView等等,这些是如何被 CompositionLocalProvider 提供给 LocalXXX类的:

@Composable
@OptIn(ExperimentalComposeUiApi::class)
internal fun ProvideAndroidCompositionLocals(owner: AndroidComposeView, content: @Composable () -> Unit) 
    val view = owner
    val context = view.context
    var configuration by remember 
        mutableStateOf(
            context.resources.configuration,
            neverEqualPolicy()
        )
    

    owner.configurationChangeObserver =  configuration = it 

    val uriHandler = remember  AndroidUriHandler(context) 
    val viewTreeOwners = owner.viewTreeOwners ?: throw IllegalStateException(
        "Called when the ViewTreeOwnersAvailability is not yet in Available state"
    )

    val saveableStateRegistry = remember 
        DisposableSaveableStateRegistry(view, viewTreeOwners.savedStateRegistryOwner)
    
    DisposableEffect(Unit) 
        onDispose 
            saveableStateRegistry.dispose()
        
    

    val imageVectorCache = obtainImageVectorCache(context, configuration)
    CompositionLocalProvider(
        LocalConfiguration provides configuration,
        LocalContext provides context,
        LocalLifecycleOwner provides viewTreeOwners.lifecycleOwner,
        LocalSavedStateRegistryOwner provides viewTreeOwners.savedStateRegistryOwner,
        LocalSaveableStateRegistry provides saveableStateRegistry,
        LocalView provides owner.view,
        LocalImageVectorCache provides imageVectorCache
    ) 
        ProvideCommonCompositionLocals(
            owner = owner,
            uriHandler = uriHandler,
            content = content
        )
    

这就是为何这些内容在我们所有的 Composable 函数当中是隐式可用的。

CompositionLocal 的替代方案

某些场景下,CompositionLocal 可能不合适,甚至过度使用。此时可采取如下方案:

  • 显式参数:在极简单逻辑情况,应尽量使用显示参数传递,且只传递有效参数,避免造成参数过多。

  • 控制反转:另一种避免参数过多或无效参数的方法就是控制反转。一些逻辑可以不在子级页面进行,而应该转移到父级页面来进行。

    例如下面的例子中,在子级页面使用了 viewModel 调用 loadData

@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")
    

在某些场景下控制反转可以将子级脱离出来,达到高度复用,可以更灵活。同样,可以用 lambda 表达式来实现:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) 
    // ...
    ReusablePartOfTheScreen(
        content = 
            Button(
                onClick = 
                    myViewModel.loadData()
                
            ) 
                Text("Confirm")
            
        
    )


@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit) 
    Column 
        // ...
        content()
    

以上是关于Jetpack Compose 中的 CompositionLocal的主要内容,如果未能解决你的问题,请参考以下文章

Jetpack Compose中的导航路由

JetPack Compose 基础(3)Compose 中的主题

Jetpack Compose 中的作用域状态

Jetpack Compose中的手势操作

Jetpack Compose中的Accompanist

Jetpack Compose中的Canvas