Android 官方給 Compose 搞了一個不需要 UI 環境的 Composable

最近 Android 官方又新增了一個 androidx.transform 的 patch,它的作用是支援在沒有畫面的環境裡跑 Composable 邏輯,然後產出一個持續更新的 State<R>,對,你沒看錯,就是在沒有畫面的環境裡跑 Composable。

這裡的核心新增 API 主要是:

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

以及一個 Composable 版本:

@Composable
public fun <R> transform(
    defaultValue: R,
    onUpdate: @Composable () -> R
): State<R>

而它的本質實際上是:建立一個 headless Composition,不需要任何 UI 環境和 Layout 的 Compose Runtime 環境。

我們都知道,一般來說正常 Compose 是這樣「State - Composable - UI」這樣的變化,但是在 transform 裡就不一樣了,大概是:

State / Flow / Composable 依賴變化
  ↓
headless Composable 重新執行
  ↓
計算出一個新結果
  ↓
寫入 State<R>

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

@Composable
fun UserPage() {
    val user by viewModel.user.collectAsState()
    Text(user.name)
}

但是如果你想在非 UI 層,就需要使用 Compose 的狀態訂閱、remember、derived、collectAsState、CompositionLocal,或是一套 Composable 狀態計算邏輯

transform 的想法是,Compose Runtime 的回應式計算能力又不一定就需要 UI,比如:

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

val recomposer = Recomposer(finalContext)
val composition = Composition(UnitApplier, recomposer)

然後這裡的 UnitApplier 什麼都不插入、不移動、不刪除:

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 裡一般是這麼寫:

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

但是後續就可以寫成:

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」,比如一個庫可以暴露:

@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 也能獨立測試性狀態邏輯,也算是一個友善解耦了。


原文出處:https://juejin.cn/post/7648174967504404522


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝6   💬2  
340
🥈
我愛JS
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登