第 3 章 · 核心概念 — Pager / ViewBuilder / attr-event 三件套

点击各 Tab 切换主题 · 拖动控件实时观察 Flexbox 变化 · 点击红心体验响应式 UI

「三件套」心智模型

所有 Kuikly 页面都由这三个概念组合而成,理解它们 = 理解 80% 的 Kuikly。

1. Pager

页面基类,整张屏幕的根。持有状态、生命周期、UI 描述。

2. ViewBuilder

带 receiver 的 Kotlin 闭包,描述 UI 长什么样、组件怎么嵌套。

3. attr / event

每个组件的样式 + 交互配置块。attr 写"长啥样",event 写"响应啥"。

最小化代码示意

@Page("MinimalPage")
class MinimalPage : Pager() {                      // ← Pager 基类
    private var count by observable(0)            // ← 状态

    override fun body(): ViewBuilder {            // ← ViewBuilder
        val ctx = this
        return {
            View {
                attr {                            // ← attr:样式
                    allCenter()
                    backgroundColor(Color.WHITE)
                }
                event {                           // ← event:交互
                    click { ctx.count++ }
                }
                Text { attr { text("${ctx.count}") } }
            }
        }
    }
}

Pager 生命周期 · 时间轴

按时间顺序触发,每个阶段适合做不同的事情:

created() 页面对象创建

pageData、初始化字段、订阅长期数据流。UI 还没渲染,不能操作 view。

② body() 第一次调用

引擎拿到 ViewBuilder,构建 BuildTree → RenderTree → 下发原生指令。后续 attr 字段变化也会重跑 attr 块。

pageDidAppear() 页面显示在屏幕

发 PV 埋点、起入场动画、启动定时器、申请权限。

页面运行中

用户点击、滑动、网络回调 → 改 observable → attr 块重跑 → UI 自动更新。

pageWillDestroy() 页面即将销毁

取消所有正在进行的网络请求、解绑监听、停止定时器,避免内存泄漏。

常见错误:在错误的时机做事

做了什么放错了哪正确位置
调 pageData.params放 body() 里created()
发埋点放 created() 里(页面还没显示)pageDidAppear()
取消 timer没写(页面销毁后还在跑)pageWillDestroy()
fetch user info写在 attr {} 里created() 或 pageDidAppear()

ViewBuilder 是「构造 UI 树的指令」

下面这段 DSL 代码:

View {
    attr { allCenter() }
    Text  { attr { text("Hello") } }
    Image { attr { src("avatar.png") } }
    View {
        attr { flexDirectionRow() }
        Text { attr { text("A") } }
        Text { attr { text("B") } }
    }
}

对应生成的 BuildTree

View (root, allCenter)
├── Text "Hello"
├── Image avatar.png
└── View (row)
├── Text "A"
└── Text "B"

关键认知

  • ViewBuilder 不是 UI 树,是"构造 UI 树的函数"
  • 每次执行才会生成 BuildTree 节点
  • 可以抽成函数复用(类似 React 函数组件 / Compose @Composable)
  • 组件嵌套方式:组件名 { attr{} event{} 子组件 }

attr vs event:职责对照

每个组件后面都有这两个块,分得很清楚 —— attr 写"是什么",event 写"做什么"

attr {} ─ 样式 / 布局 / 数据

  • backgroundColor(Color.RED)
  • fontSize(20f)
  • padding(12f)
  • flexDirectionRow()
  • text("hello")
  • src("avatar.png")
  • borderRadius(8f)

event {} ─ 交互 / 监听

  • click { e -> ... }
  • longPress { e -> ... }
  • doubleClick { e -> ... }
  • pan { e -> ... }
  • scrollEnd { e -> ... }
  • onLoadFinish { e -> ... }
  • onTextChange { v -> ... }

为什么要分开?(面试高频)

原因解释
职责清晰 "长啥样"和"响应啥"逻辑分离,DSL 一眼能扫
响应式追踪精度 attr 单独成块,引擎能精确知道哪些 observable 被它读了,更新只刷该属性
渲染指令拆分 attr → setProp 指令;event → addEventListener 指令;分开方便批量合并
执行频率不同 attr 会被多次调用(每次 observable 变化);event 只在挂载时注册一次

⚠️ 高频错误:把副作用写到 attr 里

// ❌ 错:attr 块会被多次执行
attr {
    text("${ctx.count}")
    println("rendering")          // ← 重复打印
    fetchUser()                   // ← 重复请求
}

// ✅ 对:副作用放生命周期或 event
override fun pageDidAppear() {
    fetchUser()
}

Flexbox 实时演示

调整下面的下拉菜单,看看 flex 各属性如何影响布局:

A
B
C
D

对应 Kuikly DSL

View {
    attr {
        flexDirectionRow()
        justifyContentFlexStart()
        alignItemsStretch()
        flexWrapNowrap()
    }
    Text { attr { text("A") } }
    Text { attr { text("B") } }
    Text { attr { text("C") } }
    Text { attr { text("D") } }
}

常见 Flexbox 速查表

需求Kuikly API
横向排列flexDirectionRow()
子组件全部居中allCenter()
两端对齐 / 中间均分justifyContentSpaceBetween()
子组件占满剩余空间子组件 attr { flex(1f) }
换行排列flexWrapWrap()

「用户卡片页」实时演示

这是 3.7 节代码的浏览器渲染版。点击红心 / 关注按钮观察响应式 UI 更新效果。

张三
Kuikly 学习中

对应的 Kotlin DSL(节选)

View {                                  // 卡片容器
    attr { backgroundColor(Color.WHITE); borderRadius(12f); padding(16f) }

    View {                              // 上半部分:头像 + 名字
        attr { flexDirectionRow(); alignItemsCenter() }
        Image { attr { src(...); size(48f, 48f); borderRadius(24f) } }
        View {
            attr { marginLeft(12f); flex(1f) }
            Text { attr { text("张三"); fontSize(16f); fontWeightBold() } }
            Text { attr { text("Kuikly 学习中"); fontSize(12f); marginTop(4f) } }
        }
    }

    View {                              // 下半部分:操作按钮
        attr { flexDirectionRow(); marginTop(16f); justifyContentSpaceBetween() }

        View {                          // 点赞按钮(响应式)
            attr {
                paddingHorizontal(16f); height(36f); borderRadius(18f)
                backgroundColor(if (ctx.liked) Color(0xFFFFEBEEL) else Color(0xFFF5F5F7L))
            }
            event { click { ctx.liked = !ctx.liked; ctx.likeCount += if (ctx.liked) 1 else -1 } }
            Text { attr { text(if (ctx.liked) "♥" else "♡") } }
            Text { attr { text("${ctx.likeCount}") } }
        }

        View {                          // 关注按钮
            attr { allCenter(); backgroundColor(Color(0xFF1976D2L)); height(36f) }
            event { click { /* TODO 关注 */ } }
            Text { attr { text("+ 关注"); color(Color.WHITE) } }
        }
    }
}

响应式 UI 工作原理(脑图版)

点击「♡」按钮
触发 event { click { ctx.liked = !ctx.liked } }
Kuikly 检测到 liked 字段变化(observable 委托追踪)
查找:哪些 attr 块读了 liked?→ 卡片背景 + Text "♡"
只重跑这两个 attr 块,发出 setProp 指令
原生控件 setBackgroundColor / setText(毫秒级)