Jetpack Compose 中的 CompositionLocal

news2024/12/29 17:15:13

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

但随着参数越来越多,组件也越来越多,并且有些数据还需要保持私有性,这时这种方式就会显得很繁琐臃肿,难以维护。 对于这些情况,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()
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/389709.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vscode插件推荐

文章目录前言一、vscode插件推荐&#xff1f;1、 Chinese (Simplified) (简体中文) Language Pack for Visual Studio Code2、Auto Close Tag3、Auto Import3、Error Lens4、vscode-icons5、ES7 React/Redux/React-Native snippets6、GitLens — Git supercharged7、JavaScript…

【FPGA】Verilog:时序电路应用 | 序列发生器 | 序列检测器

前言&#xff1a;本章内容主要是演示Vivado下利用Verilog语言进行电路设计、仿真、综合和下载 示例&#xff1a;序列发生器与序列检测器 ​ 功能特性&#xff1a; 采用 Xilinx Artix-7 XC7A35T芯片 配置方式&#xff1a;USB-JTAG/SPI Flash 高达100MHz 的内部时钟速度 存储器…

车道线检测CondLaneNet论文和源码解读

CondLaneNet: a Top-to-down Lane Detection Framework Based on Conditional Convolution Paper&#xff1a;https://arxiv.org/pdf/2105.05003.pdf code&#xff1a;GitHub - aliyun/conditional-lane-detection 论文解读&#xff1a; 一、摘要 这项工作作为车道线检测任…

js垃圾回收机制

内存的生命周期 ]S环境中分配的内存&#xff0c;一般有如下生命周期 1.内存分配:当我们声明变量、函数、对象的时候&#xff0c;系统会自动为他们分配内存 2.内存使用:即读写内存&#xff0c;也就是使用变量、函数等 3.内存回收: 使用完毕&#xff0c;由垃圾回收器自动回收不再…

MySQL实战解析底层---事务到底是隔离的还是不隔离的

目录 前言 “快照”在 MVCC 里是怎么工作的&#xff1f; 更新逻辑 前言 讲事务隔离级别的时候提到过&#xff0c;如果是可重复读隔离级别&#xff0c;事务 T 启动的时候会创建一个视图 read-view之后事务 T 执行期间&#xff0c;即使有其他事务修改了数据&#xff0c;事务 T…

​ ​​ ​IIS之FTP服务器 部署 (图文详细) 千锋

目录 概述 部署 步骤&#xff1a; 二重新配置FTP服务器 概述 1、File Transfor Protocol 文件传输协议 2、端口号&#xff1a; TCP 20/21 3、工作方式&#xff1a; 1)主动模式 2&#xff09;被动模式 部署 步骤&#xff1a; 配置静态IP 安装IIS-ftp软件 使用默认站…

学python的第三天---基础(1)

一、圆的面积print("A{:.4f}".format(s))二、两点间的距离![在这里插入图片描述](https://img-blog.csdnimg.cn/0d07c41d856d470796c79067b78c41b6.png)写法一&#xff1a;写法二&#xff1a;三、钞票和硬币写法一&#xff1a;写法二&#xff1a;四、倍数在python中实…

Spring Aware总结

概述 Spring中Aware到底是什么意思&#xff1f; 我们在看Spring源码的时候&#xff0c;经常可以看到xxxAwarexxx的身影&#xff0c;通常我会很疑惑&#xff0c;Aware到底是什么意思呢&#xff1f; 比如图片中这些包含Aware关键字的类或者接口。 我对下面3个类或接口进行了解…

【FMCW 02】测距

承接上篇博文 中频IF信号 &#xff0c;我们已经知道得到的中频IF信号的形式为&#xff1a; xIF(t)A′′cos⁡(2πKτt2πfoτ)x_{\tiny{IF}}(t) A^{\prime \prime} \cos(2\pi K\tau t2\pi f_o \tau ) xIF​(t)A′′cos(2πKτt2πfo​τ) 其中时延τ2dc\tau \frac{2d}{c}τc2…

【数据库】15分钟了解TiDB

由于目前的项目把mysql换成了TiDb&#xff0c;所以特意来了解下tidb。其实也不能说换&#xff0c;由于tidb和mysql几乎完全兼容&#xff0c;所以我们的程序没有任何改动就完成了数据库从mysql到TiDb的转换&#xff0c;TiDB 是一个分布式 NewSQL (SQL 、 NoSQL 和 NewSQL 的优缺…

C++之空间配置器

目录 一、C语言中的类型转换 二、C的类型转换 三、C强制类型转换 static_cast reinterpret_cast const_cast volatile关键字 dynamic_cast 什么情况下需要将父转成子呢&#xff1f; static_cast与dynamic_cast转换对比 四、空间配置器 什么是空间配置器 为什么需要…

raspberry pi播放音视频

文章目录目的QMediaPlayerGStreamerwhat is GStreamer体系框架优势omxplayerwhat is omxplayercommand Linekey bindings运行过程中错误ALSA目的 实现在树莓派下外接扬声器&#xff0c; 播放某段音频&#xff0c; 进行回音测试。 QMediaPlayer 首先我的安装是5.11版本。 优先…

【并发编程二十一:终章】c++20协程( co_yield、co_return、co_await )

【并发编程二十一】c20协程(co_yield、co_return、co_await &#xff09;一、协程分类1、控制机制划分2、有栈&#xff08;stackfull)/无栈&#xff08;stackless)划分二、c20协程三、co_yield1、demo2、相关知识点介绍四、co_return五、co_await一、协程分类 上一篇我们讲解了…

如何让AI帮你干活-娱乐(2)

背景&#xff1a;好容易完成朋友的任务&#xff0c;帮忙给小朋友绘画比赛生成一些创意参考图片。他给我个挑战更高的问题&#xff0c;是否可以帮他用AI生成一些视频。这个乍一听以现在AI技术根本不太可能完成。奈何他各种坚持&#xff0c;无奈被迫营业。苦脸接受了这个不可能完…

Java线程知识点总结

文章目录Java 线程基础线程简介什么是进程什么是线程进程和线程的区别创建线程ThreadRunnableCallable、Future、FutureTaskCallableFutureFutureTaskCallable Future FutureTask 示例线程基本用法线程休眠线程礼让终止线程守护线程线程通信wait/notify/notifyAlljoin管道线程…

MATLAB——数据及其运算

MATLAB数值数据数值数据类型的分类1&#xff0e;整型整型数据是不带小数的数&#xff0c;有带符号整数和无符号整数之分。表中列出了各种整型数据的取值范围和对应的转换函数。2&#xff0e;浮点型浮点型数据有单精度(single&#xff09;和双精度&#xff08;(double)之分&…

精粤X99M-PLUS D3+ E5-2696 v3电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网&#xff0c;转载需注明出处。硬件型号驱动情况主板精粤X99M-PLUS D3处理器E5-2696 v3已驱动内存64GB ECC DDR3 1866MHz (16GB*4)已驱动硬盘TOPMORE CAPRICORNUS NVMe 1TB已驱动显卡AMD Radeon™ RX 570 series (4GB/MSI)已驱动声卡Realtek ALC897 英特…

Android framework系列2 - Init进程

1、源码 入口&#xff1a;system/core/init/main.cpp2 流程图 https://note.youdao.com/s/EtnCswft 3、代码详解 主入口共三步&#xff0c;如流程图所示&#xff0c;我们主要看下最后一步 入口在init.cpp下&#xff0c;这个阶段主要来解析init.rc并执行此文件下的命令 看到…

多人协作|RecyclerView列表模块新架构设计

多人协作|RecyclerView列表模块新架构设计多人协作设计图新架构设计与实现设计背景与新需求新架构设计多人协作设计图 根据产品设计&#xff0c;将首页列表即将展示内容区域&#xff0c;以模块划分成多个。令团队开发成员分别承接不同模块进行开发&#xff0c;且互不影响任务开…

【Maven】P2 创建 Maven java/web 工程

Maven项目Maven 项目构建命令使用 Maven插件 创建 java/web 工程创建工程格式创建 java 工程创建 web 工程IDEA 中创建 Maven Java 工程IDEA 中创建 Maven web 工程Maven 项目构建命令 mvn compile # 编译 mvn clean # 清理 mvn test # 测试 mvn package # 打包 mvn …