JetPack Compose主题配色太少怎么办,来设计自己的颜色系统吧(下)

简介: JetPack Compose 正式版已经发布好几个月了,在这段时间里,除了业务相关需求之外,我也开始了 Compose 在实际项目中的落地实验,因为一旦要接入当前项目,那么遇到的问题其实远远大于新创建一个项目所需要的问题。

反思

我们上面说的都是使用方面的,那么你有没有想过?为什么官方自定义设计系统要使用 CompositionLocal 呢?

可能有新同学并没有使用过这个,为了便于更好理解,首先我们先搞清楚 CompositionLocal 是干什么的,先不说其通俗概念,我们简单用个小例子就能讲明白。

解构

在常见的开发场景中,我们很多时候,经常会将某个参数传递给其他方法,我们称之为显示传递。

切换一下场景,我们在 Compose 中,经常会给可组合函数传递参数,于是这个方式被 Google 学术化称为: 数据以参数的形式 向下流经 整个界面树传递给每个可组合函数 ,就如下述所示:

@Composable
fun A(message: String) {
  Column() {
     B(message)
  }
}
@Composable
fun B(message: String) {
    Text(text = message)
    C(message)
}
@Composable
fun C(message: String) {
    Text(text = message)
}

上述示例中有3个可组合函数,其中 A 需要接收一个 message 字符串,并且将其传递给 B ,而 B 同时又需要传递给 C ,类似于无限套娃一样,此时我们可能感觉还行吧,但是如果这种套娃出现 n 层呢,但是数据如果不止一个呢?此时将可能非常麻烦。

那么有没有其他方式解决呢?在 Compose 中,官方给出了标准答案,那就是 CompositionLocal

即使用 CompositionLocal 来完成 composable 树中的数据共享,并且 CompositionLocal 具有层级,它可以被限定在某个 composable 作为根节点的子树中,默认向下传递,同时子树中的某个 composable也可以对该 CompositionLocal 进行覆盖,然后这个新值就会在这个 composable 中继续向下传递。

composable 即可组合函数,简单理解就是使用了 @Composable 标注的方法。

实践

如下所示,我们对上述代码使用 CompositionLocal 进行改造:

val MessageContent = compositionLocalOf { "simple" }
@Composable
fun A(message: String) {
    Column() {
        // provides 相当于写入数据
        CompositionLocalProvider(MessageContent provides message) {
            B()
        }
    }
}
@Composable
fun B() {
    Text(text = MessageContent.current)
    CompositionLocalProvider(MessageContent provides "临时对值进行更改") {
        C()
    }
}
@Composable
fun C() {
    Text(text = MessageContent.current)
}

先定义了一个名为 MessageContentCompositionLocal ,其默认值为 “simple” , A 方法接收一个 message 字段,并且将其写入 MessageContent ,然后在 B 中,我们就可以获取到刚才方法 A 中写入到CompositionLocal的数据,而无需显示的在方法参数里增加字段。

同样,方法B也可以对这个 CompositionLocal 进行更改,这样 C 就会拿到另一个值。

并且当我们使用 CompositionLocal.current 来获取数据的时候,这个 current 会返回距离当前组件最近的那一个值,所以我们也可以利用其来做一些隐式分离的基础实现。

扩展

相应的,我们再说一下创建 CompositionLocal 的方式:

  • compositionLocalOf:在重组期间更改提供的值只会使读取其 current 值的内容无效。
  • staticCompositionLocalOf:与 compositionLocalOf 不同,Compose 不会跟踪staticCompositionLocalOf 的读取。更改该值会导致提供 CompositionLocal 的整个 contentlambda 被重组,而不仅仅是在组合中读取 current 值的位置。

总结

我们在上面大概了解了 CompositionLocal 的作用,试想一下,如果不用它,如果让我们自己去实现一个颜色系统,可能就会陷入我们最开始那种 随心所欲 的写法。

首先,那种写法可以用吗?当然可以用,但是实际中问题会很多,比如说主题的更改会导致而且不符合 Compose 的设计,而且如果我们可能有一部分业务在一定情况下,它可能一直保持一个主题色,那么此时怎么解决?

如果是方法1,可能此时会进入硬编码阶段,即使用复杂的业务逻辑去完成; 但如果是利用 CompositionLocal 呢?这个问题还会存在吗,只需要写入一个新的颜色配置即可,这个逻辑结束后再重新写入当前主题配置即可,还会存在复杂的逻辑缠绕情况吗?

这也就是为什么 Google 选择使用 CompositionLocal 去自定义颜色系统以及整个主题系统中可以供用户操纵的配置,即隐式,对使用者而言,无感知的就可以办到。

深入分析

看完了 CompositionLocal 的妙用与实际场景,我们不妨想一想,CompositionLocal 到底是怎么实现的,所谓知其然知其所以然。不深入源码往往很难理解具体实现,所以这部分的解析可能略显复杂。

CompositionLocal

言归正传,我们先看一眼源码,相应的注释与代码如下:

sealed class CompositionLocal<T> constructor(defaultFactory: () -> T) {
    // 默认值
    internal val defaultValueHolder = LazyValueHolder(defaultFactory)
    // 写入最新数据
    @Composable
    internal abstract fun provided(value: T): State<T>
    // 返回由最近的 CompositionLocalProvider 提供的值
    @OptIn(InternalComposeApi::class)
    inline val current: T
        @ReadOnlyComposable
        @Composable
        //直接追这里代码
        get() = currentComposer.consume(this)
}

我们知道获取数据的时候使用的 current ,那么直接追这里即可。

currentComposer.consume()

@InternalComposeApi
override fun <T> consume(key: CompositionLocal<T>): T =
    // currentCompositionLocalScope() 获取父可组合项提供的当前CompositionLocal范围map
    resolveCompositionLocal(key, currentCompositionLocalScope())

这段代码主要用于解析本地可组合项,从而获得数据。我们先看里面的 currentCompositionLocalScope()

currentCompositionLocalScope()

private fun currentCompositionLocalScope(): CompositionLocalMap {
    //如果可组合项当前正在插入并且有数据提供者
    if (inserting && hasProvider) {
        // 从插入表中取离当前 composable 最近的group(可以理解为直接去取index最近的currentGroup)
        var current = writer.parent
        // 如果这个group不是空的
        while (current > 0) {
            // 在插槽表中取出这个group的key与当前composable的key进行对比
            if (writer.groupKey(current) == compositionLocalMapKey &&
                writer.groupObjectKey(current) == compositionLocalMap
            ) {
                // 返回指定位置的 CompositionLocalMap
                return writer.groupAux(current) as CompositionLocalMap
            }
            // 死循环,不断的向上找,如果当前组里没有,就继续向上找,直到找到可以与当前匹配的
            current = writer.parent(current)
        }
    }
    //如果当前composable的slotTable内的数组不为空
    if (slotTable.groupsSize > 0) {
        //从当前插槽中取里当前离 composable 最近的graoup
        var current = reader.parent
        // 默认值为-1,如果存在,即意味着存在可组合项
        while (current > 0) {
            if (reader.groupKey(current) == compositionLocalMapKey &&
                reader.groupObjectKey(current) == compositionLocalMap
            ) {
                //从providerUpdates数组中获取当前CompositionLocalMap,插入见 - startProviders
                return providerUpdates[current]
                    ?: reader.groupAux(current) as CompositionLocalMap
            }
            current = reader.parent(current)
        }
    }
    //如果没找到,则返回父 composable 的 Provider
    return parentProvider
}

用于获取离当前 composable 最近的 CompositionLocalMap

resolveCompositionLocal()

private fun <T> resolveCompositionLocal(
    key: CompositionLocal<T>,
    scope: CompositionLocalMap
): T = if (scope.contains(key)) {
    // 如果当前父CompositionLocalMap里包含了当前local,直接从map中取
    scope.getValueOf(key)
} else {
    // 否则也就意味着其目前就是最顶层,没有父local,直接使用默认值
    key.defaultValueHolder.value
}

使用当前 CompositionLocal 当做 key ,然后去离当前最近的 CompositionLocalMap 里查找对应的 value ,如果有直接返回,否则使用 CompositionLocal 的默认值。

总结

所以当我们使用 CompositionLocal.current 获取数据时,内部其实是通过 currentCompositionLocalScope() 获取父CompositionLocalMap,注意,这里为什么是map呢? 因为这里获取的是当前父可组合函数下 所有CompositionLocal,所以源码里的consume 方法参数里需要传递当前 CompositionLocal 进去,判断当前我们要取的 local 有没有存在在其中,如果有,则直接取,否则使用默认值。

那问题来了,我们的 CompositionLocal是怎么被可组合树保存的呢? 带着这个问题,我们继续往下深究。

CompositionLocalProvider

要想知道 CompositionLocal 是如何被可组合树保存,必须从下面开始看起。

fun CompositionLocalProvider(vararg values: ProvidedValue<*>, content: @Composable () -> Unit) {
    currentComposer.startProviders(values)
    content()
    currentComposer.endProviders()
}

这里的 currentComposer 又是什么?而且为什么先start,后end呢?

我们点进去 currentComposer 看看。

/**
 * Composer is the interface that is targeted by the Compose Kotlin compiler plugin and used by
 * code generation helpers. It is highly recommended that direct calls these be avoided as the
 * runtime assumes that the calls are generated by the compiler and contain only a minimum amount
 * of state validation.
 */
interface Composer
     👇
internal class ComposerImpl : Composer

可以看到在 Composer 的定义里, Composer 是为 Compose kotlin 编译器插件 提供的,google强烈不建议我们自己手动调用,也就是说,这里的 startend 其实就是两个标记,编译器会自己调用,或者说就是为了方便编译器。接着我们去看 startProviders()

startProviders

@InternalComposeApi
override fun startProviders(values: Array<out ProvidedValue<*>>) {
    //获得当前Composable下的CompositionLocal-Map
    val parentScope = currentCompositionLocalScope()
    ...
    val currentProviders = invokeComposableForResult(this) {
        compositionLocalMapOf(values, parentScope)
    }
    val providers: CompositionLocalMap
    val invalid: Boolean
    //如果可组合项正在插入树中或者其是第一次被调用,则为true
    if (inserting) {
        //更新当前的CompositionLocalMap
        providers = updateProviderMapGroup(parentScope, currentProviders)
        invalid = false
        hasProvider = true
    } else {
        //如果当前可组合项不可以跳过(即发生了改变)或者providers不相同,则更新当前Composable的group
        if (!skipping || oldValues != currentProviders) {
            providers = updateProviderMapGroup(parentScope, currentProviders)
            invalid = providers != oldScope
        } else {
            //否则跳过当前更新
            skipGroup()
            providers = oldScope
            invalid = false
        }
    }
    //如果本次重组无效且没有正在插入,更新当前group的 CompositionLocalMap
    if (invalid && !inserting) {
        providerUpdates[reader.currentGroup] = providers
    }
    // 将当前的操作push到栈中,后续还要再弹出
    providersInvalidStack.push(providersInvalid.asInt())
    ...
    // 将providers数据写入group,最终会写入到SlotTable-slots(插槽的缓冲区)数组中
    start(compositionLocalMapKey, compositionLocalMap, false, providers)
}

这个方法用于启动数据提供者,如果看过 compose 的设计原理就会知道这里其实就相当于 group 的一个开始标记,其内部的内容主要是先获取离当前 composable 最近的 CompositionLocalMap ,compositionLocalMapOf() 将当前传递进来的 value 更新到对应的 CompositionLocalMap 中并返回,然后将这个map再更新到当前group中。

相应的我们说了,这是一个开始标记,自然也存在一个终止标记,即 end,在上述源码中,我们可以知道,其就是 endProviders():

endProviders

override fun endProviders() {
    endGroup()
    endGroup()
    // 将当前的操作出栈
    providersInvalid = providersInvalidStack.pop().asBool()
}

总结

当我们使用 CompositionLocalProvider 将数据绑定到 CompositionLocal 时,其内部会将其保存到距离当前 composable 最近的 CompositionLocalMap 里面,当我们后续想要使用的时候,CompositionLocal.current 读取数据时,其会去查找对应的 CompositionLocalMap ,并且以我们的 CompositionLocalkey,如果存在则返回,否则使用默认值返回。

碎碎念

本文其实并不是特别难的问题,但却是 Compose 在实际中会遇到的一个问题,解决这个问题很简单,但理解背后的设计却是更重要的,希望通过本文,大家可以更好的理解 CompositionLocal 在实际中的场景以及设计理念。当然了解具体的源码其实还是需要了解 Compose 的基础设计,这点参考文章下方贴出的Android开发者链接即可。后续我将继续深追 Compose 在实际应用中需要解决的问题并且分析思路,如果有错误,希望不吝赐教。

目录
相关文章
|
2月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
58 4
|
4月前
|
存储 移动开发 Android开发
使用kotlin Jetpack Compose框架开发安卓app, webview中h5如何访问手机存储上传文件
在Kotlin和Jetpack Compose中,集成WebView以支持HTML5页面访问手机存储及上传音频文件涉及关键步骤:1) 添加`READ_EXTERNAL_STORAGE`和`WRITE_EXTERNAL_STORAGE`权限,考虑Android 11的分区存储;2) 配置WebView允许JavaScript和文件访问,启用`javaScriptEnabled`、`allowFileAccess`等设置;3) HTML5页面使用`<input type="file">`让用户选择文件,利用File API;
|
5月前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
5月前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
5月前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
5月前
|
监控 Android开发 数据安全/隐私保护
安卓kotlin JetPack Compose 实现摄像头监控画面变化并录制视频
在这个示例中,开发者正在使用Kotlin和Jetpack Compose构建一个Android应用程序,该程序 能够通过手机后置主摄像头录制视频、检测画面差异、实时预览并将视频上传至FTP服务器的Android应用
|
5月前
深入了解 Jetpack Compose 中的 Modifier
深入了解 Jetpack Compose 中的 Modifier
|
5月前
|
Android开发
Jetpack Compose: Hello Android
Jetpack Compose: Hello Android
|
5月前
|
安全 网络安全 API
kotlin安卓开发JetPack Compose 如何使用webview 打开网页时给webview注入cookie
在Jetpack Compose中使用WebView需借助AndroidView。要注入Cookie,首先在`build.gradle`添加WebView依赖,如`androidx.webkit:webkit:1.4.0`。接着创建自定义`ComposableWebView`,通过`CookieManager`设置接受第三方Cookie并注入Cookie字符串。最后在Compose界面使用这个自定义组件加载URL。注意Android 9及以上版本可能需要在网络安全配置中允许第三方Cookie。
|
5月前
|
Android开发 Kotlin
kotlin安卓开发【Jetpack Compose】:封装SnackBarUtil工具类方便使用
GPT-4o 是一个非常智能的模型,比当前的通义千问最新版本在能力上有显著提升。作者让GPT开发一段代码,功能为在 Kotlin 中使用 Jetpack Compose 框架封装一个 Snackbar 工具类,方便调用