前言
腾讯开源的 Kuikly 框架基于 Kotlin Multiplatform(KMP)技术,面向客户端开发的全新跨段解决方案,目前已经开源了 Android、IOS、鸿蒙平台能力,Web 和小程序计划 Q2 开源。
github 地址:
环境配置
参考链接:https://kuikly.tds.qq.com/%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B/env-setup.html
目前我手上只有一个 Windows 的笔记本,IOS 和鸿蒙都只支持 MAC,因此本文只展示在安卓端的使用
我的 Android Studio 版本是 2024.3.2,大于等于 2024.2.1 的版本要将 Gradle JDK 版本切换为 JDK17(默认是 JDK21)
然后安装下图的 Kotlin MultiPlatform 和 Kuikly Template 插件
项目创建
由于上面安装了 Kuikly Template 插件,重启 IDE 后,我们就能新建 Kuikly 项目
然后是设置 Gradle 的镜像源,具体可以参考我之前的文章
https://www.ithere.net/article/1855865994731257858
下载完依赖后直接启动 androidApp,会出现 Kuikly 页面路由
实现一个 HelloWorld
首先 Kuikly 是通过 Pager 作为页面的入口,类似 Android 的 Activity。通过前面安装的插件,我们可以直接在 share/src/commonMain/kotlin/xxx.xxx.xxx/ 目录下新建 Kuikly Pager 类
创建的类默认会继承 Pager 类并且会有@Page 注解,注解里面的值就是页面的路由,会默认重写 body 方法,在里面添加你想要的组件
@Page("HelloWorld")
internal class HelloWorld : BasePager() {
override fun body(): ViewBuilder {
return {
attr {
allCenter()
}
Text {
attr {
text("Hello Kuikly")
fontSize(30f)
}
}
}
}
}
运行后,在路由跳转处输入@Page 注解的值 HelloWorld,就能正常显示了
实现一个简单的 todo-list
internal class TodoItem(private val scope: PagerScope) : BaseObject() {
var id: Int by scope.observable(0)
var text: String by scope.observable("")
var isDone: Boolean by scope.observable(false)
}
@Page("todo")
internal class TodoAppPage : Pager() {
private lateinit var inputRef: ViewRef<InputView>
private var dataList: ObservableList<TodoItem> by observableList()
private var inputText :String by observable("")
override fun createEvent(): ComposeEvent {
return ComposeEvent()
}
override fun body(): ViewBuilder {
val ctx = this
return {
attr {
backgroundColor(Color.WHITE)
flexDirectionColumn()
autoDarkEnable(false)
}
View {
attr {
paddingTop(ctx.pagerData.statusBarHeight)
width(ctx.pagerData.pageViewWidth)
}
View {
attr {
// KMP primary light color
backgroundColor(Color(0xFF6200EE))
}
Text {
attr {
margin(13f)
text("Todo List")
fontSize(20f)
fontWeightSemiBold()
color(Color.WHITE)
}
}
}
}
List {
attr {
flex(1f)
marginLeft(10f)
marginTop(10f)
}
// event {
// scroll {
//
// }
// }
vfor({ ctx.dataList }) { item ->
View {
attr {
width(ctx.pagerData.pageViewWidth)
height(60f)
flexDirectionRow()
alignItemsCenter()
}
CheckBox {
attr {
size(20f, 20f)
checked(item.isDone)
defaultImageSrc("https://vfiles.gtimg.cn/wuji_dashboard/xy/componenthub/Efeg39sG.png")
checkedImageSrc("https://vfiles.gtimg.cn/wuji_dashboard/xy/componenthub/m5kRYKMt.png")
}
event {
checkedDidChanged {
item.isDone = it
}
}
}
Text {
attr {
marginLeft(10f)
size(300f, 15f)
color(Color.BLACK)
fontSize(15f)
text(item.text)
}
}
Button {
attr {
size(40f, 40f)
titleAttr {
text("X")
color(Color.RED)
fontSize(15f)
}
}
event {
click {
val itemToRemove = ctx.dataList.find { it.id == item.id }
itemToRemove?.let {
ctx.dataList.remove(it)
}
}
}
}
}
}
}
View {
attr {
flexDirectionRow()
}
Input {
ref {
ctx.inputRef = it
}
attr {
borderRadius(3f)
border(Border(1f, BorderStyle.SOLID, Color.GRAY))
margin(10f)
size(320f, 40f)
placeholder("Add a todo")
placeholderColor(Color.GRAY)
color(Color.BLACK)
fontSize(15f)
}
event {
textDidChange {
ctx.inputText = it.text
}
}
}
Button {
attr {
size(40f, 40f)
marginTop(10f)
titleAttr {
text("+")
fontSize(30f)
fontWeightNormal()
}
}
event {
click {
val item = TodoItem(this)
item.id = ctx.dataList.maxOfOrNull(TodoItem::id)?.plus(1) ?: 1
item.text = ctx.inputText
ctx.dataList.add(item)
}
}
}
}
}
}
override fun created() {
super.created()
for (index in 1..5) {
val item = TodoItem(this)
item.id = index
item.text = "Some text $index"
dataList.add(item)
}
}
override fun viewDidLoad() {
super.viewDidLoad()
setTimeout(pagerId, 5000) {
val inputView = inputRef.view!!
inputView.setText("")
inputView.blur()
}
}
}
最终实现效果
遇到的问题
Kuikly 中的 CheckBox 组件如果不添加defaultImageSrc 和checkedImageSrc 就无法渲染
复选框没有默认的图片要自己添加,我是参考了 github 的 demo 里面的图片,可以去看源码目前初始化就是给了空字符串
CheckBox {
attr {
size(30f, 30f)
checked(true)
defaultImageSrc("https://vfiles.gtimg.cn/wuji_dashboard/xy/componenthub/Efeg39sG.png")
checkedImageSrc("https://vfiles.gtimg.cn/wuji_dashboard/xy/componenthub/m5kRYKMt.png")
disableImageSrc("https://vfiles.gtimg.cn/wuji_material/web283034f3-ee18-407b-c117-cdaf23bd7a38.png")
disable(ctx.disable)
}
event {
checkedDidChanged {
KLog.i("2", "checkedDidChanged:" + it.toInt())
ctx.disable = true
}
}
}
Kuikly 中的 List 组件如果不添加 flex 这个属性也无法渲染
这个也很奇怪,官方文档里面并没有 flex(1f) 这个属性,不看例子根本找不到问题
List {
attr {
flex(1f)
}
event {
scroll {
}
}
Refresh {
ref {
ctx.refreshRef = it
}
attr {
height(50f)
allCenter()
}
Text {
attr {
color(Color.BLACK)
fontSize(20f)
text(ctx.refreshText)
transform(Skew(-10f, 0f))
}
}
event {
refreshStateDidChange {
when(it) {
RefreshViewState.REFRESHING -> {
ctx.refreshText = "正在刷新"
setTimeout(2000) {
ctx.dataList.clear()
for (index in 0..10) {
val item = ListItem(ctx)
item.title = "我是第${ctx.dataList.count()}个卡片"
ctx.dataList.add(item)
}
ctx.refreshRef.view?.endRefresh()
ctx.footerRefreshRef.view?.resetRefreshState() // 刷新成功后,需要重置尾部刷新状态
}
}
RefreshViewState.IDLE -> ctx.refreshText = "下拉刷新"
RefreshViewState.PULLING -> ctx.refreshText = "松手即可刷新"
}
}
}
}
vfor({ ctx.dataList }) { item ->
EasyCard {
attr {
//title = item.title
listItem = item
}
event {
titleDidClick {
KLog.i("ListViewDemoPage", "did fire titleDidClick")
}
}
}
}
Hover {
ref {
// ctx.redBlockRef = it
}
attr {
absolutePosition(top = ctx.redBlockStickTop, left =0f, right =0f)
height(50f)
backgroundColor(Color.RED)
}
}
Hover {
ref {
// ctx.redBlockRef = it
}
attr {
absolutePosition(top = 600f, left =0f, right =0f)
height(50f)
backgroundColor(Color.BLUE)
}
}
Hover {
ref {
// ctx.redBlockRef = it
}
attr {
absolutePosition(top = 900f, left =0f, right =0f)
height(50f)
backgroundColor(Color.YELLOW)
}
}
Hover {
ref {
// ctx.redBlockRef = it
}
attr {
absolutePosition(top = 1200f, left =0f, right =0f)
height(50f)
backgroundColor(Color.BLACK)
}
}
vif({ctx.dataList.isNotEmpty()}) {
FooterRefresh {
ref {
ctx.footerRefreshRef = it
}
attr {
preloadDistance(600f)
allCenter()
height(60f)
}
event {
refreshStateDidChange {
KLog.i("ListViewDemoPage", "refreshStateDidChange : $it")
when(it) {
FooterRefreshState.REFRESHING -> {
ctx.footerRefreshText = "加载更多中.."
setTimeout(1000) {
if (ctx.dataList.count() > 100) {
ctx.footerRefreshRef.view?.endRefresh(FooterRefreshEndState.NONE_MORE_DATA)
} else {
for (index in 0..10) {
val item = ListItem(ctx)
item.title = "我是第${ctx.dataList.count()}个卡片"
ctx.dataList.add(item)
}
ctx.footerRefreshRef.view?.endRefresh(FooterRefreshEndState.SUCCESS)
}
}
}
FooterRefreshState.IDLE -> ctx.footerRefreshText = "加载更多"
FooterRefreshState.NONE_MORE_DATA -> ctx.footerRefreshText = "无更多数据"
FooterRefreshState.FAILURE -> ctx.footerRefreshText = "点击重试加载更多"
else -> {}
}
}
click {
// 点击重试
ctx.footerRefreshRef.view?.beginRefresh()
}
}
Text {
attr {
color(Color.BLACK)
fontSize(20f)
text(ctx.footerRefreshText)
}
}
}
}
}