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 也能独立测试性状态逻辑,也算是一个友好解耦了。

相关推荐
用户938515635078 小时前
从零在浏览器里跑 DeepSeek-R1:WebGPU + Transformer.js 全链路实战
前端·设计模式·typescript
CodeSheep8 小时前
稚晖君公司人事大变动,来了!
前端·后端·程序员
鱼樱前端9 小时前
用 AI 做内容变收入
前端·人工智能·ai编程
浮生望9 小时前
React + TypeScript 组件设计进阶:从类型约束到无状态组件的三次进化
前端
90后的晨仔9 小时前
Mac 开发 uni-app 终极方案:让安卓模拟器彻底脱离 Android Studio 独立运行
前端·vue.js
90后的晨仔9 小时前
别再搞混了!uni-app 中 node_modules 和 uni_modules 到底是什么关系?
前端
kyriewen10 小时前
面试官问"这段逻辑为什么这样写"——我才发现,这半年我写的代码,我自己都解释不了
前端·javascript·面试
YB137510 小时前
jetpack compose 副作用 SideEffect
android·kotlin
谢小飞10 小时前
大文件上传很难?学会这招,GB文件秒传不是梦
前端·node.js
程序员黑豆11 小时前
Windows 系统 Java 环境变量配置全攻略:解决“不是内部或外部命令”
java·前端·ai编程