Skip to content

第 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       // ❌ 属性赋值(写不出来,会报错)
}

为啥这样设计?三个原因:

  1. 响应式追踪:方法调用便于 Kuikly 引擎记录"哪些字段被读了 → 这个属性依赖哪些 observable"
  2. 链式可读fontWeightBold()fontWeight = FontWeight.BOLD 更短
  3. 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<*,*>.() -> Unit Lambda,用 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 用方法调用,不用属性赋值?

  1. 响应式追踪精度:方法调用便于编译期 / 运行时记录"这个属性读取了哪些 observable"
  2. DSL 一致性:跟 Compose / 链式 Builder 风格一致,可读性好
  3. API 演进灵活:方法可以重载、加默认参数,比 var 更可扩展
  4. 减少误用:避免开发者在 attr 块里写 if/else { x = ...; x = ... } 这种竞态赋值

Q5. 如何在 ViewBuilder 闭包里访问 Pager 的字段?

:闭包里的 thisViewContainer<*,*>,不是 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 ↗