普通视图

发现新文章,点击刷新页面。
今天 — 2025年7月15日首页

Kuikly 开发笔记

作者 wyanassert
2025年7月15日 17:40

参考链接

Kuikly - 组件

Pager

Pager为Kuikly页面的入口类,类似iOS UI中的VC, Pager也有类似VC的生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 已创建
override fun created() {
super.created()

}

// 页面在屏幕上可见
override fun pageDidAppear() {
super.pageDidAppear()

}

// 页面在屏幕上消失
override fun pageDidDisappear() {
super.pageDidDisappear()

}

// 页面即将消失
override fun pageWillDestroy() {
super.pageWillDestroy()

}

一个常见的Page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 设置路由跳转名
@Page("my_ctstom_name")
// 定义一个自己的类, 继承于 BasePager
internal class MyCustomPage : BasePager() {
// 伴生对象, Kotlin 中,类没有静态成员的概念,但通过 companion object,你可以模拟静态成员的行为
companion object {
// 定义一个静态常量 TAG
private const val TAG = "MyCustomPageTag"
}

// 定义 currentName 是个可观察的属性, 设置给 UI 组件后, 值变化会直接更新 UI 组件的内容, 类似 OC 的 RAC
var currentName by observable("")

// 构造 self.view
override fun build(): ViewBuilder {
// 使用 this 会导致循环引用, 所以一般在这里定义一个 ctx
val ctx = this
return {
// 布局属性
attr {
backgroundColor(Color.WHITE)
}
// 子 View
View {

}
}
}
}

数据刷新

by observable

变量被设置为 by observable 后, UI 绑定这个变量会直接变化

1
2
3
4
5
6
7
var isOpen by observable(false)

Switch {
attr {
isOn(ctx.isOpen)
}
}

vbind

组件使用vbind包裹,当vbind内属性发生改变时,整个组件会重新绘制

1
2
3
4
5
6
7
8
9
10
11
vbind({ ctx.displaySomething }) {
if ( xx ) {
View {

}
} else if ( xx ) {
View {

}
}
}

vfor

对于数量可变的列表数据,使用vfor包裹来实现cell,列表变化时会重新绘制

1
2
3
4
5
6
7
vfor({ ctx.dataList }) { itemData ->
PersonInfoCustomizeCell {
attr {
item = itemData
}
}
}

vif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vif({ xxx }) {
View {

}
}
velseif({ }) {
View {

}
}
velse {
View {

}
}

ComposeView

1
2
3
4
5
class CustomView : ComposeView<>() {
}
internal fun ViewContainer<*, *>.Custom(init: CustomView.() -> Unit) {
addChild(CustomView(), init)
}

外部设置值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class CustomView : ComposeView<CustomViewAttr>() {

override fun body(): ViewBuilder {
val ctx = this
val bottomArrowX = ctx.attr.bottomArrowX
// ...
}

override fun createAttr(): CustomViewAttr {
return CustomViewAttr()
}
}

internal class CustomViewAttr : ComposeAttr() {
var bottomArrowX = 0f
// ...
}

// 其他类调用
class OtherClassView : ComposeView() {

override fun body(): ViewBuilder {
val ctx = this

View {
Custom {
attr {
bottomArrowX = showX
}
}
}
}
}

事件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class CustomView : ComposeView<CustomViewEvent>() {
override fun body(): ViewBuilder {
val ctx = this

View {
event {
click {
KLog.i(TAG, "onSendClick")
ctx.event.onSendClick?.invoke(ctx.attr.customViewParam)
}
}
}
}
}

class CustomViewEvent : ComposeEvent() {
var onSendClick: ((CustomViewParam) -> Unit)? = null
}
// 其他类调用
class OtherClassView : ComposeView() {

override fun body(): ViewBuilder {
val ctx = this

View {
Custom {
event {
onSendClick = { customViewParam ->
// 事件处理
}
}
}
}
}
}

布局

布局属性

Flex 布局

flexDirection 主轴

  • flexDirectionColumn(默认): 主轴方向为竖直方向,子孩子从上往下布局
  • flexDirectionRow: 主轴方向为水平方向,子孩子从左往右布局
  • flexDirectionColumnReverse: 主轴方向为竖直方向,子孩子从下往上布局
  • flexDirectionRowReverse: 主轴方向为水平方向,子孩子从右往左布局

justifyContent 主轴分布模式

  • justifyContentFlexStart(默认): 主轴开始的位置进行对齐
  • justifyContentFlexEnd: 主轴结束的位置进行对齐
  • justifyContentCenter: 主轴的中间位置进行对齐
  • justifyContentSpaceBetween: 主轴两端对齐,子孩子之间的间隔都相等
  • justifyContentSpaceAround: 每个项目两侧的间隔相等。所以,子孩子之间的间隔比项目与边框的间隔大一倍
  • justifyContentSpaceEvenly: Flex Item之间的间距相等,包括与边缘位置的距离

alignItems 交叉轴

  1. alignItemsFlexStart: 交叉轴的起点对齐
  2. alignItemsFlexEnd: 交叉轴的终点位置对齐
  3. alignItemsCenter: 交叉轴的中点位置对齐
  4. alignItemsStretch(默认): 如果 Flex 容器的孩子没有指定大小(高度或者宽度,取决于交叉轴是水平还是竖直)的话,将占满 Flex 容器
  • 当 flexDirection 为 flexDirectionRow 时,交叉轴的方向为竖直方向,此时 alignItems 各个属性的效果为
    企业微信截图_6d150c72-644a-42cd-9a28-8b92a9a082df
  • 当 flexDirection 为 flexDirectionColumn 时,交叉轴的方向为水平方向,此时 alignItems 的各个属性效果为:
    企业微信截图_ce12e3a8-ebf2-4c89-a397-638292dbc2c5

Flex Item 布局属性

上面讲述的 Flex Container 属性,是针对 Flex Container 下的所有孩子生效。Flex Item 也可自己设置布局属性,覆盖 Flex Container 的属性。

alignSelf

alignSelf属性是控制Flex Item自身在 Flex 容器的交叉轴上的对齐方式,会覆盖 Flex 容器指定的 alignItems 属性,可选值为:

  1. alignSelfFlexStart: Flex Item 自身在 Flex 容器的交叉轴的起点对齐
  2. alignSelfCenter: Flex Item 自身在 Flex 容器的交叉轴中点对齐
  3. alignSelfFlexEnd: Flex Item 自身在 Flex 容器的交叉轴终点对齐
  4. alignItemsStretch: Flex Item 自身在交叉轴方向上铺满Flex 容器
  • alignSelf 与 alignItems 差不多,具体的对齐方向与 flexDirection 的值有关
  • 当 flexDirection 的值为 flexDirectionColumn 时,Flex Item 设置 alignSelf 的效果如下:

flex

Flex Item 在主轴上,占据 Flex 容器的剩余可用空间的比例。 可用空间是指 Flex 容器除去已经被占用的空间,剩下的空间大小, 比如: 顶部是一个 titlebar,剩下的空间分配给列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
View {
attr {
size(screenWidth, screenHeight)
flexDirectionColumn() // 主轴方向为竖直方向,孩子从上往下排列
// alignItems默认值为 alignItemsStretch, 因此在交叉轴,即水平方向上
// 其孩子的宽度与父亲一样大
}

View { // title bar
attr {
height(56f)
// 宽度为父亲的宽度, 因为父亲的alignItems 默认为 alignItemsStretch,
// 在交叉轴上的大小会占满父亲
}
}

List {
attr {
flex(1f) // 表示占满父容器主轴上的可用空间,即占满父亲可用的高度(screenHeight - 56f)
}
}
}

绝对布局

与 superView 一样大

1
2
3
4
5
View {
attr {
absolutePositionAllZero()
}
}

保持间距

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
View {
attr {
size(screenWidth, screenHeight)
}

Image {
attr {
positionAbsolute()
// 表示宽高与父容器的宽高一样大
top(0f)
bottom(0f)
right(0f)
left(0f)
}
}
}

常用UI组件

设置背景色/圆角

1
2
3
4
attr {
backgroundColor(ctx.buildThemedColor("skin_floor_color"))
borderRadius(10f)
}

文本

1
2
3
4
5
6
7
8
9
10
Text {
attr {
fontSize(36f.pxw)
fontWeight500()
marginTop(40f.pxw)
marginBottom(10f.pxw)
text(itemData.title)
color(ctx.buildThemedColor("skin_text_main_color"))
}
}

加载图片

1
2
3
4
5
6
7
Image {
attr {
src("https://musicx.y.qq.com/kuikly/assets/ic_svip_quality_guide_check.png")
size(DESIGN_DESC_ICON_SIZE.pxh(), DESIGN_DESC_ICON_SIZE.pxh())
marginRight(DESIGN_DESC_ICON_MARGIN_RIGHT.pxh())
}
}

列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
List {
attr {
positionAbsolute()
}
vfor({ ctx.dataList }) { itemData ->
View {
attr {
Size(pagerWidth, 40F)
flexDirectionRow()
}

Image {
attr {
size(30F, 30F)
src(itemData.avatarUrl)
}
}

Text {
attr {
fontSize(36f.pxw)
fontWeight500()
marginTop(40f.pxw)
marginBottom(10f.pxw)
text(itemData.title)
color(ctx.buildThemedColor("skin_text_main_color"))
}
}
}
}
}

列表 cell 高度设置(在 cell 设置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
List {
ref {
// it 是一个隐式名称(implicit name)的参数,用于表示单个参数的 lambda 表达式中的参数。当你在 lambda 表达式中只有一个参数时,可以使用 it 来代替显式声明参数名称
ctx.listViewRef = it
}
attr {
absolutePositionAllZero()
}
vfor({ ctx.dataList }) { itemData ->
PersonInfoCustomizeCell {
attr {
item = itemData
}
}
}
}

// cell 单独设置
override fun body(): ViewBuilder {
val ctx = this
return {
attr {
height(120F)
}
View {
attr {
backgroundColor(Color.WHITE)
borderRadius(10F)
positionAbsolute()
top(0F)
left(20F)
right(20F)
bottom(10F)
flexDirectionRow()
}
}
}
}

列表调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import com.tencent.kuikly.core.views.ListView

internal class xxxPager : BasePager() {
var listViewRef : ViewRef<ListView<*, *>>? = null


override fun build(): ViewBuilder {
val ctx = this
return {
List {
ref {
ctx.listViewRef = it
}
attr {
absolutePositionAllZero()
}
}
}
}
override fun viewDidLayout() {
super.viewDidLayout()

this.listViewRef?.view?.setContentInset(top = 500F)
this.listViewRef?.view?.setContentOffset(0F, -500F) // 设置 inset 后 自动产生了一个 offsetY 偏移
}
}

subView 回调

1
2
3
4
5
6
7
8
// subView 先获取到 pager
var pager = getPager()
// 判定 pager 类型,
if (pager is SomePager)
{
// 调用被标记为 observable 的对象, 达到调用的目的
pager.isShowAutoTranslateWarningDialog = true
}

弹窗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 是否需要展示自动翻译弹窗
var isShowAutoTranslateWarningDialog by observable(false)

return {
View {
// 自动翻译弹窗
ctx.buildAutoTanslateWarningDialog().invoke(this);
}
}

private fun buildAutoTanslateWarningDialog(): ViewBuilder {
val ctx = this
return {
SendMsgWarningDialog {
attr {
showDialog = ctx.isShowAutoTranslateWarningDialog
title = StringConst.AUTOTRANSLATE_DIALOG_TITLE
content = StringConst.AUTOTRANSLATE_DIALOG_CONTENT
}
event {
onButtonSureClick = {
ctx.isShowAutoTranslateWarningDialog = false
KLog.i(TAG, "[clickedTranslateBtnForAlert] click sure ")
}
onButtonCancelClick = {
// 取消
ctx.isShowAutoTranslateWarningDialog = false
KLog.i(TAG, "[clickedTranslateBtnForAlert] click cancel ")
}
}
}
}
}


// 最后 在需要的时候
this.isShowAutoTranslateWarningDialog = true

逻辑

日志

1
2
3
4
KLog.i(
TAG,
"[onMessageBodyLongPress] msgFrame=$msgFrame, msgBodyFrame=$msgBodyFrame, clickX=$clickX, clickY=$clickY, msgBodyLeft=$msgBodyLeft, messageModel=$messageModel"
)

缓存

1
bubblePlugin?.read(key = KEY_HAVE_DISPLAYED_GUIDE_VIEW + userId + myOwnEncryptUin)

判空(null 执行 toInt() 会崩溃)

1
!clickCountBefore.isNullOrEmpty()

类型转换

1
2
// toLong() 在遇到 "abc" 这样的字符串 会崩溃, 要用 toLongOrNull, toInt() 也有类似问题
var clickTs = clickTsBefore?.toLongOrNull() ?: 0L

延时处理

1
2
3
setTimeout(500) {
// xxx
}

Kuikly 开发遇到的坑

作者 wyanassert
2025年7月14日 14:25

Kuikly是基于Kotlin MultiPlatform(KMP)构建的跨端开发框架, 支持Android、iOS、HarmonyOS和H5/小程序等多个平台, 据 PCG 开发团队的报告, Kuikly 可以提升团队 85%的开发效率.
这里记录一些我自己使用 Kuikly 遇到的一些坑, 希望后面能少点踩坑吧.

  1. 热重载显示成功, 但是实际并没有成功
    • iOS 使用热重载开发, 会需要手机挂 http 代理到开发的电脑上, 如果忘记这一步(或者因为别的什么事情把代理关掉了), 手机上仍然会提示热重载成功, 但实际只是从内置 bundle 加载成功, 并不是从开发电脑加载成功, 遇到这种疑问记得确认下这个代理是不是挂上了
  2. iOS 源码引入 Kuikly, 使用 XCode 调试 Kuikly 代码 (除了热重载的另一种开发模式)但是报错 B(l) ARM64 branch out of range (134218124 max is +/-128MB): from
    • Kuikly 工程太大, 导致程序的代码段过大, 导致的链接错误, 可能需要修改 Kuikly 代码
  3. 某些 case, 图片 ImageView 匹配上了错误的 url, 导致图片显示出错
    • Kuikly 为了性能 对图片做了复用, 我们又接管了 Kuikly 图片的下载, 所以在下载完成后需要校验url是不是还能匹配上这个 KRImageView:
    • NSString *curUrl = SAFE_CAST([imageView valueForKey:@"css_src"], NSString) ; 然后再跟当前的url比较下
  4. iOS 产物打包失败, 但是安卓成功, 需要修改 gradle 的编译内存大小设置, 可能是内存不够
  5. 热重载模式下, 单例不能用私有变量(属性)记录值, 比如记录生命周期点击次数,
    • 因为单例会在热重载模式下重新生成一个实例,
    • 考虑到线上热修复实际也是使用的热重载模式, 建议还是不要使用单例的私有变量记录
  6. 本地缓存双端实现不一致, iOS 缓存默认做了一个与用户 id 绑定的逻辑, 安卓没做
  7. 控制台重复输出, 点击一下, Mac 上的”控制台”app 输出了 5 条一样的日志, 误以为是代码执行了 5 次, 但是 Xcode 显示日志又只有一次, 重启”控制台” app 恢复正常
  8. iOS 热重载偶现报错,
    • ./kuiklyw run 执行后, 还要在按一下i, 切换到 iOS 的热重载, 默认是安卓
  9. toInt() / toLong() 崩溃
    • kotlin 中 toInt() / toLong(), kotlin 非数字字符串 不能转成数字会抛异常. 应该使用 toLongOrNull, 转不了会返回 null
  10. Switch 组件(系统默认的开关), 连续设置值后, 在 iOS 表现正常, 但是在安卓丢失了背景色导致整个组件都不可见
    • 最后一次修改 改为延迟修改
❌
❌