本文原作者:Wind
原文发布于公众号“Hellotalk技术”
自开源以来,Kuikly跨平台框架迅速获得了开发者社区的青睐,众多团队已成功落地项目并积累了宝贵经验。本期分享的HelloTalk实践文章,从技术选型、基建落地、原生组件扩展,到多语言、模块化架构乃至AI提效,系统性地呈现了Kuikly在复杂业务中的全链路实践。无论你的团队正在评估还是已深度使用Kuikly,这篇文章都将提供有益参考。
为什么选择Kuikly
年初的时候,我们一直在思考怎么提升开发效率。相同的页面要开发两次,相同的逻辑要实现两遍,这不仅消耗人力,还容易导致两端表现不一致。作为iOS和Android双端并重的产品,我们希望通过跨平台方案来减少重复工作,统一页面和业务逻辑。
在调研了几个方案后,我们选择了Kuikly,主要看重这几点:
技术架构的契合度
Kuikly基于Kotlin MultiPlatform(KMP)构建,这和我们的技术栈比较契合。团队对Kotlin已经有一定的了解,上手不会太难。而且KMP的逻辑跨平台能力可以让我们在保持原生性能的同时实现代码复用。
原生UI渲染策略
Kuikly不像Flutter或React Native那样自己绘制UI,而是复用平台的原生UI组件。这样带来的好处是:
1. 页面的表现和交互完全遵循平台特性
2. 原生组件的性能优势能够保留
3. 可以无缝集成现有的原生组件和第三方库
4. 包体积增量较小,对App体积影响可控
Kuikly带来的实际收益
经过一段时间的实践,我们确实体会到了Kuikly带来的好处:
开发效率的提升
一份代码,多端运行是最直观的收益。对于HelloTalk这样iOS和Android双端并重的产品来说:
- 消除重复工作:相同的页面和业务逻辑只需要用Kotlin写一次
- 保证双端一致性:统一的代码天然保证了两端表现一致,避免了需求理解偏差
- 降低沟通成本:不再需要频繁地跨端联调和对齐
- 加快迭代速度:需求变更时只需改一处代码
轻量且高性能
Kuikly基于KMP和原生组件,不需要内置庞大的渲染引擎:
- 轻量:包体积增量可控
- 高性能:直接使用系统原生UI组件渲染,性能表现和纯原生基本一致
- 可动态化:支持动态下发页面配置,具备热更新能力
完整的跨端能力
Kuikly不仅仅是UI跨平台,还提供了完善的基建能力,真正实现了页面和业务逻辑的统一:
- UI跨端:页面布局和交互逻辑统一编写
- 逻辑跨端:业务逻辑可以在CommonMain中编写,彻底消除重复实现
高度的可扩展性
这是我们感受最深的一点。Kuikly不是一个封闭的黑盒,而是一个开放的框架:
- 组件可扩展:可以轻松扩展自定义的跨端组件
- 原生能力可桥接:通过DeclarativeBaseView等机制,可以方便地将原生能力封装为Kuikly组件
- 灵活的模块化:支持多模块架构,便于团队协作
接入过程的挑战
虽然Kuikly带来了很多好处,但实际接入时也遇到了一些挑战,这里客观地说明一下。
前期基础建设的投入
在使用Kuikly开发需求时,我们发现现有的Kuikly工程缺少一些必要的基础组件,同时HelloTalk项目本身也有一些复杂的业务规则。这导致前期需要花较长时间做基础建设:
基础组件的补充和开发
Kuikly虽然提供了一套基础组件,但针对HelloTalk的具体场景,我们需要:
1. 补充一些常用但还没有实现的组件
2. 将Kuikly的组件体系和HelloTalk现有的设计规范对齐
3. 针对特殊场景开发定制化的组件(如多语言显示、特殊交互等)
原生能力的封装和桥接
HelloTalk作为一个成熟的产品,积累了大量的原生能力和工具类。要在Kuikly中使用这些能力,需要:
1. 将现有的原生UI组件封装为Kuikly可用的跨端组件
2. 将原生的工具类、网络库等基础设施桥接到Kuikly环境
3. 统一处理iOS和Android两端的实现差异
复杂业务规则的适配
HelloTalk经过多年迭代,积累了很多复杂的业务规则,迁移到Kuikly时需要特别处理:
1. 多语言和国际化:包括RTL布局、特殊语言的显示优化等
2. 性能优化规则:长列表、大图加载等场景的优化策略
3. 兼容性处理:确保Kuikly页面与原生页面的交互和跳转无缝衔接
必要的投入
虽然前期投入了较多时间在基础建设上,但这些工作是必要的:
1. 为后续开发铺平道路:完善的基础组件库和工具链,能让后续的需求开发更高效
2. 建立最佳实践:在摸索过程中积累的经验,形成了适合团队的开发规范
3. 提升复用价值:封装好的组件和工具可以在多个页面中复用,投入产出比会越来越高
4. 真正发挥Kuikly价值:只有基础设施完善了,才能真正发挥Kuikly跨端开发的效率优势
开发页面展示
以下是我们使用Kuikly开发并已上线的部分页面截图:
页面截图
开发实践
Pager的学习和使用
Pager是接入Kuikly后第一个需要学习的概念。作为直接展示给用户的页面容器,理解Pager的使用是开发Kuikly页面的基础。
Pager是什么
在Kuikly框架中,Pager是页面的入口类,类似于Android的Activity和iOS的ViewController。它承载整个页面的UI,负责管理页面的生命周期、状态和渲染。
根据官方文档,当原生容器跳转到Kuikly页面时,KuiklyCore会根据传递的pageName找到对应的Pager创建闭包,创建Pager实例。
Pager的基本使用
一个基础的Pager需要做三件事:
1. 使用@Page注解注册
每个Pager类都需要用@Page注解标记,并指定一个唯一的路由名称:
Kotlin @Page("HelloWorld") internal class HelloWorldPage : Pager() { // ... } |
这个路由名称是原生代码打开该页面时使用的标识。
2. 实现body方法
body方法是必须实现的,它返回页面的UI结构描述:
Kotlin override fun body(): ViewBuilder { return { attr { allCenter() } Text { attr { text("Hello Kuikly") fontSize(14f) } } } } |
3. 获取页面参数PagerData
原生端打开Kuikly页面时可以传递参数,这些参数存储在PagerData中。根据官方文档,我们可以在生命周期方法中通过pagerData获取:
Kotlin override fun created() { super.created() // 获取原生传递的参数 val userId = pagerData.params.optString("userId") } |
Pager的生命周期
理解Pager的生命周期对于正确处理页面逻辑很重要。根据官方文档,Pager有以下生命周期方法:
Kotlin @Page("ExamplePage") class ExamplePager : BasePager() { // 1. 页面创建 - 在body()之前调用 override fun created() { super.created() // 必须先调用父类方法 // 适合做初始化和数据拉取 fetchData() } // 2. 页面可见 override fun pageDidAppear() { super.pageDidAppear() // 页面在屏幕上完全显示时调用 // 适合做页面埋点、开始动画等 } // 3. 页面不可见 override fun pageDidDisappear() { super.pageDidDisappear() // 页面从屏幕上消失时调用 // 适合暂停动画、停止播放等 } // 4. 页面即将销毁 override fun pageWillDestroy() { super.pageWillDestroy() // 适合做资源释放、清理工作 // 注意:此时无法调用Module的Native方法 } } |
几个注意事项:
1. 重写生命周期方法时,必须先调用super父类方法
2. created()在body()之前执行,适合做数据准备
3. pageWillDestroy()中无法通过Module调用Native方法
4. PagerData需要在created()或之后访问,在全局变量初始化时访问会报错
响应式状态管理
Kuikly的一大特点是响应式UI更新。当状态变量改变时,UI会自动重新渲染,用observable就可以实现:
Kotlin // 定义响应式状态 private var count by observable(0)
// UI中使用状态 override fun body(): ViewBuilder { val ctx = this return { Text { attr { text("点击次数: ${ctx.count}") } } Button { attr { text("点击我") } event { onClick { ctx.count++ // 修改状态会自动触发UI更新 } } } } } |
一些体会
在学习和使用Pager的过程中,我们有这些体会:
1. 从简单的静态页面入手,逐步添加交互和数据
2. 正确使用生命周期方法能避免很多问题
3. 充分利用BasePager提供的便捷方法,避免重复造轮子
4. 保持代码结构清晰,让代码易于维护
5. 掌握observable的使用是写好Kuikly页面的关键
扩展原生View
在掌握了Pager的基本使用后,我们很快遇到了一个实际问题:如何在Kuikly中使用原生的业务组件?这就需要用到DeclarativeBaseView,它是连接Kuikly和原生组件的桥梁。
为什么需要扩展原生View
在开发过程中,我们发现Kuikly提供的基础组件(如Image、Text等)虽然能满足一般需求,但无法直接适配我们App已有的业务规范。
以头像组件为例:HelloTalk对头像有一套完整的显示规范,包括特定的圆角样式、边框处理、不同尺寸的规格定义、默认占位图的统一管理、图片加载和缓存策略等。
如果直接使用Kuikly的Image组件,我们需要在每个地方重复配置这些规则,不仅繁琐,还容易出现不一致。所以我们需要将原生端已有的头像组件(HTAvatarView)封装成Kuikly可用的组件。
这就是DeclarativeBaseView的作用:将原生组件封装成Kuikly的声明式组件,让它能像内置组件一样在Kuikly DSL中使用。
DeclarativeBaseView的基本使用
封装一个原生组件需要在两端实现:
Kotlin端 - 定义声明式组件
在commonMain中定义继承自DeclarativeBaseView的类:
Kotlin // base/src/commonMain/kotlin/com/hellotalk/kuikly/base/widget/avatar/NativeAvatarView.kt
class NativeAvatarView : DeclarativeBaseView() { override fun createAttr(): NativeAvatarViewAttr = NativeAvatarViewAttr() override fun createEvent(): NativeAvatarViewEvent = NativeAvatarViewEvent() // 这个名字必须和原生端注册的名字一致 override fun viewName(): String = "NativeAvatarView" }
// 定义属性类 class NativeAvatarViewAttr : Attr() { // 设置头像大小 fun avatarSize(avatarSize: Int): NativeAvatarViewAttr { "avatarSize" with avatarSize return this } // 设置头像地址 fun avatarUrl(avatarUrl: String): NativeAvatarViewAttr { "avatarUrl" with avatarUrl return this } }
// 定义事件类(如果需要) class NativeAvatarViewEvent : Event()
// 提供DSL扩展函数 fun ViewContainer<*, *>.NativeAvatarView(init: NativeAvatarView.() -> Unit) { addChild(NativeAvatarView(), init) } |
几个关键点:
1. DeclarativeBaseView需要两个泛型参数:属性类和事件类
2. viewName()返回的字符串用于关联原生端的实现
3. 属性通过"key" with value的方式设置,会传递给原生端
4. DSL扩展函数让组件可以在body()中直接使用
原生端 - 实现和注册组件
Android端实现:
Kotlin // androidApp/src/main/java/com/hellotalk/kuikly/view/avatar/NativeAvatarView.kt
class NativeAvatarView(context: Context) : AppCompatImageView(context), IKuiklyRenderViewExport {// 实现这个接口 // 处理从Kuikly传递过来的属性 override fun setProp(propKey: String, propValue: Any): Boolean { when (propKey) { "avatarSize" -> { updateLayoutParams { width = propValue as? Int ?: 0 height = propValue as? Int ?: 0 } } "avatarUrl" -> { val avatarUrl = (propValue as? String).orEmpty() // 使用App的图片加载逻辑 setImageResource(R.drawable.ic_avatar_default) } else -> return super.setProp(propKey, propValue) } return true } } |
注册到Kuikly:
Kotlin // androidApp/src/main/java/com/hellotalk/kuikly/HTKuiklyRenderViewBaseDelegator.kt
override fun registerExternalRenderView(kuiklyRenderExport: IKuiklyRenderExport) { super.registerExternalRenderView(kuiklyRenderExport) with(kuiklyRenderExport) { renderViewExport( "NativeAvatarView", // 必须和viewName()返回值一致 renderViewExportCreator = { context -> NativeAvatarView(context) } ) } } |
iOS端实现:
Objective-C // iosApp/iosApp/KuiklyExpand/view/NativeAvatarView.h
#import #import "KuiklyRenderViewExportProtocol.h"
@interface NativeAvatarView : UIImageView @end |
Objective-C // iosApp/iosApp/KuiklyExpand/view/NativeAvatarView.m
@implementation NativeAvatarView
@synthesize hr_rootView;
#pragma mark - Initialization
- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.contentMode = UIViewContentModeScaleAspectFill; self.clipsToBounds = YES; [self setPlaceholderImage]; } return self; }
#pragma mark - KuiklyRenderViewExportProtocol
- (void)hrv_setPropWithKey:(NSString *)propKey propValue:(id)propValue { if ([propKey isEqualToString:@"avatarSize"]) { CGFloat size = [propValue floatValue]; [self setAvatarSize:size]; } else if ([propKey isEqualToString:@"avatarUrl"]) { NSString *url = [propValue description]; [self setAvatarUrl:url]; } // 处理基础CSS属性(必须) KUIKLY_SET_CSS_COMMON_PROP; }
- (void)hrv_prepareForReuse { // 组件复用时的清理工作 [self sd_cancelCurrentImageLoad]; [self setPlaceholderImage]; KUIKLY_RESET_CSS_COMMON_PROP; }
#pragma mark - Property Setters
- (void)setAvatarSize:(CGFloat)avatarSize { // 更新尺寸 [self updateSizeConstraints:avatarSize]; // 更新圆角 self.layer.cornerRadius = avatarSize / 2.0; self.layer.masksToBounds = YES; }
- (void)setAvatarUrl:(NSString *)avatarUrl { if (!avatarUrl || avatarUrl.length == 0) { [self setPlaceholderImage]; return; } // 使用SDWebImage加载图片 [self sd_setImageWithURL:[NSURL URLWithString:avatarUrl] placeholderImage:[self placeholderImage] options:SDWebImageRetryFailed completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { if (error) { [self setPlaceholderImage]; } }]; }
@end |
iOS端组件注册:
iOS端的组件注册比较简单,Kuikly会通过类名自动发现和注册实现了KuiklyRenderViewExportProtocol协议的组件,无需手动注册。只要确保:
1. 类实现了KuiklyRenderViewExportProtocol协议
2. 类名与Kotlin端viewName()返回值一致
3. 文件被正确编译到项目中
Android与iOS端实现对比:
对比项 | Android端 | iOS端 |
实现接口/协议 | IKuiklyRenderViewExport | KuiklyRenderViewExportProtocol |
属性设置方法 | setProp(propKey, propValue) | hrv_setPropWithKey:propValue: |
组件复用清理 | 通常在onDetachedFromWindow() | hrv_prepareForReuse |
注册方式 | 需要手动注册到registerExternalRenderView | 自动发现,无需手动注册 |
必须调用的宏 | 无 | KUIKLY_SET_CSS_COMMON_PROP |
语言 | Kotlin | Objective-C |
在Pager中使用
封装完成后,就可以像使用内置组件一样使用了:
Kotlin @Page("user_profile") class UserProfilePager : BasePager() { override fun body(): ViewBuilder { return { View { attr { flex(1f) } // 使用封装的头像组件 NativeAvatarView { attr { width(100f) height(100f) avatarSize(100) avatarUrl(" https://example.com/avatar.jpg") } } } } } } |
Shadow的概念和使用
在封装某些组件时,我们还会遇到布局测量的问题,这时就需要用到`Shadow`。
Shadow是什么
Shadow是Kuikly中用于布局计算的代理类,它的作用是:
1. 在布局计算阶段,将测量请求转发给原生端
2. 原生端根据实际内容计算出准确的尺寸
3. 将计算结果返回给Kuikly的布局引擎
为什么需要Shadow
以文本组件为例,文本的实际高度取决于文本内容的长度、字体大小和行高、容器的宽度约束、文本的换行规则等。这些复杂的计算在原生端已经有成熟的实现,通过Shadow机制,我们可以直接利用原生的测量能力,而不需要在Kuikly端重新实现。
Shadow的基本使用
以HTTextView为例,它使用Shadow来实现准确的文本测量:
Kotlin open class HTTextView : DeclarativeBaseView(), MeasureFunction { var shadow: HTTextShadow? = null // 初始化时创建Shadow override fun willInit() { super.willInit() shadow = HTTextShadow(pagerId, nativeRef, viewName()) } // 设置属性时同步到Shadow override fun didSetProp(propKey: String, propValue: Any) { if (isShadowProp(propKey)) { shadow?.setProp(propKey, propValue) flexNode.markDirty() // 标记需要重新布局 } } // 实现测量函数 override fun measure( node: FlexNode, width: Float, height: Float, measureOutput: MeasureOutput ) { // 通过Shadow计算尺寸 val size = shadow?.calculateRenderViewSize(width, height) measureOutput.width = size?.width ?: 0f measureOutput.height = size?.height ?: 0f } } |
Shadow的核心方法:
1. calculateRenderViewSize(width, height) - 请求原生端计算尺寸
2. setProp(key, value) - 同步属性到Shadow,用于测量计算
3. markDirty() - 标记需要重新计算
原生端的配合
在注册组件时,可以提供对应的Shadow创建器:
Kotlin renderViewExport( "KRHTTextView", renderViewExportCreator = { context -> KRHTTextView(context) }, shadowExportCreator = { // 提供Shadow创建器 KRHTTextShadow() } ) |
原生端实现calculateRenderViewSize方法来响应测量请求:
Kotlin override fun calculateRenderViewSize(width: Float, height: Float): String { // 根据当前属性和约束计算实际尺寸 val measuredWidth = // ... 测量逻辑 val measuredHeight = // ... 测量逻辑 return "$measuredWidth|$measuredHeight" } |
多语言的适配方式
多语言适配实现
我们的App已经在原生端建立了完善的多语言管理体系。在接入Kuikly后,为了兼容宿主App的多语言系统,避免在Kuikly端重复定义和维护大量的多语言文案,我们采用了Module桥接的方式:Kuikly端只维护一个多语言Key的列表配置(`KRStrings`),实际的多语言文案获取则通过`LocalizedStringModule`桥接到原生端,直接复用原生的多语言资源。
多语言系统架构
整体数据流如下:
Plain Text Kuikly层 → LocalizedStringModule(桥接) → 原生端KRLocalizedStringModule → 宿主App多语言资源 |
核心组件包括:
1. KRStrings - Kuikly端维护的多语言Key列表配置
2. LocalizedStringModule - Kuikly侧的桥接Module
3. KRLocalizedStringModule - 原生侧的Module实现,负责从宿主App多语言资源中获取文案
4. localizable()扩展函数 - 便捷的调用方式
Kuikly端的Key列表配置
在Kuikly端,我们只需要维护一个多语言Key的列表配置:
Kotlin // base/src/commonMain/kotlin/com/hellotalk/kuikly/base/res/strings/KRStrings.kt
object KRStrings { const val REPORT_BUG = "Report Bug" const val PLEASE_ENABLE_PHOTO_ALBUM_ACCESS = "Please enable photo album access" const val LINE_BREAK = "Line Break" const val RESTORE_PURCHASES = "Restore Purchases" // ... 更多多语言Key } |
这些Key对应着原生端多语言资源中的Key,值通常设置为英文,作为找不到对应翻译时的兜底显示。我们不需要在Kuikly端维护各语言的翻译文案,所有翻译都复用原生端已有的资源。
LocalizedStringModule - Kuikly侧的实现
LocalizedStringModule是Kuikly侧的Module,负责将多语言请求桥接到原生端:
Kotlin // base/src/commonMain/kotlin/com/hellotalk/kuikly/base/res/strings/LocalizedStringModule.kt
class LocalizedStringModule : Module() { companion object { const val MODULE_NAME = "KRLocalizedStringModule" private const val METHOD_GET_STRING = "getString" }
override fun moduleName(): String = MODULE_NAME
fun getString(name: String, vararg args: Any?): String { val data = JSONObject().apply { put("key", name) if (args.isNotEmpty()) { put("params", JSONArray().apply { for (arg in args) { put(arg) } }) } } val result = syncToNativeMethod(METHOD_GET_STRING, data, null) return result } } |
这个Module通过syncToNativeMethod同步调用原生方法,支持在文案中使用占位符进行参数替换(如:Hello %s)。
便捷的使用方式
为了简化调用,我们提供了String.localizable()扩展函数:
Kotlin // base/src/commonMain/kotlin/com/hellotalk/kuikly/base/pager/api/BasePagerExt.kt
fun String.localizable(): String { return HTPagerManager.instance.acquireModule(LocalizedStringModule.MODULE_NAME) ?.getString(this) ?: this } |
这样在Pager中就可以很方便地使用多语言:
Kotlin @Page("example_page") class ExamplePager : BasePager() { override fun body(): ViewBuilder { return { View { attr { flex(1f) } HTText { attr { text(KRStrings.REPORT_BUG.localizable()) fontSize(16f) } } Button { attr { text(KRStrings.RESTORE_PURCHASES.localizable()) } event { onClick { // 处理点击 } } } } } } } |
原生端实现
Android端:
Kotlin // androidApp/src/main/java/com/hellotalk/kuikly/module/KRLocalizedStringModule.kt
class KRLocalizedStringModule : KuiklyRenderBaseModule() { companion object { const val MODULE_NAME = "KRLocalizedStringModule" }
override fun call(method: String, params: Any?, callback: KuiklyRenderCallback?): Any? { return when (method) { "getString" -> getString(params) else -> super.call(method, params, callback) } }
private fun getString(params: Any?): String? { val json = params?.toString()?.let { JSONObject(it) } ?: return null val name = json.optString("key") // 通过StringResMap将key映射到R.string资源ID val resId = StringResMap.getStringResId(name) ?: return name // 支持参数化文案 val formatArgsJson = json.optJSONArray("params") return if (formatArgsJson != null && formatArgsJson.length() > 0) { val args = Array(formatArgsJson.length()) { i -> formatArgsJson.get(i).toString() } KRApplication.application.getString(resId, *args) } else { KRApplication.application.getString(resId) } } } |
iOS端:
Objective-C // iosApp/iosApp/KuiklyExpand/Modules/KRLocalizedStringModule.m
@implementation KRLocalizedStringModule
- (id)getString:(NSDictionary *)args { NSString *jsonStr = args[@"param"]; NSDictionary *param = [SwiftKuiklyTool toDictionary:jsonStr]; NSString *key = param[@"key"]; // 使用NSLocalizedString获取本地化字符串 NSString *format = NSLocalizedString(key, nil); if (![format isKindOfClass:[NSString class]]) return key ?: @""; // 处理参数替换 NSArray *params = param[@"params"]; return [SwiftKuiklyTool textString:format args:params]; }
@end |
Module的注册
在BasePager中,我们会自动注册LocalizedStringModule,这样所有继承自BasePager的页面都可以直接使用多语言功能:
Kotlin // base/src/commonMain/kotlin/com/hellotalk/kuikly/base/pager/api/BasePager.kt
override fun createExternalModules(): Map? { val externalModules = hashMapOf() // ... 其他模块 externalModules[LocalizedStringModule.MODULE_NAME] = LocalizedStringModule() // ... return externalModules } |
通过这套基于Module桥接的方案,我们成功地让Kuikly页面复用了宿主App的多语言资源。
阿拉伯语的适配实现
为什么需要特殊适配
HelloTalk作为一个面向全球用户的跨文化交流平台,支持包括阿拉伯语在内的多种语言是产品的基础能力。阿拉伯语是一种从右到左(RTL)书写的语言,这和我们常见的从左到右(LTR)的书写方向完全相反
在实际使用Kuikly官方提供的Text和RichText组件时,我们发现它们对阿拉伯语的显示存在一些问题:
1. 混合文本处理不当:当文本中同时包含阿拉伯语、数字和拉丁字母时,显示顺序和对齐方式不符合预期
2. 对齐方式不智能:纯阿拉伯语文本应该右对齐,但组件没有自动识别并调整
3. 双向文本算法:iOS和Android系统对双向文本(Bidi)的处理策略不同,需要统一处理逻辑
这些问题会严重影响阿拉伯语用户的使用体验,所以我们决定重写这两个组件。
重写HTTextView和HTRichTextView的核心差异
我们基于Kuikly的DeclarativeBaseView机制,重新实现了HTTextView和HTRichTextView,在保持与官方组件API兼容的前提下,针对阿拉伯语做了专门的优化。
主要实现差异包括:
1. 智能文本方向检测
我们实现了自动检测文本内容的能力,判断文本是否包含RTL字符:
Kotlin // Kuikly端添加RTL支持开关 const val RTL_SUPPORT = "rtlSupport"
// 使用方式 HTText { attr { text("مرحبا بك") // 阿拉伯语"欢迎" rtlSupport(true) // 启用RTL支持 } } |
在原生端,我们会检测文本内容是否包含RTL字符、是否包含数字、是否包含拉丁字母。
2. 混合文本的特殊处理
对于混合文本(同时包含RTL字符和LTR字符),我们采用了特殊的策略:
- 纯RTL文本:使用RTL书写方向,右对齐
- 混合文本:使用自然(Natural)书写方向,让系统的双向文本算法自动处理
- 纯LTR文本:使用LTR书写方向,左对齐
3. 智能对齐方式
我们实现了智能对齐逻辑:
Plain Text // iOS端示例逻辑 if (isPrimaryRTLText(text)) { alignment = NSTextAlignmentRight; // 纯RTL文本右对齐 } else if (containsRTLText(text) && containsLTRText(text)) { alignment = NSTextAlignmentNatural; // 混合文本使用自然对齐 } else { alignment = NSTextAlignmentLeft; // LTR文本左对齐 } |
这样可以根据文本内容自动选择合适的对齐方式,不需要业务代码手动判断。
4. 段落样式优化
针对RTL文本,我们优化了段落样式的设置:
- baseWritingDirection:根据文本内容智能设置段落的基础书写方向
- 首行缩进:RTL文本的首行缩进需要从右侧开始
- 行间距和字间距:保持与LTR文本一致的视觉效果
5. 跨平台一致性
由于iOS和Android对RTL文本的处理机制不同,我们在两端分别实现了相同的检测和处理逻辑,确保相同的阿拉伯语文本在两个平台显示一致。
RTL布局支持
除了文本组件,我们还扩展了通用的布局属性,提供了RTL自适应的布局能力:
Kotlin // 使用Start/End语义,自动适配RTL fun Attr.marginStart(start: Float) // LTR时=marginLeft, RTL时=marginRight fun Attr.marginEnd(end: Float) // LTR时=marginRight, RTL时=marginLeft fun Attr.paddingStart(start: Float) fun Attr.paddingEnd(end: Float)
// 使用示例 View { attr { marginStart(16f) // 在阿拉伯语环境下自动变为右边距 paddingStart(8f) } } |
这些扩展方法根据CommonResConfig.isRtl标志自动映射到正确的方向,业务代码无需手写RTL分支判断。
实现效果
通过重写这两个组件,我们实现了:
1. 自动识别:无需手动判断文本语言,组件自动识别并处理
2. 显示正确:阿拉伯语文本在各种场景下都能正确显示
3. 混合文本支持:包含多种语言的文本也能正确渲染
4. API兼容:与官方组件保持API兼容,业务代码只需替换组件名即可
这套方案不仅解决了阿拉伯语的显示问题,也为支持其他RTL语言(如希伯来语、波斯语等)打下了基础。
多模块的搭建
随着Kuikly开发的深入,我们开发的页面和组件越来越多,如果所有代码都放在一个模块中,会导致编译时间变长、代码管理混乱、团队协作困难。为了更好地组织代码和提高开发效率,我们采用了Kuikly的多模块架构。
为什么需要多模块
在使用多模块之前,我们面临这些问题:
1. 编译时间长:所有代码在一个模块中,每次修改都需要重新编译整个模块
2. 代码耦合:不同业务的代码混在一起,难以维护和复用
3. 团队协作:多人同时开发容易产生代码冲突
4. 职责不清:基础能力、通用组件、业务页面混杂在一起
Kuikly提供的多模块机制可以解决这些问题,它允许我们将代码按职责划分到不同的模块中,各模块独立编译,互不干扰。
项目的模块结构
我们的项目采用了以下模块结构:
Plain Text HTKuiklyProject/ ├── androidApp/ - Android宿主应用 ├── iosApp/ - iOS宿主应用 ├── base/ - 基础模块(基础组件和工具) └── shared/ - 业务模块(业务页面) |
其中base模块负责基础能力层,shared模块负责业务逻辑层,两个原生宿主负责框架初始化和原生能力注册。
多模块配置
要让Kuikly支持多模块,需要在每个模块的build.gradle.kts中进行配置。
base模块配置
Kotlin // base/build.gradle.kts
plugins { kotlin("multiplatform") kotlin("native.cocoapods") id("com.android.library") id("com.google.devtools.ksp")// KSP插件,用于代码生成 }
// KSP配置 ksp { arg("moduleId", "base") // 模块唯一ID arg("isMainModule", "false") // 是否为主模块 arg("enableMultiModule", "true")// 启用多模块支持 }
dependencies { // 添加KSP处理器依赖 compileOnly("com.tencent.kuikly-open:core-ksp:${Version.getKuiklyVersion()}") { add("kspAndroid", this) add("kspIosArm64", this) add("kspIosX64", this) add("kspIosSimulatorArm64", this) } } |
配置说明:
1. moduleId:模块的唯一标识符,用于区分不同的模块
2. isMainModule:标识是否为主模块。通常只有一个模块设置为true
3. enableMultiModule:启用多模块支持,让KSP能够正确处理跨模块的页面注册
shared模块配置
Kotlin // shared/build.gradle.kts
ksp { arg("moduleId", "shared") arg("isMainModule", "true") // shared作为主模块 arg("enableMultiModule", "true") }
dependencies { // 依赖base模块 implementation(project(":base")) // 添加KSP处理器 compileOnly("com.tencent.kuikly-open:core-ksp:${Version.getKuiklyVersion()}") { add("kspAndroid", this) add("kspIosArm64", this) add("kspIosX64", this) add("kspIosSimulatorArm64", this) } } |
多模块的工作原理
Kuikly的多模块机制基于KSP(Kotlin Symbol Processing)实现:
1. 编译时处理:KSP在编译时扫描各模块中的`@Page`注解
2. 代码生成:为每个模块生成页面注册代码
3. 模块聚合:主模块(shared)会聚合所有子模块(base)的页面注册信息
4. 统一路由:运行时,所有模块的页面都可以通过路由名称访问
这样的设计使得每个模块可以独立开发和编译,模块之间通过依赖关系组织,页面路由全局统一。
模块间的依赖关系
Plain Text androidApp ──┐ ├──> shared ──> base ──> kuikly-core iosApp ──────┘ |
依赖方向是单向的,base模块不依赖shared模块,保证了基础层的独立性和可复用性。
settings.gradle.kts配置
在项目根目录的settings.gradle.kts中,需要声明所有模块:
Kotlin rootProject.name = "HTKuiklyProject" include(":androidApp") include(":shared") include(":base") |
多模块带来的好处
通过多模块架构,我们获得了这些收益:
1. 编译速度提升:修改某个模块时,只需重新编译该模块,其他模块不受影响
2. 职责清晰:基础能力和业务逻辑分离,分工明确
3. 便于复用:base模块可以在多个项目中复用
4. 团队协作:不同团队可以负责不同模块,减少代码冲突
通过以上配置,我们成功地将Kuikly项目拆分为多个模块,每个模块独立编译和管理,大大提高了开发效率。
通过AI开发Kuikly
在使用Kuikly开发过程中,我们探索出了一套基于AI的开发协作模式。为了规范化和标准化开发流程,我们构建了一套完整的AI辅助开发系统,大大提升了开发效率和代码质量。
AI辅助开发系统概览
我们在项目的ai-prompts/目录下建立了一套完整的AI协作系统,包含:
Plain Text ai-prompts/ ├── 00-role-definition.md # AI角色定义 ├── 01-development-workflow.md # 开发工作流程 ├── layer-configs/ # 分层级开发配置 ├── knowledge-base/ # 知识库 └── templates/ # 代码模板 |
这套系统的核心思想是:让AI深度理解Kuikly的技术栈和项目规范,按照标准化的工作流程协助开发,避免重复造轮子,确保代码质量和一致性。
核心文件说明
角色定义(00-role-definition.md)
这个文件定义了AI作为Kuikly开发专家应该具备的能力和遵循的原则:
核心技术能力:
1. Kuikly框架:Pager系统、组件体系、模块系统、状态管理
2. KMP(Kotlin Multiplatform):跨平台开发、代码组织
3. Android开发:View系统、Compose、自定义View
4. iOS开发:UIKit、SwiftUI、自定义View
开发原则:
1. 组件优先原则:优先使用已有组件,避免重复造轮子
2. 渐进式开发原则:先分析方案,再选择组件,最后实现代码
3. 规范至上原则:严格遵循项目代码规范与最佳实践
4. 文档同步原则:代码与文档同步更新
5. 质量保证原则:代码质量优先于开发速度
工作流程(01-development-workflow.md)
定义了标准的六阶段开发流程:
Plain Text 阶段1: 理解需求 ↓ 阶段2: 分析方案 ↓ 阶段3: 查找组件 ⚠️ 必需步骤,不可跳过 ↓ 阶段4: 设计架构 ↓ 阶段5: 编写代码 ↓ 阶段6: 生成文档 |
这个流程的关键是阶段3(查找组件)是强制步骤,必须先查询知识库确认没有现成组件可用,才能开始编写新代码。这样可以最大程度地复用现有代码,避免重复开发。
分层级配置(layer-configs/)
根据代码的复杂度和抽象层级,我们定义了五个开发层级:
- L1 - 业务代码层:具体业务页面(Pager),直接使用组件实现业务需求
- L2 - 业务组件层:可复用的业务级组件,封装常见业务场景
- L3 - 基础组件层:通用基础组件,不涉及具体业务
- L4 - 跨平台组件层:KMP共享组件,实现跨平台复用
- L5 - Native View层:原生自定义视图,需要Android/iOS分别实现
每个层级都有对应的配置文件,明确了该层级的特点、开发规范、组件使用指南等。
知识库(knowledge-base/)
知识库是整个系统的核心,包含了Kuikly开发所需的所有技术文档:
目录结构:
Plain Text knowledge-base/ ├── quick-reference/ # 快速参考 │ ├── components-quick.md # 组件速查表 │ ├── modules-quick.md # 模块速查表 │ ├── patterns-quick.md # 常用模式 │ └── ... ├── components/ # 组件详细文档 │ ├── button.md │ ├── text.md │ ├── list.md │ ├── ht-components/ # 自定义组件 │ └── ... ├── modules/ # 模块详细文档 │ ├── router.md │ ├── network.md │ ├── ht-modules/ # 自定义模块 │ └── ... ├── architecture/ # 架构文档 │ └── ... └── guidelines/ # 开发指南 ├── pager-development-guide.md ├── component-usage-guidelines.md └── ... |
知识库的作用:
1. 快速参考(quick-reference/):供AI快速查询的精简版文档,包含组件速查表、模块速查表、常用模式等,让AI在1-2秒内就能找到需要的组件
2. 详细文档:完整的API文档、使用示例、注意事项,供深入了解组件细节时使用
3. 开发指南:Pager开发、组件使用、暗黑模式、RTL、多语言等主题的系统性指南
知识库的价值:
- 避免重复造轮子:AI可以快速查询已有组件,避免重复开发
- 确保代码一致性:所有开发都基于同一套文档和规范
- 提高开发效率:不需要每次都去翻阅源码或官方文档
- 降低学习成本:新接手的开发者可以快速上手
代码模板(templates/)
提供了标准的代码结构模板:
1. 业务页面模板
2. 业务组件模板
3. 基础组件模板
4. Native View模板
使用模板可以确保代码结构的一致性,减少低级错误。
实际使用方式
当我们需要开发一个新功能时,会给AI提供以下提示词:
Markdown # Kuikly 开发任务## 角色配置 请加载以下文件作为你的工作指南: - ai-prompts/00-role-definition.md - ai-prompts/01-development-workflow.md - ai-prompts/layer-configs/L1-business-logic.md
## 开发需求 [详细描述需求]
## 功能需求- 功能点1 - 功能点2
## UI要求 [UI描述或设计图]
## 技术要求 [特殊技术要求]
请按照标准工作流程完成开发。 |
AI会按照六阶段流程,先理解需求、分析方案、查找可用组件,然后再设计架构、编写代码、生成文档。整个过程规范、高效、可追溯。
推荐结语
Kuikly跨端框架正受到越来越多开发者的关注,并已在众多业务中成功落地。希望HelloTalk的实践经验能为各位开发者带来帮助和参考。同时,也欢迎更多开发者积极参与分享,共同推动技术社区的成长。
若你刚接触Kuikly,可通过以下途径快速了解与接入:
● 进入腾讯端服务联盟(tds.qq.com)主页访问Kuikly官网(framework.tds.qq.com)。
● 通过以下Kuikly文档和仓库,快速体验:
官方文档
Github仓库
● 关注和联系他们