前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

欢迎来到 KuiklyUI 的实战世界。

如果你是一名跨平台开发的初学者,或是对鸿蒙生态下的高性能 UI 框架充满好奇,那么这篇文章将是你最理想的入门指南。我们将以经典的 Todo 待办事项应用为载体,从 0 到 1 完整实现一个可以运行在 Android、iOS、鸿蒙等多平台的高性能应用。

在这篇文章里,你将学到:

  • KuiklyUI 框架的核心概念与优势
  • 基于 Kotlin Multiplatform (KMP) 的项目结构
  • 数据模型与 ViewModel 的设计与实现
  • 页面布局与组件的使用
  • 新增、删除、状态切换等核心功能的逻辑编写
  • 数据持久化与多端适配
  • 项目的运行、调试与打包

整篇教程一步一代码、一步一原理,哪怕你是第一次接触 KuiklyUI,也能轻松跟着完成。

一、什么是 KuiklyUI?

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

核心优势

  • 极致性能:核心逻辑编译为 Native 二进制代码,渲染性能接近原生应用,帧率稳定在 60fps。
  • 原生体验:直接调用各平台原生渲染引擎,而非 WebView 或自绘引擎,保证了与系统的无缝融合。
  • 一次编写,多端运行:一套 Kotlin 代码,可同时编译运行在 Android、iOS、鸿蒙、Web 等平台。
  • 轻量无负担:框架体积小,编译速度快,对项目的侵入性极低。
  • 声明式语法:采用现代声明式 UI 写法,代码直观易读,开发效率高。

二、开发环境搭建与项目创建

在开始编码前,我们需要先准备好开发环境。

2.1 环境准备

  1. 安装 JDK:KuiklyUI 基于 Kotlin,需要 JDK 11 或更高版本。
  2. 安装 IDE:推荐使用 Android Studio,它对 Kotlin Multiplatform 项目支持最完善。
  3. 配置 Kotlin 插件:确保 Android Studio 中的 Kotlin 插件已更新到最新版。

2.2 创建项目

打开 Android Studio,选择“New Project”,在模板中找到 KuiklyUI 官方提供的 KMP 项目模板,或手动创建一个 KMP 项目。项目名称我们可以取名为 KuiklyUIDemo

创建成功后,项目结构大致如下:

  • commonMain:存放共享的业务逻辑和 UI 代码,是我们主要的工作区。
  • androidMain:Android 平台特定代码。
  • iosMain:iOS 平台特定代码。
  • harmonyMain:鸿蒙平台特定代码。

三、需求分析:我们要做一个什么样的 Todo App?

在动手写代码前,先明确我们要实现的核心功能,这样思路会更清晰:

  1. 展示待办事项列表:以列表形式展示所有待办事项。
  2. 添加新事项:通过输入框和按钮,新增待办事项。
  3. 标记完成/未完成:点击事项可以切换其完成状态。
  4. 删除事项:提供删除功能,移除不需要的事项。
  5. 数据持久化:关闭应用后再次打开,之前的任务依然存在。

这些功能覆盖了从数据建模到 UI 交互的完整开发流程,是入门学习的绝佳案例。

四、核心功能实现

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

)

这个数据类包含三个字段:

  • id:唯一标识符,用于区分不同的待办事项。
  • content:待办事项的内容文本。
  • isCompleted:标记事项是否完成,默认为 false

4.2 创建页面逻辑类 (ViewModel)

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

/**

 * @ProjectName : KuiklyUI

 * @Author : GuoJiaHui

 * @Time : 2026年01月29日 17:30

 * @Description : 待办事项页面逻辑

 * 负责管理待办事项列表的数据状态、处理用户交互事件(添加、删除、切换状态)

 */

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

import com.tencent.kuikly.core.mutableStateListOf

import com.tencent.kuikly.core.mutableStateOf

import com.tencent.kuikly.core.viewModel

import com.tencent.kuikly.demo.storage.TodoStorage

class TodoPage : ViewModel() {

    // 待办事项列表,使用 mutableStateListOf 实现响应式更新

    private val _todoList = mutableStateListOf<TodoItem>()

    val todoList: List<TodoItem> = _todoList

    // 输入框的文本内容

    val inputText = mutableStateOf("")

    // 页面加载时,从本地存储读取数据

    override fun onLoad() {

        super.onLoad()

        _todoList.addAll(TodoStorage.loadTodos())

    }

    // 添加新的待办事项

    fun addTodo() {

        val content = inputText.value.trim()

        if (content.isNotEmpty()) {

            val newTodo = TodoItem(

                id = System.currentTimeMillis(),

                content = content

            )

            _todoList.add(newTodo)

            TodoStorage.saveTodos(_todoList)

            inputText.value = "" // 清空输入框

        }

    }

    // 切换待办事项的完成状态

    fun toggleTodoStatus(item: TodoItem) {

        val index = _todoList.indexOf(item)

        if (index != -1) {

            _todoList[index] = _todoList[index].copy(

                isCompleted = !_todoList[index].isCompleted

            )

            TodoStorage.saveTodos(_todoList)

        }

    }

    // 删除待办事项

    fun deleteTodo(item: TodoItem) {

        _todoList.remove(item)

        TodoStorage.saveTodos(_todoList)

    }

}

这个 ViewModel 做了几件关键的事:

  1. 状态管理:使用 mutableStateListOf 和 mutableStateOf 来管理响应式数据,当数据变化时,UI 会自动更新。
  2. 业务逻辑:封装了 addTodotoggleTodoStatusdeleteTodo 等核心操作。
  3. 数据持久化:在每次数据变更后,调用 TodoStorage 进行保存。
  4. 生命周期:在 onLoad 生命周期中,从本地存储恢复数据。

4.3 实现数据持久化(这个可以不用写)

为了让数据在应用关闭后不丢失,我们需要实现一个简单的本地存储工具类。在 demo/src/commonMain/kotlin/com/tencent/kuikly/demo/storage 目录下创建 TodoStorage.kt

package com.tencent.kuikly.demo.storage

import com.tencent.kuikly.core.Storage

import com.tencent.kuikly.demo.pages.todo.TodoItem

import kotlinx.serialization.decodeFromString

import kotlinx.serialization.encodeToString

import kotlinx.serialization.json.Json

object TodoStorage {

    private const val STORAGE_KEY = "todo_list"

    private val json = Json { ignoreUnknownKeys = true }

    fun saveTodos(todos: List<TodoItem>) {

        val jsonString = json.encodeToString(todos)

        Storage.put(STORAGE_KEY, jsonString)

    }

    fun loadTodos(): List<TodoItem> {

        val jsonString = Storage.get(STORAGE_KEY) ?: return emptyList()

        return try {

            json.decodeFromString<List<TodoItem>>(jsonString)

        } catch (e: Exception) {

            emptyList()

        }

    }

}

这个工具类使用 KuiklyUI 内置的 Storage 接口,结合 Kotlin 的序列化库,将待办事项列表以 JSON 字符串的形式保存到本地,并在需要时读取和反序列化。

4.4 编写 UI 页面

现在,我们来编写 Todo 应用的主页面 TodoPageUI.kt。这个文件将使用 KuiklyUI 的声明式语法,将 ViewModel 中的数据和逻辑与 UI 组件绑定。

Page {

    // 关联 ViewModel

    val viewModel = remember { TodoPage() }

    Column(

        modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))

    ) {

        // 顶部标题栏

        Text(

            text = "我的待办",

            modifier = Modifier.fillMaxWidth().padding(16.dp),

            style = TextStyle(

                fontSize = 24.sp,

                fontWeight = FontWeight.Bold,

                color = Color(0xFF333333)

            ),

            textAlign = TextAlign.Center

        )

        // 输入区域

        Row(

            modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),

            verticalAlignment = Alignment.CenterVertically

        ) {

            TextField(

                value = viewModel.inputText.value,

                onValueChange = { viewModel.inputText.value = it },

                modifier = Modifier.weight(1f).height(48.dp),

                placeholder = { Text("请输入待办事项...") }

            )

            Button(

                onClick = { viewModel.addTodo() },

                modifier = Modifier.padding(start = 8.dp).height(48.dp),

                colors = ButtonDefaults.buttonColors(

                    containerColor = Color(0xFF6200EE)

                )

            ) {

                Text("添加", color = Color.White)

            }

        }

        // 待办事项列表

        LazyColumn(

            modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp)

        ) {

            items(viewModel.todoList) { item ->

                Row(

                    modifier = Modifier

                        .fillMaxWidth()

                        .padding(vertical = 8.dp)

                        .background(Color.White, RoundedCornerShape(8.dp))

                        .padding(16.dp)

                        .clickable { viewModel.toggleTodoStatus(item) },

                    verticalAlignment = Alignment.CenterVertically

                ) {

                    Checkbox(

                        checked = item.isCompleted,

                        onCheckedChange = { viewModel.toggleTodoStatus(item) }

                    )

                    Text(

                        text = item.content,

                        modifier = Modifier.weight(1f).padding(start = 12.dp),

                        style = TextStyle(

                            fontSize = 16.sp,

                            color = if (item.isCompleted) Color.Gray else Color(0xFF333333),

                            textDecoration = if (item.isCompleted) TextDecoration.LineThrough else null

                        )

                    )

                    IconButton(

                        onClick = { viewModel.deleteTodo(item) }

                    ) {

                        Icon(

                            imageVector = Icons.Default.Delete,

                            contentDescription = "删除",

                            tint = Color(0xFFFF4444)

                        )

                    }

                }

            }

        }

    }

}

这个页面主要由三部分组成:

  1. 标题栏:显示应用名称。
  2. 输入区域:包含一个文本输入框和一个“添加”按钮,用于新增待办事项。
  3. 列表区域:使用 LazyColumn 高效渲染待办事项列表,每一项包含复选框、内容文本和删除按钮。

UI 与 ViewModel 的绑定非常紧密:

  • TextField 的 value 绑定到 viewModel.inputText.value,并在 onValueChange 时更新它。
  • 列表的数据源是 viewModel.todoList,当列表变化时,LazyColumn 会自动刷新。
  • 点击事件(添加、切换状态、删除)都直接调用 ViewModel 中的方法。

改为其他代码

/**

 * @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))

                            }

                        }

                    }

                }

                // 列表循环渲染

                vfor({ ctx.todoList }) { item ->

                    View {

                        attr {

                            flexDirectionColumn()

                        }

                        // 列表项行

                        View {

                            attr {

                                flexDirectionRow()

                                alignItemsCenter()

                                padding(top = 12f, bottom = 12f)

                            }

                            // 复选框

                            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.1 运行项目

在 Android Studio 中,选择目标平台(如 Android 模拟器或 iOS 模拟器),点击“Run”按钮,即可编译并运行项目。

5.2 功能测试

  1. 在输入框中输入内容,点击“添加”按钮,列表中会出现新的待办事项。
  2. 点击事项前方的复选框或事项本身,事项会被标记为已完成,文字变为灰色并带有删除线。
  3. 点击事项右侧的删除图标,该事项会从列表中移除。
  4. 关闭应用后再次打开,之前添加的事项依然存在。

5.3 常见问题排查

  • UI 不更新:检查数据是否使用了 mutableStateOf 或 mutableStateListOf,只有响应式数据才能触发 UI 更新。
  • 数据不保存:检查 TodoStorage 中的键名是否一致,以及序列化/反序列化逻辑是否正确。
  • 按钮无响应:检查 onClick 回调是否正确绑定到了 ViewModel 的方法。

5.4、环境搭建与项目创建类问题

  1. 安装 JDK 后,命令行输入 java -version 提示找不到命令,无法继续环境配置。
  2. Android Studio 中找不到 KuiklyUI 插件,插件市场搜索不到,无法安装。
  3. 创建项目时没有 KuiklyUI KMP 模板,无法新建项目。
  4. 项目创建后一直卡在 Gradle 下载/同步,长时间不动,无法进入编码。
  5. Kotlin 版本不匹配,提示 KuiklyUI 依赖加载失败,项目无法构建。
  6. 打开项目后 commonMain 目录标红,代码全报错,无法正常编写。

5.5、项目运行与真机调试类问题

  1. 点击运行按钮 直接报错,应用安装失败,无法在手机上打开。
  2. 连接真机后 Android Studio 识别不到设备,无法运行调试。
  3. 应用编译成功,但手机安装后闪退/打不开,一打开就关闭。
  4. 运行时报错 Minimum SDK 版本过低,无法在目标设备运行。
  5. iOS / 鸿蒙平台 无法运行,只有 Android 能跑,多端适配失败。
  6. 预览面板不显示,UI 预览加载失败,无法实时查看界面。

5.6、代码编写与功能不生效问题

  1. 编写 TodoItem 数据类后,@Serializable 标红报错,无法序列化。
  2. ViewModel 中 mutableStateOf / mutableStateListOf 找不到,代码无法运行。
  3. 输入框输入内容后 添加按钮无反应,待办事项添加不进去。
  4. 点击复选框 状态无法切换,已完成/未完成样式不变化。
  5. 删除按钮点击后 事项不消失,列表无法刷新。
  6. 关闭应用再打开 数据全部丢失,持久化不生效。
  7. TodoStorage 存储工具类 报错无法使用,提示找不到存储接口。
  8. LazyColumn 列表 不显示任何内容,界面一片空白。

页面文字、按钮、输入框 样式错乱、位置重叠,布局无法正常显示。

六、总结与进阶

通过这个 Todo 应用,你已经掌握了 KuiklyUI 开发的核心流程:

  1. 数据建模:使用 Kotlin 数据类定义应用状态。
  2. 逻辑分离:使用 ViewModel 管理业务逻辑和状态,实现了关注点分离。
  3. 响应式 UI:利用 KuiklyUI 的声明式语法,实现了数据驱动的界面更新。
  4. 数据持久化:了解了如何在多平台项目中实现数据的本地存储。

这些知识是构建更复杂应用的基石。如果你想继续深入,可以学习以下内容:

  • 网络请求:集成 Retrofit 或 Ktor,从服务器获取和提交数据。
  • 导航与路由:实现多页面应用,学习页面之间的跳转和参数传递。
  • 主题与样式:深入学习 KuiklyUI 的主题系统,打造更美观的应用。
  • 原生能力调用:通过 Native Bridge 调用相机、定位等系统原生功能。

恭喜你,已经成功迈出了 KuiklyUI 开发的重要一步!

结语

KuiklyUI 凭借其卓越的性能和简洁的语法,正在成为跨平台开发的有力工具。希望这篇实战教程能帮助你打开跨平台开发的大门,未来你可以用 KuiklyUI 构建更多高效、流畅的应用。

如果你在实践中遇到任何问题,欢迎查阅 KuiklyUI 的官方文档或参与社区讨论。

Logo

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

更多推荐