【开源鸿蒙跨平台开发–KuiklyUI–03】KuiklyUI 入门实战:从零打造高性能跨平台 Todo 应用

1. 什么是 KuiklyUI?

KuiklyUI 是腾讯开源的一款基于 Kotlin Multiplatform (KMP) 的高性能跨平台 UI 框架。它的核心理念是 “Write Once, Run Everywhere”,但与 Flutter 或 React Native 不同,KuiklyUI 更加强调原生性能轻量级

核心优势

  • 极致性能:核心逻辑编译为 Native 二进制 (Android/iOS/HarmonyOS),UI 渲染直接驱动平台原生组件 (View/UIKit/ArkUI)。
  • 原生体验:拒绝"自绘引擎"带来的非原生手感,KuiklyUI 的组件就是原生组件。
  • 双 DSL 支持:支持自研的简洁 DSL 和 Jetpack Compose 风格 DSL。
  • 全平台覆盖:Android, iOS, HarmonyOS Next, Web (H5), 小程序。

2. 环境准备

在开始之前,请确保你的开发环境满足以下要求:

  • JDK: JDK 17+ (推荐 JDK 17.0.6 或更高)
  • Android 开发: Android Studio Ladybug 或更高版本 (支持 KMP 插件)
  • 鸿蒙开发: DevEco Studio 5.0+ (用于运行鸿蒙宿主工程)
  • 构建工具: Gradle 8.0+ (项目内置,无需手动安装)
  • 操作系统: Windows / macOS / Linux

2.1 鸿蒙环境特别配置

对于鸿蒙开发,你需要配置鸿蒙 SDK 的环境变量,以便 Gradle 能够找到它:

  • Windows: 设置系统环境变量 DEVECO_SDK_HOME 指向你的 DevEco SDK 路径(例如 C:\Users\YourName\AppData\Local\Huawei\Sdk)。
  • macOS: 在 ~/.bash_profile~/.zshrc 中添加 export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk

3. 项目结构概览

克隆项目后,你会看到以下核心目录:

KuiklyUI/
├── core/                    # 框架核心 (KMP)
├── compose/                 # Compose DSL 适配层
├── demo/                    # 演示代码 (我们将在这里编写业务逻辑)
│   └── src/commonMain/      # 跨平台通用代码 (Write Code Here!)
│       ├── kotlin/com/tencent/kuikly/demo/
│       │   ├── pages/       # 页面目录
│       │   ├── widgets/     # 公共组件目录
│       │   └── App.kt       # 应用入口
├── androidApp/              # Android 宿主工程 (仅包含启动代码)
├── ohosApp/                 # HarmonyOS 宿主工程 (仅包含启动代码)
├── iosApp/                  # iOS 宿主工程
└── 2.0_ohos_demo_build.bat  # 鸿蒙编译脚本 (Windows)

开发理念:我们的绝大部分代码(95%以上)都编写在 demo/src/commonMain 目录下,各个平台的 App 目录仅作为宿主壳工程,负责加载 Kuikly 引擎。


从AtomGit上下载这个项目KuiklyTodo
解压存放代码的地方查看项目代码,本项目是基于模板项目修改的
在这里插入图片描述

4. 实战:开发一个 Todo 应用

我们将实现一个功能完善的待办事项应用,包含:

  1. 待办事项列表展示
  2. 添加新事项
  3. 标记完成/未完成
  4. 删除事项

4.1 定义数据模型

首先,我们需要一个数据类来描述待办事项。在 demo/src/commonMain/kotlin/com/tencent/kuikly/demo/pages/todo 目录下创建 TodoItem.kt

package com.tencent.kuikly.demo.pages.todo

data class TodoItem(
    val id: Long,
    var content: String,
    var isCompleted: Boolean = false
)

4.2 创建页面逻辑类 (ViewModel)

在同目录下创建 TodoPage.kt。这是页面的"大脑",负责状态管理和业务逻辑。

/**
 * @ProjectName : KuiklyUI
 * @Author : GuoJiaHui
 * @Time : 2026年01月29日 17:30 PM
 * @Description : 待办事项页面逻辑控制器
 * 负责管理待办事项列表的数据状态、增删改查操作
 */
package com.tencent.kuikly.demo.pages.todo

import com.tencent.kuikly.core.annotations.Page
import com.tencent.kuikly.core.base.ViewBuilder
import com.tencent.kuikly.demo.pages.base.BasePager
import com.tencent.kuikly.core.reactive.handler.observable
import com.tencent.kuikly.core.reactive.handler.observableList

/**
 * 待办事项页面类
 * 使用 @Page 注解标记为页面,路由名称为 "todo_page"
 */
@Page("todo_page")
internal class TodoPage : BasePager() {
    /** 待办事项列表数据,使用 observableList 实现响应式更新 */
    var todoList by observableList<TodoItem>()
    
    /** 输入框文本状态,使用 observable 实现双向绑定 */
    var inputText by observable("")

    /**
     * 页面创建生命周期回调
     * 在此初始化数据
     */
    override fun created() {
        super.created()
        // 初始化演示数据
        initData()
    }

    /**
     * 初始化演示数据
     * 添加默认的待办事项
     */
    private fun initData() {
        todoList.add(TodoItem(1, "学习 KuiklyUI 框架"))
        todoList.add(TodoItem(2, "创建一个 Todo 应用"))
    }

    /**
     * 构建页面 UI 结构
     * @return ViewBuilder UI 构建函数
     */
    override fun body(): ViewBuilder {
        return buildTodoUI(this)
    }

    /** 生成 ID 的计数器,起始值为 100 */
    private var nextId = 100L

    /**
     * 添加新的待办事项
     * 校验输入是否为空,创建新项并添加到列表,最后清空输入框
     */
    fun addTodo() {
        if (inputText.isBlank()) return
        val newItem = TodoItem(nextId++, inputText)
        todoList.add(newItem)
        inputText = ""
    }

    /**
     * 切换待办事项的完成状态
     * @param item 目标待办事项对象
     */
    fun toggleTodo(item: TodoItem) {
        item.isCompleted = !item.isCompleted
        // 触发列表更新:通过重新赋值触发 observableList 的更新机制
        val index = todoList.indexOf(item)
        if (index >= 0) {
            todoList[index] = item 
        }
    }

    /**
     * 删除指定的待办事项
     * @param item 要删除的待办事项对象
     */
    fun deleteTodo(item: TodoItem) {
        todoList.remove(item)
    }

    /**
     * 清除所有已完成的待办事项
     * 遍历列表并移除 isCompleted 为 true 的项
     */
    fun clearCompleted() {
        todoList.removeAll { it.isCompleted }
    }
}

4.3 编写声明式 UI

创建 TodoPageUI.kt,使用 Kuikly 自研 DSL 描述界面。这里的写法类似 Flutter 或 SwiftUI,但完全基于 Kotlin。

/**
 * @ProjectName : KuiklyUI
 * @Author : GuoJiaHui
 * @Time : 2026年01月29日 17:30 PM
 * @Description : 待办事项页面UI构建逻辑
 * 包含整体布局、样式定义及事件绑定
 */
package com.tencent.kuikly.demo.pages.todo

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.*
import com.tencent.kuikly.core.directives.vfor
import com.tencent.kuikly.core.directives.vif
import com.tencent.kuikly.core.base.Border
import com.tencent.kuikly.core.base.BorderStyle
import com.tencent.kuikly.core.base.BoxShadow

/**
 * 构建 Todo 页面的 UI 结构
 * @param page 页面上下文对象,需转换为 TodoPage 类型以访问数据
 * @return ViewBuilder UI 构建闭包
 */
fun buildTodoUI(page: Pager): ViewBuilder {
    val ctx = page as TodoPage
    return {
        View {
            attr {
                // 设置页面主容器充满全屏
                flex(1f)
                backgroundColor(Color.WHITE)
                // 处理安全区域内边距,适配刘海屏/动态岛
                padding(
                    top = 16f + ctx.pageData.safeAreaInsets.top,
                    left = 16f + ctx.pageData.safeAreaInsets.left,
                    right = 16f + ctx.pageData.safeAreaInsets.right,
                    bottom = 16f + ctx.pageData.safeAreaInsets.bottom
                )
            }

            // 标题栏组件
            Text {
                attr {
                    text("待办事项清单")
                    fontSize(24f)
                    fontWeightBold()
                    marginBottom(20f)
                    color(Color.BLACK)
                }
            }

            // 顶部输入区域容器
            View {
                attr {
                    flexDirectionRow() // 水平布局
                    marginBottom(20f)
                    height(50f)
                    alignItemsCenter() // 垂直居中
                }

                // 文本输入框组件
                Input {
                    attr {
                        flex(1f) // 占据剩余宽度
                        text(ctx.inputText) // 绑定输入文本
                        placeholder("需要做什么?")
                        fontSize(16f)
                        backgroundColor(Color(0xFFF5F5F5))
                        borderRadius(8f)
                        height(44f)
                    }
                    event {
                        // 监听文本变化事件,更新数据模型
                        textDidChange { params ->
                            ctx.inputText = params.text
                        }
                    }
                }

                // 添加按钮组件
                View {
                    attr {
                        width(80f)
                        height(44f)
                        marginLeft(10f)
                        backgroundColor(Color(0xFF007AFF))
                        borderRadius(8f)
                        justifyContentCenter() // 内容居中
                        alignItemsCenter()
                        // 增加按钮投影效果
                        boxShadow(BoxShadow(0f, 2f, 4f, Color(0x40007AFF)))
                    }
                    Text {
                        attr {
                            text("添加")
                            color(Color.WHITE)
                            fontWeightBold()
                            fontSize(16f)
                        }
                    }
                    event {
                        // 绑定点击事件,调用添加逻辑
                        click { ctx.addTodo() }
                    }
                }
            }

            // 列表显示区域
            View {
                attr {
                    flex(1f) // 占据剩余空间
                }

                // 空状态提示:当列表为空时显示
                vif({ ctx.todoList.isEmpty() }) {
                    View {
                        attr {
                            flex(1f)
                            justifyContentCenter()
                            alignItemsCenter()
                        }
                        Text {
                            attr {
                                text("暂无待办事项")
                                fontSize(18f)
                                color(Color(0xFF999999))
                                marginBottom(8f)
                            }
                        }
                        Text {
                            attr {
                                text("快去添加一个吧!")
                                fontSize(14f)
                                color(Color(0xFFCCCCCC))
                            }
                        }
                    }
                }
                
                // 列表渲染循环:遍历 todoList 生成列表项
                vfor({ ctx.todoList }) { item ->
                    View {
                        attr {
                            flexDirectionColumn()
                        }
                        
                        // 列表项内容行
                        View {
                            attr {
                                flexDirectionRow()
                                alignItemsCenter()
                                padding(top = 12f, bottom = 12f)
                            }

                            // 复选框组件(自定义 View 模拟)
                            View {
                                attr {
                                    width(24f)
                                    height(24f)
                                    borderRadius(12f)
                                    // 根据完成状态设置边框和背景色
                                    border(Border(2f, BorderStyle.SOLID, if (item.isCompleted) Color(0xFF007AFF) else Color(0xFFCCCCCC)))
                                    backgroundColor(if (item.isCompleted) Color(0xFF007AFF) else Color.TRANSPARENT)
                                    marginRight(12f)
                                    justifyContentCenter()
                                    alignItemsCenter()
                                }
                                event {
                                    // 点击复选框切换状态
                                    click { ctx.toggleTodo(item) }
                                }
                                // 勾选图标
                                Text {
                                    attr {
                                        text("✓")
                                        color(Color.WHITE)
                                        fontSize(14f)
                                        fontWeightBold()
                                        // 控制勾选标记的透明度
                                        opacity(if (item.isCompleted) 1f else 0f)
                                    }
                                }
                            }

                            // 待办事项文本内容
                            Text {
                                attr {
                                    text(item.content)
                                    fontSize(16f)
                                    // 完成状态文字颜色变浅
                                    color(if (item.isCompleted) Color(0xFF999999) else Color(0xFF333333))
                                    flex(1f)
                                    // 完成状态添加删除线
                                    if (item.isCompleted) textDecorationLineThrough() else "textDecoration" with "none"
                                }
                                event {
                                    // 点击文字也可切换状态
                                    click { ctx.toggleTodo(item) }
                                }
                            }

                            // 删除按钮
                            View {
                                attr {
                                    padding(8f) // 增加点击热区
                                }
                                event {
                                    click { ctx.deleteTodo(item) }
                                }
                                Text {
                                    attr {
                                        text("✕")
                                        color(Color(0xFFFF3B30)) // 红色警示色
                                        fontSize(18f)
                                    }
                                }
                            }
                        }
                        
                        // 分割线
                        View {
                            attr {
                                height(1f)
                                backgroundColor(Color(0xFFEEEEEE))
                                marginLeft(36f) // 缩进以对齐文本
                            }
                        }
                    }
                }
            }

            // 底部状态栏与操作区(仅当列表不为空时显示)
            vif({ ctx.todoList.isNotEmpty() }) {
                View {
                    attr {
                        flexDirectionColumn()
                    }

                    // 顶部分割线
                    View {
                        attr {
                            height(1f)
                            backgroundColor(Color(0xFFEEEEEE))
                        }
                    }

                    // 底部内容容器
                    View {
                        attr {
                            flexDirectionRow()
                            justifyContentSpaceBetween() // 两端对齐
                            alignItemsCenter()
                            padding(top = 16f)
                        }
                        
                        // 剩余待办计数
                        Text {
                            attr {
                                text("${ctx.todoList.count { !it.isCompleted }} 项待办")
                                fontSize(14f)
                                color(Color(0xFF666666))
                            }
                        }
                        
                        // 清除已完成按钮
                        View {
                            attr {
                               padding(8f)
                            }
                            event {
                                click { ctx.clearCompleted() }
                            }
                            Text {
                                attr {
                                    text("清除已完成")
                                    fontSize(14f)
                                    color(Color(0xFF007AFF))
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}


5. 跨端运行指南

5.1 Android 运行

Android 是最简单的运行平台。

  1. 打开 Android Studio。
  2. 等待 Gradle Sync 完成。
  3. 在运行配置下拉菜单中选择 androidApp
  4. 点击运行按钮 (Run)。

修改完代码后要先重新构建
在这里插入图片描述
构建完成后点击最上面的运行
安卓运行效果
在这里插入图片描述
在这里插入图片描述

提示:如果遇到 KMP 相关的配置错误,请确保你的 Android Studio 安装了 Kotlin Multiplatform Mobile 插件。

5.2 HarmonyOS 运行

鸿蒙的运行流程涉及 Native 库的交叉编译,需要特别注意。

步骤 1:编译 Native 库

KuiklyUI 的核心逻辑需要编译成鸿蒙系统的动态链接库 (.so)。

  1. 找到项目根目录下的 2.0_ohos_demo_build.bat 文件。
  2. 双击运行,或者在终端中执行:
    .\2.0_ohos_demo_build.bat
    
  3. 脚本会自动执行 Gradle 任务,编译生成 libshared.so 并复制到 ohosApp/entry/libs/arm64-v8a/ 目录下。

    注意:如果脚本报错找不到 SDK,请检查章节 2.1 中的环境变量配置。

步骤 2:配置 DevEco Studio
  1. 启动 DevEco Studio。
  2. 选择 Open Project,打开项目中的 ohosApp 目录。
  3. 等待 Sync 完成。
步骤 3:实现 Native Bridge (可选但推荐)

如果你的应用需要跳转页面或调用原生能力,需要配置 Bridge。
打开 ohosApp/entry/src/main/ets/kuikly/modules/KRBridgeModule.ets,完善 openPage 方法:

// KRBridgeModule.ets
import router from '@ohos.router';

// ... 

// TODO open new page
private openPage(params: KRAny) {
  console.info('KRBridgeModule: openPage method execution started');
  try {
    let args: Record<string, Object>;
    if (typeof params === 'string') {
      args = JSON.parse(params) as Record<string, Object>;
    } else {
      args = params as Record<string, Object>;
    }
    const url = args['url'] as string;
    
    // 使用鸿蒙原生路由跳转
    router.pushUrl({
      url: 'pages/Index',
      params: {
        pageName: url // 将目标页面名传给 Index 页面
      }
    });
  } catch (e) {
    console.error(`KRBridgeModule: openPage error: ${JSON.stringify(e)}`);
  }
}
步骤 4:真机运行

由于 Kuikly 编译的是 ARM64 架构的 .so 库,不支持 x86 模拟器

  1. 连接你的鸿蒙真机 (Mate 60 / Pura 70 等)。

  2. 或者申请 华为云真机 (参考我们另一篇云真机部署教程)。

  3. 点击 DevEco Studio 的运行按钮。

    真机运行效果
    在这里插入图片描述
    在这里插入图片描述


6. 调试技巧与常见问题

6.1 如何查看日志?

  • Android: 使用 Logcat,过滤 KuiklySystem.out
  • HarmonyOS: 使用 DevEco Studio 的 Log 面板,过滤 HiLog,关键字 KuiklyKRBridgeModule
    • 在 Kotlin 代码中可以使用 KLog.i("Tag", "Message") 打日志。

6.2 常见编译错误

  • Unresolved reference: System:
    • 原因: Kotlin/Native (鸿蒙/iOS) 中没有 Java 的 System 类。
    • 解决: 使用 Clock.System.now() (需要 kotlinx-datetime) 或者简单的自增 ID (如本教程所示)。
  • settings.2.0.ohos.gradle.kts does not exist:
    • 原因: 缺少鸿蒙专用构建配置。
    • 解决: 运行 .bat 脚本前确保该文件存在 (脚本通常会自动处理或依赖项目预设)。

7. 总结

通过这个实战,我们不仅完成了一个 Todo 应用,还深入理解了:

  1. MVI 架构雏形:State (状态) -> UI (视图) -> Event (事件) 的单向数据流。
  2. DSL 布局:如何用 Kotlin 代码构建复杂的 Flexbox 布局。
  3. 多端适配:如何通过一套代码,在 Android 和 HarmonyOS 上运行原生的 UI。

KuiklyUI 让跨平台开发不再是 WebView 的妥协,而是原生性能的释放。快去尝试构建更复杂的应用吧!

8. 延伸阅读

欢迎加入开源鸿蒙跨平台社区:开源鸿蒙跨平台开发者社区

版权声明:本文遵循 CC 4.0 BY-SA 协议,转载请注明出处。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐