前言:基于KuiklyUI的跨平台开发实战

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

在跨平台开发领域,Flutter、React Native 早已成为主流选择,但它们或依赖自绘引擎、或基于 WebView 桥接,在原生性能平台一致性上仍有优化空间。而腾讯开源的 Kuikly 框架,以 Kotlin Multiplatform(KMP)为核心,采用「Kotlin DSL 驱动原生渲染」的独特路线,完美解决了这一痛点——它不引入额外渲染引擎,直接将 DSL 映射为 Android 的 View、iOS 的 UIKit、HarmonyOS 的 ArkUI 原生组件,实现了「一套代码、多端原生体验」。

汇率计算器作为高频刚需的轻量级工具,无需复杂业务逻辑,却涵盖了网络请求、状态管理、原生入口配置、跨端适配等核心开发场景,是入门 Kuikly 框架的最佳实战项目。本文将完全基于 KuiklyUI-mini 精简模板,从环境搭建、项目解构、核心业务开发,到 Android 与 HarmonyOS 双端原生入口配置、调试打包,手把手带你完成 Kuikly-exchange-rate 项目的全流程开发。

本文核心适配新手,全程无冗余理论,所有步骤均与实际代码落地强绑定,配套开源模板与成品项目地址,克隆即可上手开发。

项目开源地址

一、项目背景与Kuikly架构核心

1.1 为什么选择 Kuikly 开发本项目?

对比传统跨平台框架,Kuikly 对汇率计算器这类轻量工具的适配性极强,核心优势体现在三点:

  1. 原生性能极致:无 WebView 开销,不自带渲染引擎,编译产物为 Android 的 .aar、HarmonyOS 的 .har/.so,启动速度、内存占用与原生开发几乎无差异,满足汇率实时计算的流畅性需求。
  2. 技术栈统一:全程使用 Kotlin 语言编写,业务逻辑、UI 布局、跨端通信一站式完成,无需切换 Java、Dart 或 ArkTS,大幅降低新手学习成本。
  3. 双端无缝兼容:基于 KuiklyUI-mini 模板,可实现一套代码在 Android 与 HarmonyOS 端同时运行,原生入口配置逻辑统一,无需为不同平台单独编写启动代码。

1.2 KuiklyUI-mini 架构解构(核心重点)

KuiklyUI-mini 的核心设计是业务与宿主分离,这也是 Kuikly 跨平台能力的核心体现,项目整体分为「共享层」与「宿主层」两大部分,结构清晰且职责明确:

KuiklyUI-mini/

├── androidApp/       // Android 宿主工程(启动入口+原生能力桥接)

├── ohosApp/          // HarmonyOS 宿主工程(启动入口+ArkUI 容器)

├── shared/           // KMP 共享层(核心!所有跨端复用代码)

│   ├── src/commonMain/  // 通用代码目录(UI、业务、模型、工具类)

│   └── build.gradle.kts // 共享层构建配置

└── settings.gradle.kts  // 项目模块管理

  1. Shared 模块(大脑):包含汇率计算器的所有核心代码——UI 布局(Kotlin DSL)、汇率数据模型、网络请求封装、计算逻辑、本地存储等,编译后会生成对应平台的中间产物,供宿主层调用。
  2. Host Apps 模块(躯壳):仅负责启动应用提供原生能力桥接(如网络权限、本地存储)、配置原生入口,不参与核心业务逻辑,保证跨端代码的纯粹性。

1.3 Kuikly 原生渲染原理(极简理解)

当我们在 Shared 层用 Kotlin DSL 编写 View { attr { backgroundColor(Color.WHITE) } } 时,底层会完成四步核心操作,实现跨端原生渲染:

  1. 构建虚拟 DOM:Kotlin 代码运行时生成轻量级虚拟组件树;
  2. 精准 Diff 计算:状态变化时,框架仅计算新旧组件树的差异,避免全量渲染;
  3. 分发渲染指令:将差异转化为 Create、Update、Delete 等指令序列;
  4. 原生组件映射:指令传递到对应平台,Android 端生成 FrameLayout 等 View 组件,HarmonyOS 端生成 Stack、Column 等 ArkUI 组件。

二、前期准备:环境搭建与项目初始化

2.1 必备开发环境(版本严格匹配)

为避免构建失败、调试异常,需严格按照以下版本配置环境,这是新手开发的关键前提:

工具/环境

推荐版本

核心作用

JDK

17+

Kotlin 与 Kuikly 编译的基础环境

Android Studio

最新稳定版

Android 端开发、调试、打包,需安装 Kotlin 插件

DevEco Studio

5.0+

HarmonyOS 端开发、调试、打包,需安装鸿蒙 SDK(API 11+)

Gradle

8.0+

项目构建工具,Kuikly 工程核心依赖

Kuikly 插件

1.1.0+

提供 Kuikly 工程模板、编译支持(Android Studio 中安装)

2.2 环境验证步骤

  1. Android 端验证:打开 Android Studio,新建空 Kotlin 项目,运行后模拟器正常显示页面,说明环境无误;
  2. HarmonyOS 端验证:打开 DevEco Studio,新建空 ArkTS 项目,完成签名配置后运行,模拟器正常显示页面,说明环境无误。

2.3 项目初始化:克隆模板并配置

  1. 克隆模板项目:从开源地址拉取 KuiklyUI-mini 模板,解压到本地开发目录;
  2. 导入项目:用 Android Studio 打开模板根目录,等待 Gradle 自动下载依赖(首次下载需保证网络畅通,耗时稍长);
  3. 依赖问题排查:若出现「Gradle 版本不匹配」,在 File -> Project Structure -> Project 中切换为 8.0+ 版本;若出现「依赖下载失败」,可手动同步 Gradle 或检查网络代理。

三、核心前置配置:路由与原生入口(跨端启动关键)

在编写业务代码前,必须先完成路由定义原生入口配置——这是连接「宿主层」与「共享层」的桥梁,决定了应用启动后加载哪个页面。

3.1 路由系统设计(Shared 层)

路由是页面的唯一标识,同时也是原生层与跨端层通信的协议。在 shared/src/commonMain/kotlin/com/goway/kuiklymini/Routes.kt 中定义核心页面路由:

// 路由常量类,统一管理所有页面标识

object Routes {

    // 模板默认首页(功能列表页)

    const val ROUTER_PAGE = "router"

    // 汇率计算器核心页面(本次开发核心)

    const val EXCHANGE_RATE_PAGE = "exchange_rate"

}

设计思考:使用 const val 定义路由,既保证 Kotlin 代码中的调用一致性,也方便后续原生端(如 DeepLink 跳转)直接引用,避免硬编码导致的页面匹配失败。

3.2 Android 端原生入口配置(宿主层)

Android 端的启动入口是 Activity,核心是在宿主工程中配置「启动时加载的 Kuikly 页面」。

步骤 1:定位核心文件

打开 androidApp/src/main/kotlin/com/goway/kuiklymini/KuiklyRenderActivity.kt,这是 Kuikly 页面的渲染容器。

步骤 2:修改默认启动页面

在文件中找到 pageName 方法,修改默认返回值为汇率计算器页面路由,实现启动即进入核心功能页:

// 定义当前需要渲染的 Kuikly 页面名称

private val pageName: String

    get() {

        // 1. 优先获取外部 Intent 传入的页面名(支持外部跳转)

        val pn = intent.getStringExtra(KEY_PAGE_NAME) ?: ""

        return if (pn.isNotEmpty()) {

            pn

        } else {

            // 2. 默认启动汇率计算器页面(核心修改点)

            Routes.EXCHANGE_RATE_PAGE

        }

    }

步骤 3:配置页面初始化数据

createPageData 方法中,可向跨端页面传递原生环境参数(如 appId、设备信息),本项目暂传递空参数,后续可扩展:

// 向 Kuikly 页面传递初始化数据(Map 格式)

private fun createPageData(): Map<String, Any> {

    val param = argsToMap() // 解析 Intent 中的参数

    // 注入自定义原生参数,示例:param["deviceType"] = "Android"

    return param

}

步骤 4:配置网络权限

汇率计算器需要请求实时汇率接口,需在 androidApp/src/main/AndroidManifest.xml 中添加网络权限:

<!-- 网络请求权限 -->

<uses-permission android:name="android.permission.INTERNET" />

<!-- 网络状态获取权限(可选,用于判断网络是否可用) -->

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

3.3 HarmonyOS 端原生入口配置(宿主层)

HarmonyOS 端采用 ArkTS 开发,启动入口是 EntryAbility,核心是在 ArkUI 页面中配置 Kuikly 渲染容器的启动参数。

步骤 1:定位核心文件

打开 ohosApp/entry/src/main/ets/pages/Index.ets,这是鸿蒙端的启动页面,也是 Kuikly 组件的承载页面。

步骤 2:配置 Kuikly 组件参数

build 方法中,修改 Kuikly 组件的 pagerName 参数,指定启动汇率计算器页面,并绑定生命周期与原生桥接:

@Entry

@Component

struct Index {

    // 页面名称,默认指向汇率计算器

    private pageName: string = Routes.EXCHANGE_RATE_PAGE;

    // 页面初始化数据

    private pageData: object = {};

    // Kuikly 视图代理,处理生命周期

    private kuiklyViewDelegate: KuiklyViewDelegate = new KuiklyViewDelegate();

    // 原生管理器,用于跨端通信

    private nativeManager: KRBridgeModule = new KRBridgeModule();

    build() {

        Stack() {

            // Kuikly 核心渲染组件(鸿蒙端)

            Kuikly({

                pagerName: this.pageName,

                pagerData: this.pageData,

                delegate: this.kuiklyViewDelegate,

                nativeManager: this.nativeManager

            })

        }

        .width('100%')

        .height('100%')

    }

}

步骤 3:配置鸿蒙端权限与签名
  1. 权限配置:打开 ohosApp/entry/src/main/module.json5,在 abilities 下添加网络权限:

"abilities": [

    {

        // 其他配置省略

        "permissions": [

            "ohos.permission.INTERNET"

        ]

    }

]

  1. 签名配置:在 DevEco Studio 中,通过 File -> Project Structure -> Signing Configs 完成鸿蒙应用签名,否则无法在模拟器或真机上运行。

四、核心业务开发:汇率计算器(Shared 层)(可以不用看)

Shared 层是本项目的核心,所有跨端复用的业务代码均在此编写。本节将从数据模型、网络请求、UI 布局、核心逻辑四个维度,完成汇率计算器的开发。

4.1 数据模型定义(Currency & ExchangeRate)

shared/src/commonMain/kotlin/com/goway/kuiklymini/model 目录下,创建两个核心数据类,分别封装「币种信息」与「汇率数据」,适配 Kuikly 的响应式状态管理。

4.1.1 币种数据模型(Currency.kt)

// 币种信息数据类,包含核心标识与展示信息

data class Currency(

    val code: String, // 币种代码(如 USD、CNY)

    val name: String, // 中文名称(如 美元、人民币)

    val symbol: String // 货币符号(如 $、¥)

)

4.1.2 汇率数据模型(ExchangeRate.kt)

// 实时汇率数据类,以 USD 为基准货币

data class ExchangeRate(

    val base: String, // 基准币种(固定为 USD)

    val rates: Map<String, Double>, // 币种代码 -> 汇率值

    val updateTime: Long // 汇率更新时间(时间戳)

)

4.1.3 响应式状态封装

Kuikly 支持响应式编程,通过 mutableStateOf 封装页面状态,状态变化时自动触发 UI 刷新。在页面类中定义核心状态:

// 源币种(默认人民币)

private val sourceCurrency = mutableStateOf(Currency("CNY", "人民币", "¥"))

// 目标币种(默认美元)

private val targetCurrency = mutableStateOf(Currency("USD", "美元", "$"))

// 输入金额(默认空)

private val inputAmount = mutableStateOf("")

// 换算结果(默认 0.00)

private val resultAmount = mutableStateOf("0.00")

// 实时汇率数据(默认空)

private val exchangeRate = mutableStateOf<ExchangeRate?>(null)

// 网络请求状态(用于展示加载/错误提示)

private val isLoading = mutableStateOf(false)

4.2 网络请求封装:获取实时汇率

本项目使用公开免费汇率接口(无需申请密钥),封装 Kuikly 兼容的网络请求工具,实现汇率数据的获取、缓存与异常处理。

4.2.1 网络请求工具类(ApiService.kt)

shared/src/commonMain/kotlin/com/goway/kuiklymini/service 目录下创建 ApiService.kt,使用 Kotlin 协程实现异步请求:

import io.ktor.client.*

import io.ktor.client.call.*

import io.ktor.client.plugins.contentnegotiation.*

import io.ktor.client.request.*

import io.ktor.serialization.kotlinx.json.*

import kotlinx.serialization.json.Json

// 汇率接口地址(免费公开,以 USD 为基准)

private const val EXCHANGE_RATE_URL = "https://api.exchangerate-api.com/v4/latest/USD"

// 网络请求单例类

object ApiService {

    // 初始化 Ktor 客户端(Kuikly 推荐的网络请求库)

    private val client by lazy {

        HttpClient {

            install(ContentNegotiation) {

                json(Json {

                    ignoreUnknownKeys = true // 忽略接口中未定义的字段

                    prettyPrint = true

                })

            }

        }

    }

    /**

     * 获取实时汇率数据

     * @return ExchangeRate? 汇率数据(失败返回 null)

     */

    suspend fun getExchangeRate(): ExchangeRate? {

        return try {

            // 发起 GET 请求并解析为 ExchangeRate 实体

            client.get(EXCHANGE_RATE_URL).body<ExchangeRate>()

        } catch (e: Exception) {

            // 捕获网络异常、解析异常,新手可直接打印日志

            println("获取汇率失败:${e.message}")

            null

        }

    }

}

4.2.2 汇率缓存与离线适配

为保证离线状态下应用可正常使用,在 ApiService 基础上,封装「内存缓存 + 本地存储」的缓存策略(使用 Kuikly 兼容的轻量级存储):

// 内存缓存的汇率数据

private var cacheRate: ExchangeRate? = null

/**

 * 获取带缓存的汇率数据

 * @return ExchangeRate? 优先返回最新数据,失败则返回缓存

 */

suspend fun getCachedExchangeRate(): ExchangeRate? {

    // 1. 尝试获取最新数据

    val newRate = getExchangeRate()

    if (newRate != null) {

        cacheRate = newRate

        // 2. 保存到本地存储(后续实现)

        saveRateToLocal(newRate)

        return newRate

    }

    // 3. 最新数据获取失败,返回内存缓存

    return cacheRate ?: getRateFromLocal()

}

// 本地存储相关方法(后续实现)

private suspend fun saveRateToLocal(rate: ExchangeRate) {}

private suspend fun getRateFromLocal(): ExchangeRate? { return null }

4.3 UI 布局开发:Kuikly DSL 实现

shared/src/commonMain/kotlin/com/goway/kuiklymini/pages 目录下,创建 ExchangeRatePage.kt,继承 Kuikly 的 Pager 类,通过自研 DSL 编写核心界面。

汇率计算器界面分为 4 个核心区域:金额输入区币种切换区结果展示区汇率更新提示区,采用 Flex 布局实现跨端适配。

import com.tencent.kuikly.core.base.Color

import com.tencent.kuikly.core.pager.Pager

import com.tencent.kuikly.core.views.*

import com.tencent.kuikly.core.directives.vif

// 汇率计算器核心页面

internal class ExchangeRatePage : Pager() {

    // 响应式状态(前文已定义,此处省略)

    override fun body() = buildUI()

    // 构建页面 UI

    private fun buildUI() = View {

        attr {

            flex(1f) // 充满全屏

            backgroundColor(Color(0xFFF5F7FA)) // 页面背景色

            padding(20f) // 全局内边距

            flexDirectionColumn() // 垂直布局

            alignItemsCenter() // 水平居中

        }

        // 1. 金额输入区

        Input {

            attr {

                width("100%")

                height(60f)

                fontSize(24f)

                placeholder("请输入换算金额")

                backgroundColor(Color.WHITE)

                borderRadius(12f)

                padding(left = 16f, right = 16f)

                text(inputAmount.value) // 绑定输入状态

            }

            event {

                // 监听输入变化,实时触发计算

                textDidChange { params ->

                    inputAmount.value = params.text

                    calculateResult()

                }

            }

        }

        // 2. 币种切换区(水平布局)

        View {

            attr {

                width("100%")

                height(80f)

                flexDirectionRow()

                justifyContentSpaceBetween()

                alignItemsCenter()

                marginVertical(20f)

            }

            // 源币种选择器

            CurrencyItem(currency = sourceCurrency.value) { currency ->

                sourceCurrency.value = currency

                calculateResult()

            }

            // 切换箭头

            Text {

                attr {

                    text("⇄")

                    fontSize(24f)

                    color(Color(0xFF007AFF))

                }

            }

            // 目标币种选择器

            CurrencyItem(currency = targetCurrency.value) { currency ->

                targetCurrency.value = currency

                calculateResult()

            }

        }

        // 3. 结果展示区

        View {

            attr {

                width("100%")

                height(100f)

                backgroundColor(Color.WHITE)

                borderRadius(12f)

                justifyContentCenter()

                alignItemsCenter()

                boxShadow(0f, 2f, 8f, Color(0x10000000)) // 阴影效果

            }

            Text {

                attr {

                    text("${targetCurrency.value.symbol} ${resultAmount.value}")

                    fontSize(32f)

                    fontWeightBold()

                    color(Color.BLACK)

                }

            }

        }

        // 4. 汇率更新提示区(加载/更新时间)

        vif({ isLoading.value }) {

            Text {

                attr {

                    text("正在获取最新汇率...")

                    fontSize(14f)

                    color(Color(0xFF999999))

                    marginTop(20f)

                }

            }

        } ?: {

            Text {

                attr {

                    val time = exchangeRate.value?.updateTime ?: 0L

                    text("汇率更新于:${formatTime(time)}")

                    fontSize(14f)

                    color(Color(0xFF999999))

                    marginTop(20f)

                }

            }

        }

    }

    // 时间格式化工具方法(后续实现)

    private fun formatTime(timestamp: Long): String {

        return "2026-02-24 12:00" // 临时占位,后续实现具体格式化

    }

}

4.3.1 币种选择组件封装(CurrencyItem.kt)

为实现代码复用,在 shared/src/commonMain/kotlin/com/goway/kuiklymini/widget 目录下,封装 CurrencyItem 自定义组件,支持点击弹出币种选择列表:

// 币种选择项组件

fun ViewBuilder.CurrencyItem(

    currency: Currency,

    onSelect: (Currency) -> Unit // 选择回调

) {

    View {

        attr {

            width(120f)

            height(50f)

            backgroundColor(Color.WHITE)

            borderRadius(8f)

            justifyContentCenter()

            alignItemsCenter()

            border(Border(1f, Color(0xFFE5E7EB), BorderStyle.SOLID))

        }

        Text {

            attr {

                text("${currency.code} (${currency.name})")

                fontSize(16f)

                color(Color.BLACK)

            }

        }

        event {

            // 点击弹出币种选择列表(后续实现)

            click {

                // 暂直接切换为欧元(示例),后续替换为选择器逻辑

                onSelect(Currency("EUR", "欧元", "€"))

            }

        }

    }

}

4.4 核心计算逻辑实现

ExchangeRatePage 中,实现 calculateResult 方法,基于实时汇率完成金额换算,处理空输入、零汇率等异常情况。

/**

 * 核心换算逻辑

 * 公式:目标金额 = 源金额 × (目标币种汇率 / 源币种汇率)

 */

private fun calculateResult() {

    // 1. 校验输入与汇率数据

    val amount = inputAmount.value.toDoubleOrNull() ?: 0.0

    val rateData = exchangeRate.value ?: return

    val sourceRate = rateData.rates[sourceCurrency.value.code] ?: 1.0

    val targetRate = rateData.rates[targetCurrency.value.code] ?: 1.0

    // 2. 避免除以零异常

    if (sourceRate == 0.0) {

        resultAmount.value = "0.00"

        return

    }

    // 3. 执行换算并格式化结果

    val result = amount * (targetRate / sourceRate)

    resultAmount.value = String.format("%.4f", result) // 保留 4 位小数

}

4.5 页面初始化逻辑

ExchangeRatePage 中,重写 onInit 方法,实现页面启动时的汇率数据加载,触发首次计算:

override suspend fun onInit() {

    super.onInit()

    // 加载汇率数据

    isLoading.value = true

    exchangeRate.value = ApiService.getCachedExchangeRate()

    isLoading.value = false

    // 触发首次计算

    calculateResult()

}

五、多端调试与原生入口验证

完成核心业务开发后,需分别在 Android 与 HarmonyOS 端进行调试,验证原生入口配置与跨端功能是否正常。

5.1 Android 端调试

  1. 选择运行配置:在 Android Studio 顶部,选择 androidApp 模块,搭配已连接的 Android 模拟器或真机;
  2. 启动应用:点击「运行」按钮,应用启动后应直接进入汇率计算器页面;
  3. 功能验证
    • 输入金额,查看是否实时显示换算结果;
    • 切换币种,验证计算逻辑是否正常;
    • 关闭网络,验证是否使用缓存汇率(后续完善本地存储后测试)。

5.2 HarmonyOS 端调试

  1. 打开宿主工程:用 DevEco Studio 打开 ohosApp 目录,完成 Gradle 同步;
  2. 选择运行配置:搭配 HarmonyOS 模拟器或真机,选择 entry 模块;
  3. 启动应用:点击「运行」按钮,验证是否正常加载汇率计算器页面;
  4. 功能验证:与 Android 端一致,重点验证网络请求、UI 布局、状态更新是否正常。

六、打包上线:双端原生产物生成

6.1 Android 端打包(生成 APK/AAB)

  1. 构建发布版本:在 Android Studio 中,执行 Gradle -> androidApp -> Tasks -> build -> assembleRelease
  2. 获取产物:打包完成后,在 androidApp/build/outputs/apk/release 目录下获取 APK 文件;
  3. 签名配置:正式上线需配置签名文件,在 build.gradle.kts 中添加签名信息,避免应用安装后无法打开。

6.2 HarmonyOS 端打包(生成 HAP)

  1. 构建发布版本:在 DevEco Studio 中,执行 Build -> Build Hap(s) -> Build Release Hap(s)
  2. 获取产物:在 ohosApp/entry/build/outputs/hap/release 目录下获取 HAP 文件;
  3. 发布准备:根据鸿蒙应用市场要求,完成应用签名、权限审核等流程。

七、常见问题与优化方向

7.1 新手开发常见问题

  1. Gradle 依赖下载失败:检查网络代理,或手动指定 Gradle 镜像源;
  2. Kuikly 组件渲染异常:确保 Kuikly 插件版本与框架版本匹配,重新同步 Gradle;
  3. 网络请求失败:确认已添加网络权限,且接口地址可正常访问。

7.2 项目优化方向

  1. 完善本地存储:使用 Kuikly 兼容的 KMP 本地存储库(如 multiplatform-settings),实现汇率数据的持久化缓存;
  2. 丰富币种选择器:实现底部弹窗式币种列表,支持搜索功能,覆盖 20+ 主流币种;
  3. 添加历史记录:本地存储最近换算记录,支持一键回填;
  4. 深色模式适配:基于 Kuikly 的主题系统,实现浅色/深色模式自动切换。

结语

本文基于 KuiklyUI-mini 模板,完整实现了跨平台汇率计算器的开发流程,核心覆盖了 Kuikly 框架的架构设计、原生入口配置、Kotlin DSL 开发、网络请求封装、跨端调试等核心知识点。

作为大一软件工程专业的新手,通过本项目不仅能掌握 Kuikly 跨平台开发的基础能力,还能巩固 Kotlin 语法、网络请求、状态管理等核心编程技能。后续可基于本项目,结合「正大杯」市场调研比赛的母婴方向,尝试开发轻量级母婴工具类跨平台应用,进一步深化对 Kuikly 框架的理解。

所有核心代码均已开源至 Kuikly-exchange-rate 项目,可直接克隆参考,结合本文步骤逐步调试,快速完成跨平台应用开发实战。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐