最近 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 一变,transform 的 onUpdate 就会像 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 里面生产 Statetransform:在 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 也能独立测试性状态逻辑,也算是一个友好解耦了。