Android 官方给 Compose 搞了个不需要 UI 环境的 Composable

最近 Android 官方又新增了一个 androidx.transform 的 patch,它的作用是支持在没有界面的环境里跑 Composable 逻辑,然后产出一个持续更新的 State<R> ,对,你没看错,就是在没有界面的环境里跑 Composable。

这里的核心新增 API 主要是:

less 复制代码
public fun <R> transform(
    context: CoroutineContext = EmptyCoroutineContext,
    scope: CoroutineScope = CoroutineScope(context),
    defaultValue: R,
    onUpdate: @Composable () -> R,
): State<R>

以及一个 Composable 版本:

less 复制代码
@Composable
public fun <R> transform(
    defaultValue: R,
    onUpdate: @Composable () -> R
): State<R>

而它的本质实际上是:创建一个 headless Composition,不需要任何 UI 环境和 Layout 的 Compose Runtime 环境。

我们都知道,一般来说正常 Compose 是这样 「State - Composable - UI」 这样的变化,但是在 transform 里就不一样了,大概是:

css 复制代码
State / Flow / Composable 依赖变化
  ↓
headless Composable 重新执行
  ↓
计算出一个新结果
  ↓
写入 State<R>

比如以前 Compose 的 Composable 基本绑定在 UI 上,一般都是:

kotlin 复制代码
@Composable
fun UserPage() {
    val user by viewModel.user.collectAsState()
    Text(user.name)
}

但是如果你想在非 UI 层, 就需要使用 Compose 的状态订阅、remember、derived、collectAsState、CompositionLocal, 或者一套 Composable 状态计算逻辑

transform 的想法是,Compose Runtime 的响应式计算能力又不一定就需要 UI ,比如:

sql 复制代码
val profileState: State<ProfileUiModel> = transform(
    scope = viewModelScope,
    defaultValue = ProfileUiModel.Loading,
) {
    val user by userRepository.userFlow.collectAsState(initial = null)
    val settings by settingsRepository.settingsFlow.collectAsState(initial = null)
​
    when {
        user == null || settings == null -> ProfileUiModel.Loading
        else -> ProfileUiModel.Ready(user!!, settings!!)
    }
}

比如上面的代码, userFlow / settingsFlow 一变,transformonUpdate 就会像 Composable 一样被重新执行,最终产得到新的 State<ProfileUiModel>

所以官方这里主要做了几个处理,首先就是它创建了一个没有真实节点的 Composition

ini 复制代码
val recomposer = Recomposer(finalContext)
val composition = Composition(UnitApplier, recomposer)

然后这里的 UnitApplier 什么都不插入、不移动、不删除:

kotlin 复制代码
private object UnitApplier : AbstractApplier<Unit>(Unit) {
    override fun insertBottomUp(index: Int, instance: Unit) {}
    override fun insertTopDown(index: Int, instance: Unit) {}
    override fun move(from: Int, to: Int, count: Int) {}
    override fun remove(index: Int, count: Int) {}
    override fun onClear() {}
}

也就是说 Compose 还是会执行、remember、跟踪 State 读取、触发 recomposition,但不会产生 UI tree。

然后它是手动启动 recomposer.runRecomposeAndApplyChanges() ,而 runRecomposeAndApplyChanges() 会等待 invalidation,重新组合相关 composer,然后把变化应用到对应 Composition。

最后它还加了一个 GlobalSnapshotManager,用来监听全局 Snapshot 写入并发送 apply notifications。

也就说,registerGlobalWriteObserver 会监听 global state object 的第一次写入,Composition 用这个机制在 state 被修改后安排新的 composition, 然后 sendApplyNotifications() 发送 pending apply notifications。

所以这个 patch 其实就是自己搭了一个"无头 Compose 引擎"。

那这玩意可以干嘛?还是有些用处的,比如:

把多个 Flow、State、CompositionLocal 风格的数据组合成一个最终 UiState,以前 ViewModel 里一般是这么写:

scss 复制代码
val uiState = combine(userFlow, settingFlow, orderFlow) { user, setting, order ->
    UiState(user, setting, order)
}.stateIn(...)

但是后续就可以写成:

kotlin 复制代码
val uiState = transform(viewModelScope, UiState.Loading) {
    val user by userFlow.collectAsState(null)
    val setting by settingFlow.collectAsState(null)

    buildUiState(user, setting)
}

另外一些做 sdk 的场景,可以做 "Composable state producer" ,比如一个库可以暴露:

kotlin 复制代码
@Composable
fun rememberPermissionState(): PermissionState

以前这个只能在 UI Composition 里用,但是通过 transform 的能力,可能可以把这类 Composable state producer 搬到非 UI 运行环境里?理论上感觉也没什么问题。

而且最重要的是,这个模块放在 commonMain,依赖 androidx.compose.runtime:runtime:1.11.1 和 coroutines ,看起来是 Compose Runtime 的通用化能力

对比 derivedStateOf / produceState ,这个 transform 更底层,它自己创建一个 Composition,然后把一段 Composable 计算结果导出成 State ,简单粗暴对比的话:

  • derivedStateOf:在 Compose 里面做派生
  • produceState:在 Compose 里面生产 State
  • transform:在 Compose 外面开一个小 Compose runtime,然后生产 State

这其实和社区的 Molecule 库(Cash App 开源)原理也高度一致,Molecule 也是一个 headless Compose runtime 设计,只不过 API 上 Molecule 产出的是 StateFlow / Flow

所以,以前你想在 ViewModel 里做复杂的 derived state,通常要么手写 Flow 操作,要么引入 Molecule,或者把状态生产逻辑写在 Composable 里和 UI 强耦合。

而现在,可以复用 Compose Runtime 里的状态、remember、snapshot、recomposition、effect 等能力,然后还不需要耦合 Compose UI 场景,不再需要拉进 compose.ui 也能独立测试性状态逻辑,也算是一个友好解耦了。

相关推荐
dust_and_stars1 小时前
ubuntu24上安装chrome和edge浏览器
前端·chrome·edge
老王以为1 小时前
我的多屏编程工作流:从切窗口到空间锚定
前端
旺王雪饼 www2 小时前
localStorage 和 sessionStorage区别与联系
服务器·前端·javascript
道友可好2 小时前
Superpowers vs OpenSpec vs Spec Kit:该选哪个?
前端·人工智能·后端
এ慕ོ冬℘゜2 小时前
【双月日期范围选择器】博客(可直接交作业 / 上线)
前端·javascript·交互·jquery
珊瑚里的鱼2 小时前
C++的强制类型转换
android·开发语言·c++
问心无愧05132 小时前
ctf show web入门102
android·java·前端·笔记
前端尤雨西2 小时前
package.json 中版本号遵循什么原则
前端
用户81423861188412 小时前
CSS或JS实现逐帧动画方案
前端