主题
第 3 章 核心概念 — Pager / ViewBuilder / attr-event 三件套
学习目标:吃透 Kuikly DSL 的「三件套心智模型」 —— 一个 Pager 是怎么组织的、ViewBuilder 闭包到底是什么、attr 与 event 各自的边界在哪里;同时建立 Kuikly 的 Flexbox 布局思维,看到一张设计稿能立刻写出对应的容器结构。
3.1 三件套总览
┌──────────────────────────────────────────────────────┐
│ ★ Kuikly 心智模型「三件套」 │
├──────────────────────────────────────────────────────┤
│ │
│ 1. Pager ← "页面",整个屏幕的根容器 │
│ │ │
│ ├── body() : ViewBuilder │
│ │ │
│ 2. ViewBuilder ← "描述 UI 长啥样"的 Kotlin 闭包 │
│ │ │
│ ├── 组件 { attr {} event {} 子组件 } │
│ │ │
│ 3. attr / event ← 每个组件的「样式 + 交互」两栏配置 │
│ │
└──────────────────────────────────────────────────────┘📌 生活化类比:把一个 Pager 想成一份「网店商品详情页」:
- Pager ≈ 整张商品页(包了导航条、主图、详情、底部购买栏)
- ViewBuilder ≈ 「页面布局图」 —— 告诉装修师傅每个区域放什么
- attr ≈ 「装修说明」 —— 颜色、尺寸、字体、间距
- event ≈ 「交互说明」 —— 哪些按钮可以点、点了跳到哪
记住这张图,整章你就毕业一半了。
3.2 Pager:页面的"根容器"
3.2.1 Pager 是什么
Pager 是 Kuikly 提供的页面基类,一个 Pager 对应屏幕上的一整页。它做了 4 件事:
┌────────────────────────────────────────────────────────┐
│ Pager 的 4 大职责 │
├────────────────────────────────────────────────────────┤
│ 1. 持有 状态(observable 字段、列表、网络数据) │
│ 2. 描述 UI(实现 body() 返回 ViewBuilder) │
│ 3. 接收 路由参数(pageData,从宿主传入的初始化数据) │
│ 4. 监听 生命周期(created / appear / disappear / destroy)│
└────────────────────────────────────────────────────────┘最小可运行示例:
kotlin
@Page("MinimalPage")
internal class MinimalPage : Pager() {
override fun body(): ViewBuilder {
return {
// 这里描述 UI
}
}
}📌 一个
@Page("xxx")注解 + 继承Pager()+ 实现body(),就构成了一个完整页面。
3.2.2 Pager 的生命周期
┌─────────────────────────────────────────────────────┐
│ Pager 生命周期(按时间顺序) │
├─────────────────────────────────────────────────────┤
│ │
│ 1. created() ★ 页面对象创建后立即触发 │
│ │ 适合做:初始化、读 pageData │
│ ▼ │
│ 2. body() ★ 第一次渲染前调用 │
│ │ 适合做:定义 UI │
│ ▼ │
│ 3. pageDidAppear() ★ 页面出现在屏幕上 │
│ │ 适合做:发埋点、起动画 │
│ ▼ │
│ ── 页面运行中(用户操作、状态更新、UI 重渲染)── │
│ │ │
│ ▼ │
│ 4. pageWillDestroy()★ 页面即将销毁 │
│ 适合做:取消请求、解绑监听 │
└─────────────────────────────────────────────────────┘实战示例:
kotlin
@Page("LifecyclePage")
internal class LifecyclePage : Pager() {
private var userId: String = ""
override fun created() {
super.created()
// 从路由参数读初始化数据
userId = pageData.params.getString("userId") ?: ""
KLog.i("Lifecycle", "created: userId=$userId")
}
override fun pageDidAppear() {
super.pageDidAppear()
KLog.i("Lifecycle", "appear, send pv beacon")
}
override fun pageWillDestroy() {
super.pageWillDestroy()
KLog.i("Lifecycle", "destroy, cancel network")
}
override fun body(): ViewBuilder = {
Text { attr { text("user: $userId") } }
}
}3.2.3 Pager 之间怎么跳?
Kuikly 提供 PageRouterModule:
kotlin
acquireModule<PageRouterModule>(PageRouterModule.MODULE_NAME)
.openPage("DetailPage", JSONObject().apply {
put("itemId", "abc123")
})跳转细节会在第 8 章 Module 里展开。这一章你先记住:每个 @Page("xxx") 的字符串就是它的"门牌号",跳转就是按门牌号开门。
3.3 ViewBuilder:描述 UI 的"积木拼图"
3.3.1 ViewBuilder 到底是个啥?
打开 Kuikly 源码,你会看到:
kotlin
typealias ViewBuilder = ViewContainer<*, *>.() -> Unit简单说,ViewBuilder 就是一个带 ViewContainer 接收者的 Kotlin Lambda。它不是 UI 本身,而是「怎么搭建 UI 的指令」。
┌────────────────────────────────────────────────┐
│ 误区:"ViewBuilder = UI 树" │
│ 事实:ViewBuilder = 「构造 UI 树的函数」 │
├────────────────────────────────────────────────┤
│ │
│ Kuikly 引擎执行 ViewBuilder 闭包 │
│ ↓ │
│ 生成 BuildTree(原型树) │
│ ↓ │
│ Diff → RenderTree → 平台原生控件 │
└────────────────────────────────────────────────┘3.3.2 用 ViewBuilder 拼一棵树
每个组件都是一个"DSL 函数",调用时传入一个 ViewBuilder 闭包描述子组件:
kotlin
override fun body(): ViewBuilder = {
View { // 根容器
attr { allCenter() }
Text { // 子组件 1
attr { text("Hello") }
}
Image { // 子组件 2
attr { src("https://x/avatar.png") }
}
View { // 子组件 3:嵌套容器
attr { flexDirectionRow() }
Text { attr { text("A") } }
Text { attr { text("B") } }
}
}
}对应的 BuildTree 结构:
View (root)
├── Text "Hello"
├── Image avatar
└── View
├── Text "A"
└── Text "B"📌 核心心智:ViewBuilder 闭包就是用 Kotlin 写的"声明式 UI 描述"。你不操作 View 实例,你描述 UI 应该是什么样,Kuikly 引擎负责实现。
3.3.3 抽出可复用的 ViewBuilder
ViewBuilder 是一等公民,可以抽函数、传参数、当作变量:
kotlin
// 抽一个"用户头像 + 昵称"的小卡片
private fun userCard(name: String, avatar: String): ViewBuilder = {
View {
attr { flexDirectionRow(); allCenter(); padding(12f) }
Image {
attr { src(avatar); size(40f, 40f); borderRadius(20f) }
}
Text {
attr {
text(name); marginLeft(8f); fontSize(14f); color(Color.BLACK)
}
}
}
}
// 在 body() 里复用
override fun body(): ViewBuilder = {
View {
attr { padding(16f) }
// ★ 调用方式:apply 这个 ViewBuilder
userCard("张三", "https://x/zs.png").invoke(this)
userCard("李四", "https://x/ls.png").invoke(this)
}
}📌 进阶用法:可以把"通用的卡片 / 列表 item / 按钮"抽成
ViewBuilder函数,跟 React 的「函数组件」、Compose 的「@Composable 函数」一样。这是组件化的第一步。
3.4 attr 块:「样式 + 布局 + 数据」三合一
3.4.1 attr 是干嘛的
每个组件后面都跟一个 attr {} 块,所有属性都写在里面。它包含 3 类内容:
┌──────────────────────────────────────────────────┐
│ attr {} 块的 3 类内容 │
├──────────────────────────────────────────────────┤
│ 1. 样式属性 backgroundColor / borderRadius / opacity │
│ 2. 布局属性 width / margin / flexDirectionRow ... │
│ 3. 数据属性 text("...") / src("...") / value("...") │
└──────────────────────────────────────────────────┘3.4.2 一个完整 attr 长啥样
kotlin
Text {
attr {
// ── 数据 ──
text("Hello Kuikly")
// ── 样式 ──
fontSize(20f)
fontWeightBold()
color(Color(0xFF1976D2L))
backgroundColor(Color(0xFFE3F2FDL))
// ── 布局 ──
padding(12f)
marginTop(20f)
borderRadius(8f)
}
}3.4.3 attr 是「方法调用」,不是「属性赋值」
注意这里的细节,Kuikly 用的是方法调用风格:
kotlin
attr {
fontSize(20f) // ✅ 方法
color(Color.BLACK) // ✅ 方法
}
// 不是:
attr {
fontSize = 20f // ❌ 属性赋值(写不出来,会报错)
}为啥这样设计?三个原因:
- 响应式追踪:方法调用便于 Kuikly 引擎记录"哪些字段被读了 → 这个属性依赖哪些 observable"
- 链式可读:
fontWeightBold()比fontWeight = FontWeight.BOLD更短 - DSL 一致性:跟 Compose 的
Modifier.padding(...)一脉相承
3.4.4 高频 attr 速查表
| 类别 | 常用 API | 说明 |
|---|---|---|
| 尺寸 | width(100f) height(50f) size(w, h) | 单位是 dp(pt 在 iOS) |
| 背景 | backgroundColor(Color) backgroundImage(url) | 颜色或图片 |
| 边框 | borderRadius(8f) border(width, color) | 圆角 / 描边 |
| 间距 | padding(8f) paddingHorizontal(16f) marginTop(20f) | 内 / 外间距 |
| 位置 | absolutePosition(top=0f, left=0f) flexDirectionRow() | 绝对 / Flex |
| 对齐 | allCenter() justifyContentSpaceBetween() alignItemsCenter() | Flex 对齐 |
| 文字 | text("...") fontSize(16f) color(Color) fontWeightBold() | Text 专属 |
| 图片 | src("url 或本地资源") resizeCover() | Image 专属 |
| 透明度 / 旋转 | opacity(0.5f) transform { rotate(45f) } | 视觉效果 |
3.5 event 块:「交互监听」专属区
3.5.1 event 跟 attr 为什么要分开
回顾第 1 章 Q5 的答案:职责清晰、响应式追踪精度、渲染指令拆分。
代码层面:
kotlin
View {
attr { // 「我长啥样」
size(100f, 50f)
backgroundColor(Color.BLUE)
}
event { // 「我能响应啥」
click { /* ... */ }
longPress { /* ... */ }
}
}📌 生活化类比:
attr是「这个按钮上贴的标签」(红色、圆形、写着"购买");event是「这个按钮接的电线」(按下去通往哪个动作)。
3.5.2 常用事件清单
| 事件 | 触发场景 | 常用组件 |
|---|---|---|
click { e -> } | 单击 | View / Button / Text / Image |
longPress { e -> } | 长按(默认 500ms) | 同上 |
doubleClick { e -> } | 双击 | 同上 |
pan { e -> } | 手指拖动 | 任意可拖动组件 |
screenFrame { fps -> } | 每帧回调 | 动画专用 |
scrollEnd { e -> } | 滚动结束 | Scroller / List |
onLoadFinish { e -> } | 图片加载完成 | Image |
onTextChange { value -> } | 输入变化 | Input / TextArea |
3.5.3 event 闭包里能拿到什么
每个事件回调都有一个事件对象 e,里面带着上下文信息:
kotlin
event {
click { e ->
// e.x, e.y 相对组件的坐标
// e.pageX, e.pageY 屏幕坐标
// e.timestamp 事件时间戳
}
pan { e ->
// e.state "start" / "move" / "end"
// e.dx, e.dy 相对上一次的位移
}
}3.5.4 一个完整的「按钮点击」例子
kotlin
@Page("ClickDemo")
internal class ClickDemo : Pager() {
private var count by observable(0)
override fun body(): ViewBuilder {
val ctx = this
return {
attr { allCenter(); padding(20f) }
Text {
attr {
text("Click count: ${ctx.count}")
fontSize(20f)
marginBottom(20f)
}
}
View {
attr {
size(120f, 44f)
allCenter()
backgroundColor(Color(0xFF1976D2L))
borderRadius(22f)
}
event {
click { ctx.count++ }
}
Text {
attr {
text("Tap me")
color(Color.WHITE)
fontSize(16f)
}
}
}
}
}
}每点一次按钮 → count++ → Kuikly 检测到 count 变化 → 重新求值 attr 里 text("Click count: ${ctx.count}") → 直接更新 Text 控件,不重建容器。
3.6 Flexbox 布局思维 —— Kuikly 的「布局母语」
Kuikly 跟 RN / Flutter 一样,布局唯一的算法是 Flexbox。学不会 Flexbox 等于学不会 Kuikly。
3.6.1 30 秒入门 Flexbox
每个容器(View)都有 2 个核心属性:
┌─────────────────────────────────────────────────┐
│ flexDirection 子组件排列方向 │
│ - column (默认) 上 → 下 │
│ - row 左 → 右 │
├─────────────────────────────────────────────────┤
│ justifyContent 主轴对齐方式 │
│ - flexStart 从头排 │
│ - center 居中 │
│ - flexEnd 从尾排 │
│ - spaceBetween 两端对齐,中间均分 │
│ - spaceAround 两端有半个间距 │
├─────────────────────────────────────────────────┤
│ alignItems 交叉轴对齐 │
│ - flexStart / center / flexEnd / stretch │
└─────────────────────────────────────────────────┘3.6.2 3 张图看懂 Flexbox
flexDirection = column(默认):
主轴 ↓
┌──────────┐
│ ChildA │
│ ChildB │
│ ChildC │
└──────────┘flexDirection = row:
主轴 →
┌──────────────────────────┐
│ ChildA ChildB ChildC │
└──────────────────────────┘justifyContent = spaceBetween(row 方向):
┌────────────────────────────────────────┐
│ ChildA ChildB ChildC │
└────────────────────────────────────────┘
↑ 两端贴边,中间均分剩余空间3.6.3 Kuikly 的 Flexbox API 对照
| 需求 | Kuikly API |
|---|---|
| 子组件横向排列 | flexDirectionRow() |
| 子组件全部居中 | allCenter() 或 justifyContentCenter() + alignItemsCenter() |
| 子组件两端对齐 | justifyContentSpaceBetween() |
| 子组件交叉轴居中 | alignItemsCenter() |
| 让某个子组件占满剩余空间 | 子组件 attr { flex(1f) } |
| 让子组件换行 | flexWrapWrap() |
3.6.4 常见布局对照
1. 顶部导航条(左 icon + 中标题 + 右 icon)
kotlin
View {
attr {
flexDirectionRow()
height(44f)
alignItemsCenter()
justifyContentSpaceBetween()
paddingHorizontal(16f)
backgroundColor(Color.WHITE)
}
Image { attr { src("back.png"); size(24f, 24f) } }
Text { attr { text("详情"); fontSize(16f); fontWeightBold() } }
Image { attr { src("more.png"); size(24f, 24f) } }
}2. 底部 4 Tab 等分
kotlin
View {
attr { flexDirectionRow(); height(50f) }
Tab(icon = "home.png", label = "首页", modifier = { flex(1f) })
Tab(icon = "shop.png", label = "购物", modifier = { flex(1f) })
Tab(icon = "msg.png", label = "消息", modifier = { flex(1f) })
Tab(icon = "me.png", label = "我", modifier = { flex(1f) })
}3. 商品卡片(左图 + 右文)
kotlin
View {
attr { flexDirectionRow(); padding(12f) }
Image { attr { src(...); size(80f, 80f); borderRadius(4f) } }
View {
attr { flex(1f); marginLeft(12f) }
Text { attr { text("商品标题"); fontSize(14f) } }
Text { attr { text("¥99"); color(Color.RED); marginTop(8f) } }
}
}📌 熟练后:拿到任何设计稿,先看大块 → 决定 row/column → 再看子块 → 决定 flex/绝对宽高。90% 的页面都能用 Flexbox 一气呵成。
3.7 完整示例:拼一个「用户卡片页」
把这一章学到的全用起来:
kotlin
@Page("UserCardPage")
internal class UserCardPage : Pager() {
private var liked by observable(false)
private var likeCount by observable(128)
override fun body(): ViewBuilder {
val ctx = this
return {
attr {
backgroundColor(Color(0xFFF5F5F7L))
padding(20f)
}
// ─── 用户卡片 ───
View {
attr {
backgroundColor(Color.WHITE)
borderRadius(12f)
padding(16f)
}
// 上半部分:头像 + 名字
View {
attr { flexDirectionRow(); alignItemsCenter() }
Image {
attr {
src("https://example.com/avatar.png")
size(48f, 48f)
borderRadius(24f)
}
}
View {
attr { marginLeft(12f); flex(1f) }
Text {
attr {
text("张三")
fontSize(16f)
fontWeightBold()
color(Color.BLACK)
}
}
Text {
attr {
text("Kuikly 学习中")
fontSize(12f)
color(Color(0xFF9E9E9EL))
marginTop(4f)
}
}
}
}
// 下半部分:操作按钮
View {
attr {
flexDirectionRow()
marginTop(16f)
justifyContentSpaceBetween()
}
// 点赞按钮
View {
attr {
flexDirectionRow()
alignItemsCenter()
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 "♡")
color(if (ctx.liked) Color.RED else Color.BLACK)
fontSize(16f)
}
}
Text {
attr {
text("${ctx.likeCount}")
marginLeft(6f)
fontSize(14f)
}
}
}
// 关注按钮
View {
attr {
allCenter()
paddingHorizontal(16f)
height(36f)
borderRadius(18f)
backgroundColor(Color(0xFF1976D2L))
}
event { click { /* TODO 关注 */ } }
Text {
attr {
text("+ 关注")
color(Color.WHITE)
fontSize(14f)
}
}
}
}
}
}
}
}效果(脑补一下):
┌─────────────────────────────────────────┐
│ ┌──────────────────────────────────┐ │ <- F5F5F7 背景
│ │ [头] 张三 │ │
│ │ Kuikly 学习中 │ │
│ │ │ │
│ │ [♡ 128] [+ 关注] │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘点击 ♡ → 变 ♥ + 数字 +1 + 背景色变红,全部由 attr 里读 observable 字段自动响应。
3.8 三个新手必踩的坑
坑 1:在 attr 里写副作用
kotlin
// ❌ 错误示范
attr {
text("count: $count")
println("rendering") // ← attr 块会被多次调用,副作用会重复打印
fetchUserName() // ← 在 attr 里发网络请求?不!
}
// ✅ 正确:副作用放生命周期或 event
override fun pageDidAppear() {
fetchUserName()
}坑 2:忘了通过 ctx 引用 Pager 字段
kotlin
// ❌ 错误示范
override fun body(): ViewBuilder {
return {
Text { attr { text("$count") } } // ← 编译失败!闭包里的 this 是 ViewContainer,不是 Pager
}
}
// ✅ 正确写法
override fun body(): ViewBuilder {
val ctx = this // ← 把 Pager 引用提出来
return {
Text { attr { text("${ctx.count}") } }
}
}坑 3:Color 忘了写 L 后缀
kotlin
// ❌ 编译报错(数字超过 Int 范围)
backgroundColor(Color(0xFF7F52FF))
// ✅ 正确,Long 字面量必须带 L
backgroundColor(Color(0xFF7F52FFL))3.9 章末小结
★ 第 3 章核心知识图谱 ★
│
┌────────────────────┼────────────────────┐
│ │ │
┌───▼────┐ ┌─────▼─────┐ ┌────▼─────┐
│ Pager │ │ ViewBuilder│ │attr/event│
├────────┤ ├────────────┤ ├──────────┤
│ 路由名 │ │ DSL 闭包 │ │ 样式 vs │
│ 生命周期│ │ 可抽函数 │ │ 交互 │
│ 状态宿主│ │ 可嵌套 │ │ 方法风格 │
└────────┘ └────────────┘ └──────────┘
│
▼
Flexbox 是布局母语
(row/column + justify/align + flex)🎤 3.10 章末面试题(10 道高频题)
Q1. Pager、ViewBuilder、attr、event 各自是什么?怎么协作?
答:
- Pager:页面基类,持有状态、生命周期、UI 描述
- ViewBuilder:一个
ViewContainer<*,*>.() -> UnitLambda,用 Kotlin 描述 UI 长啥样 - attr:每个组件的"样式 + 布局 + 数据"配置块
- event:每个组件的"交互监听"配置块
协作流程:宿主调 Pager → Pager.body() 返回 ViewBuilder → 引擎执行闭包遍历组件 → 每个组件的 attr / event 转成属性集合 + 事件监听 → 生成 BuildTree → Diff 后下发原生指令。
Q2. Pager 的 created()、pageDidAppear()、pageWillDestroy() 各自适合做什么?
答:
created():页面对象创建后立即触发,适合读 pageData、初始化字段、订阅长期数据pageDidAppear():页面真正显示到屏幕上,适合发 PV 埋点、起出现动画、启动定时器pageWillDestroy():页面即将销毁,适合取消网络请求、解绑监听、释放资源
Q3. ViewBuilder 是不是 UI 树?
答:不是。ViewBuilder 是「构造 UI 树的函数」,是带 receiver 的 Kotlin Lambda。它本身不是 UI,每次执行才会生成(或更新)BuildTree 节点。这跟 React 的 JSX、Compose 的 @Composable 函数本质相同,都是"声明式 UI"的不同语法形态。
Q4. 为什么 Kuikly 的 attr 用方法调用,不用属性赋值?
答:
- 响应式追踪精度:方法调用便于编译期 / 运行时记录"这个属性读取了哪些 observable"
- DSL 一致性:跟 Compose / 链式 Builder 风格一致,可读性好
- API 演进灵活:方法可以重载、加默认参数,比 var 更可扩展
- 减少误用:避免开发者在 attr 块里写
if/else { x = ...; x = ... }这种竞态赋值
Q5. 如何在 ViewBuilder 闭包里访问 Pager 的字段?
答:闭包里的 this 是 ViewContainer<*,*>,不是 Pager。需要在 body() 里把 Pager 实例提出来:
kotlin
override fun body(): ViewBuilder {
val ctx = this // ★ 把 Pager 提出来
return {
Text { attr { text("${ctx.count}") } }
}
}这是新人的高频踩坑点,建议养成「val ctx = this」的肌肉记忆。
Q6. Kuikly 的布局算法是什么?
答:Flexbox。所有 View 容器默认用 Flexbox 排列子组件,核心 3 个属性:
flexDirection:主轴方向(默认 column,可改 row)justifyContent:主轴对齐alignItems:交叉轴对齐
子组件可以用 flex(n) 占据剩余空间。这跟 React Native / Compose 的布局思路完全一致。
Q7. 如何让一个子组件填满父容器剩余空间?
答:在子组件的 attr 里写 flex(1f)。例如:
kotlin
View {
attr { flexDirectionRow() }
Text { attr { text("固定宽度文字") } }
View { attr { flex(1f); backgroundColor(Color.RED) } } // 占满剩余空间
}Q8. attr 块会被执行多次吗?
答:会。attr 块本质是个 Kotlin 闭包,每次组件需要刷新属性时都会被执行。这就是为什么不能在 attr 里写副作用(如 println、网络请求)—— 会被重复调用。
正确做法:副作用放在 created() / pageDidAppear() / event 回调里。
Q9. 一个 Pager 里可以嵌套另一个 Pager 吗?
答:不可以。Pager 是"页面级"概念,对应屏幕一整屏。如果想做"页面内子模块复用",应该把对应 ViewBuilder 抽成函数或抽成 ComposeView/Component(Kuikly 提供了 Component 基类,专为可复用 UI 块设计),第 4 章会讲。
Q10. attr 块和 event 块的执行顺序、执行频率有什么区别?
答:
- attr 块:每次属性需要刷新都会重跑,频率高(取决于响应式字段变化频率)
- event 块:只在组件挂载时注册一次,注册的 Lambda 会被引擎缓存,事件触发时直接调用,不会重跑 event 块本身
所以:把"轻量、纯 UI 描述"放 attr,把"业务逻辑、副作用"放 event 回调。
下一站 → 第 4 章 · 基础组件 View / Text / Image / Input / Button / Scroller →
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗
💻 示例代码
kotlin
/**
* 第 3 章配套代码 · Pager 生命周期演示
*
* 文件位置:shared/src/commonMain/kotlin/com/example/kuikly/pages/LifecycleDemoPage.kt
*
* 本示例演示 Pager 生命周期 4 个核心方法的触发时机:
* created() → 对象刚创建(读 pageData、初始化)
* body() → 第一次构建 UI
* pageDidAppear() → 页面真正显示到屏幕(埋点、起动画)
* pageWillDestroy()→ 页面即将销毁(取消请求、解绑监听)
*
* 跑起来观察日志,即可建立"什么时候做什么事"的肌肉记忆。
*/
package com.example.kuikly.pages
import com.tencent.kuikly.core.annotations.Page
import com.tencent.kuikly.core.base.Color
import com.tencent.kuikly.core.base.ViewBuilder
import com.tencent.kuikly.core.log.KLog
import com.tencent.kuikly.core.pager.Pager
import com.tencent.kuikly.core.reactive.handler.observable
import com.tencent.kuikly.core.views.Text
import com.tencent.kuikly.core.views.View
private const val TAG = "Lifecycle"
@Page("LifecycleDemoPage")
internal class LifecycleDemoPage : Pager() {
private var userId: String = ""
private var fromSource: String = ""
private var elapsedSec by observable(0)
private var timerHandle: Any? = null
// ────────────────────────────────────────────────────────
// ① created:页面对象刚刚创建
// 适合:读 pageData、做字段初始化、订阅长期数据流
// 注意:此时 UI 还没渲染,不能直接操作 view
// ────────────────────────────────────────────────────────
override fun created() {
super.created()
userId = pageData.params.optString("userId", "")
fromSource = pageData.params.optString("from", "unknown")
KLog.i(TAG, "① created userId=$userId, from=$fromSource")
}
// ────────────────────────────────────────────────────────
// ② body:每次需要构建/重建 UI 都会调
// 第一次执行发生在 created() 之后、pageDidAppear() 之前
// 后续:根容器需要重建时
// ────────────────────────────────────────────────────────
override fun body(): ViewBuilder {
KLog.i(TAG, "② body() called")
val ctx = this
return {
attr {
allCenter()
backgroundColor(Color(0xFF1E1E1EL))
}
Text {
attr {
text("已停留 ${ctx.elapsedSec} 秒")
fontSize(22f)
color(Color.WHITE)
fontWeightBold()
}
}
View {
attr {
marginTop(20f)
paddingHorizontal(20f)
paddingVertical(8f)
borderRadius(8f)
backgroundColor(Color(0xFF424242L))
}
Text {
attr {
text("userId=$userId, from=$fromSource")
fontSize(12f)
color(Color(0xFFBDBDBDL))
}
}
}
}
}
// ────────────────────────────────────────────────────────
// ③ pageDidAppear:页面真正显示
// 适合:发 PV 埋点、起入场动画、启动定时器、申请权限
// ────────────────────────────────────────────────────────
override fun pageDidAppear() {
super.pageDidAppear()
KLog.i(TAG, "③ pageDidAppear -> 上报 PV 埋点")
// 启动一个 1 秒一次的"停留时长"计时器
// 真实工程一般用 TimerModule,这里只示意
startElapsedTimer()
}
// ────────────────────────────────────────────────────────
// ④ pageWillDestroy:页面即将销毁
// 适合:取消所有正在进行的网络请求、移除监听、停止定时器
// 若不清理,会导致 Pager 实例无法回收 → 内存泄漏
// ────────────────────────────────────────────────────────
override fun pageWillDestroy() {
super.pageWillDestroy()
KLog.i(TAG, "④ pageWillDestroy -> 取消计时器、上报停留时长 ${elapsedSec}s")
stopElapsedTimer()
}
// ────────────────────────────────────────────────────────
// 辅助:模拟一个简单计时器
// 真实项目应使用 TimerModule(第 8 章会讲)
// ────────────────────────────────────────────────────────
private fun startElapsedTimer() {
// 伪代码:acquireModule<TimerModule>(...).schedule(1000) { elapsedSec++ }
timerHandle = Any()
}
private fun stopElapsedTimer() {
timerHandle = null
}
}kotlin
/**
* 第 3 章配套代码 · ViewBuilder 抽函数复用
*
* 文件位置:shared/src/commonMain/kotlin/com/example/kuikly/pages/ReusableViewBuilder.kt
*
* 演示如何把通用 UI 块抽成 ViewBuilder 函数,在多个 Pager 里复用。
*
* 类比:React 的「函数组件」、Compose 的 `@Composable` 函数
* —— 都是一种让 UI 代码可复用的方式。
*
* 关键 API:
* - 一个 `() -> ViewBuilder` 函数 = 可参数化的 UI 模板
* - 在 body 里通过 .invoke(this) 应用 ViewBuilder
*/
package com.example.kuikly.pages
import com.tencent.kuikly.core.annotations.Page
import com.tencent.kuikly.core.base.Color
import com.tencent.kuikly.core.base.ViewBuilder
import com.tencent.kuikly.core.pager.Pager
import com.tencent.kuikly.core.views.Image
import com.tencent.kuikly.core.views.Text
import com.tencent.kuikly.core.views.View
// ────────────────────────────────────────────────────────
// 「用户卡片」可复用 ViewBuilder
//
// 参数化:name、avatar、subtitle 都从外面传入
// 返回值:ViewBuilder(可以被任何 Pager 的 body() 调用)
// ────────────────────────────────────────────────────────
fun userCard(
name: String,
avatar: String,
subtitle: String = "",
onClick: () -> Unit = {},
): ViewBuilder = {
View {
attr {
flexDirectionRow()
alignItemsCenter()
padding(12f)
marginBottom(8f)
backgroundColor(Color.WHITE)
borderRadius(8f)
}
event { click { onClick() } }
Image {
attr {
src(avatar)
size(40f, 40f)
borderRadius(20f)
backgroundColor(Color(0xFFE0E0E0L))
}
}
View {
attr { marginLeft(12f); flex(1f) }
Text {
attr {
text(name)
fontSize(14f)
fontWeightBold()
color(Color.BLACK)
}
}
if (subtitle.isNotEmpty()) {
Text {
attr {
text(subtitle)
fontSize(12f)
color(Color(0xFF9E9E9EL))
marginTop(2f)
}
}
}
}
}
}
// ────────────────────────────────────────────────────────
// 「分割线」可复用 ViewBuilder(无参数版)
// ────────────────────────────────────────────────────────
fun divider(): ViewBuilder = {
View {
attr {
height(0.5f)
backgroundColor(Color(0xFFE0E0E0L))
marginVertical(4f)
}
}
}
// ────────────────────────────────────────────────────────
// 在 Pager 里组合复用
// ────────────────────────────────────────────────────────
@Page("UserListPage")
internal class UserListPage : Pager() {
override fun body(): ViewBuilder {
return {
attr {
backgroundColor(Color(0xFFF5F5F7L))
padding(16f)
}
// ★ 调用方式:调用函数拿到 ViewBuilder,再 invoke 到当前容器
userCard(
name = "张三",
avatar = "https://example.com/zs.png",
subtitle = "Kuikly 学习中",
onClick = { /* TODO 跳转到张三主页 */ },
).invoke(this)
userCard(
name = "李四",
avatar = "https://example.com/ls.png",
subtitle = "Compose 死忠",
).invoke(this)
divider().invoke(this)
userCard(
name = "王五",
avatar = "https://example.com/ww.png",
subtitle = "Flutter 转 Kuikly",
).invoke(this)
}
}
}kotlin
/**
* 第 3 章配套代码 · 用户卡片页(attr/event/observable 综合演示)
*
* 文件位置:shared/src/commonMain/kotlin/com/example/kuikly/pages/UserCardPage.kt
*
* 本示例演示「三件套」的完整用法:
* - Pager 页面基类 + observable 状态
* - ViewBuilder 嵌套组件构建 UI 树
* - attr/event 样式属性 vs 交互监听
*
* 重点关注:
* 1. `val ctx = this` 拿到 Pager 引用,闭包里通过 ctx.xxx 读字段
* 2. attr 里 `if (ctx.liked) Color.RED else Color.GRAY` 演示响应式
* 3. event { click { ... } } 的写法和闭包捕获
*/
package com.example.kuikly.pages
import com.tencent.kuikly.core.annotations.Page
import com.tencent.kuikly.core.base.Color
import com.tencent.kuikly.core.base.ViewBuilder
import com.tencent.kuikly.core.pager.Pager
import com.tencent.kuikly.core.reactive.handler.observable
import com.tencent.kuikly.core.views.Image
import com.tencent.kuikly.core.views.Text
import com.tencent.kuikly.core.views.View
@Page("UserCardPage")
internal class UserCardPage : Pager() {
// ────────────────────────────────────────────────────────
// 响应式状态:observable 委托
// 字段值变化时会自动触发引用了它的 attr 块重新求值
// 详细原理见第 5 章
// ────────────────────────────────────────────────────────
private var liked by observable(false)
private var likeCount by observable(128)
override fun body(): ViewBuilder {
// ★ 关键 1:把 Pager 引用提出来,闭包里通过 ctx.xxx 读
val ctx = this
return {
// ─── 根容器 ───────────────────────────────
attr {
backgroundColor(Color(0xFFF5F5F7L))
padding(20f)
}
// ─── 用户卡片:白色圆角面板 ────────────────
View {
attr {
backgroundColor(Color.WHITE)
borderRadius(12f)
padding(16f)
}
// ─── 上半部分:头像 + 用户信息 ──────
View {
attr { flexDirectionRow(); alignItemsCenter() }
Image {
attr {
src("https://example.com/avatar.png")
size(48f, 48f)
borderRadius(24f)
backgroundColor(Color(0xFFE0E0E0L)) // 占位灰
}
}
View {
attr { marginLeft(12f); flex(1f) }
Text {
attr {
text("张三")
fontSize(16f)
fontWeightBold()
color(Color.BLACK)
}
}
Text {
attr {
text("Kuikly 学习中")
fontSize(12f)
color(Color(0xFF9E9E9EL))
marginTop(4f)
}
}
}
}
// ─── 下半部分:操作按钮区 ──────────────
View {
attr {
flexDirectionRow()
marginTop(16f)
justifyContentSpaceBetween()
}
// ─── 点赞按钮(响应式 UI 演示)──
View {
attr {
flexDirectionRow()
alignItemsCenter()
paddingHorizontal(16f)
height(36f)
borderRadius(18f)
// ★ 关键 2:attr 里读 ctx.liked,会被自动追踪为依赖
backgroundColor(
if (ctx.liked) Color(0xFFFFEBEEL)
else Color(0xFFF5F5F7L)
)
}
event {
// ★ 关键 3:event 闭包捕获 ctx,触发时改字段
click {
ctx.liked = !ctx.liked
ctx.likeCount += if (ctx.liked) 1 else -1
}
}
Text {
attr {
text(if (ctx.liked) "♥" else "♡")
color(if (ctx.liked) Color.RED else Color.BLACK)
fontSize(16f)
}
}
Text {
attr {
text("${ctx.likeCount}")
marginLeft(6f)
fontSize(14f)
}
}
}
// ─── 关注按钮 ───────────────────
View {
attr {
allCenter()
paddingHorizontal(16f)
height(36f)
borderRadius(18f)
backgroundColor(Color(0xFF1976D2L))
}
event { click { /* TODO 调用关注接口 */ } }
Text {
attr {
text("+ 关注")
color(Color.WHITE)
fontSize(14f)
}
}
}
}
}
}
}
}LifecycleDemoPage.kt ↗ · ReusableViewBuilder.kt ↗ · UserCardPage.kt ↗