第 5 章 · 响应式状态 — observable / observableList / 派生属性

在「响应式实验」里调字段值,看 UI 自动更新;玩 Todo demo 体验 observableList 全套用法

响应式三件套

武器用法典型场景
observable<T> var x by observable(0) 计数器、loading、表单字段、单值
observableListOf<T>() val items = observableListOf<Item>() 列表、Todo、消息流、瀑布流
observableMapOf<K,V>() val cache = observableMapOf<String, User>() 缓存、字典、动态字段集

核心心智模型

"声明 = 实现" —— 你只描述"UI 等于什么状态的什么样子",不操作 UI 本身。状态变化 → UI 自动更新。

// ❌ 命令式:手动同步 UI(容易漏更新)
btn.onClick = {
    count++
    text.text = "Count: $count"   // ← 你得记住每次都写
}

// ✅ 响应式:声明依赖(永远不会漏)
private var count by observable(0)
Text { attr { text("Count: $count") } }
View { event { click { count++ } } }

响应式背后的"魔法" · 3 步原理

1依赖收集(attr 块执行时)
框架设置全局 currentBlock = 当前 attr。每次读 observable 字段, getter 把 currentBlock 注册为该字段的"订阅者"。
2写时通知(observable.setValue)
字段被赋值时,setter 查询订阅者列表,把所有订阅了它的 attr 块加入"待重跑队列"。
3批量重跑(下一帧)
合并同一帧内的多次更新(自动去重),重跑队列里的 attr 块 → 拿到新属性 → 下发原生指令(setProp)→ 屏幕刷新。

简化版伪代码:observable 委托长这样

class ObservableDelegate<T>(initial: T) : ReadWriteProperty<Any?, T> {
    private var value = initial
    private val subscribers = mutableListOf<() -> Unit>()

    override fun getValue(thisRef, prop): T {
        // ★ STEP 1:依赖收集
        currentBlock?.let { subscribers.add(it) }
        return value
    }

    override fun setValue(thisRef, prop, newValue: T) {
        if (value != newValue) {
            value = newValue
            // ★ STEP 2 + 3:通知 + 加入待重跑队列
            subscribers.forEach { schedulerQueue.add(it) }
        }
    }
}

跟其他框架对比

框架语法追踪粒度
Kuiklyvar x by observable(0)attr 块
Vue 3const x = ref(0)render effect
Composevar x by mutableStateOf(0)@Composable 函数
SolidJSconst [x, setX] = createSignal(0)JSX 表达式

底层都是观察者模式 + 依赖追踪,差别在于追踪粒度和上下文管理方式。

响应式实验:调字段,看 UI 自动更新

右边面板的「读取依赖」是用蓝色高亮的字段。改任何一个 → 依赖它的 UI 自动刷新(高亮闪一下)。

① 状态控制台(observable 字段)

firstName = "Tom"
lastName = "Smith"
age = 20
↳ fullName (派生) = "Tom Smith"
↳ canDrink (派生) = true

② UI 渲染面板(attr 块)

Text { attr { text("${ctx.fullName}") } }
渲染结果: Tom Smith
Text { attr { text("年龄: ${ctx.age}") } }
渲染结果: 年龄: 20
Text { attr {
  text(if (ctx.canDrink) "可以喝酒 🍺" else "禁止饮酒 🚫")
} }
渲染结果: 可以喝酒 🍺
Text { attr { text("Hi ${ctx.firstName}!") } }
渲染结果: Hi Tom!

实验观察点

  • 改 firstName → ui1(fullName)+ ui4(Hi xx)同时更新,但 ui2/ui3 不动
  • 改 age → ui2 + ui3(canDrink 派生属性)同时更新
  • 派生属性 fullName / canDrink 没有显式声明依赖 —— Kotlin getter 自动追踪

Todo List 实战 · observableList + 派生属性

我的 Todo

还剩 0 项未完成

对应代码(节选)

data class TodoItem(val id: Int, val title: String, val done: Boolean)

private val todos = observableListOf<TodoItem>()
private var input by observable("")
private var filter by observable("all")

private val filteredTodos: List<TodoItem> get() = when (filter) {
    "active" -> todos.filter { !it.done }
    "done"   -> todos.filter { it.done }
    else     -> todos
}

private val activeCount: Int get() = todos.count { !it.done }

// 切换完成状态:copy() 整个 item
fun toggle(todo: TodoItem) {
    val idx = todos.indexOfFirst { it.id == todo.id }
    if (idx >= 0) todos[idx] = todo.copy(done = !todo.done)
}

5 个新手必踩的响应式坑

坑 1:用普通字段期待响应式

// ❌ 错误
private var count = 0    // 普通字段,改了 UI 不更新

// ✅ 正确
private var count by observable(0)

坑 2:用 mutableListOf 期待列表响应式

// ❌ 错误
private val items = mutableListOf<Item>()
items.add(...)    // UI 不刷新

// ✅ 正确
private val items = observableListOf<Item>()
items.add(...)

坑 3:在 attr 外读 observable

// ❌ 错误
val current = ctx.count          // 读取发生在 attr 闭包外
return {
    Text { attr { text("$current") } }  // current 不再变
}

// ✅ 正确
return {
    Text { attr { text("${ctx.count}") } }
}

坑 4:派生用 var 而不是 val + get()

// ❌ 错误:只算一次
private var fullName = "$firstName $lastName"

// ✅ 正确:每次访问重新计算
private val fullName: String get() = "$firstName $lastName"

坑 5:让全局 object 装太多 observable

// ❌ 反例
object Global {
    var anyTempState by observable(...)   // 全局响应式管理混乱
}

// ✅ 正确:长期数据用 Store,短期数据放 Pager
object UserStore {
    var loggedIn by observable(false)     // 长期、跨页面用
}

class HomePage : Pager() {
    private var loading by observable(false)  // 短期、本页用
}

速查表:能/不能驱动 UI

写法能驱动 UI?
var x by observable(0); x = 1
val list = observableListOf(); list.add(...)
list[0] = newItem (data class)
list[0].field = ... (字段非 observable)
list[0].field = ... (字段是 observable)
val x = ctx.count (在 attr 外读)
val fullName: String get() = ...
val fullName = ... (字段初始化)