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

相关推荐
子兮曰1 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰1 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万1 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝1 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
三十而立洋1 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
卡布鲁1 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试