文章
问答
冒泡
腾讯开源Kuikly的初次尝试

前言

腾讯开源的 Kuikly 框架基于 Kotlin Multiplatform(KMP)技术,面向客户端开发的全新跨段解决方案,目前已经开源了 Android、IOS、鸿蒙平台能力,Web 和小程序计划 Q2 开源。

github 地址:

https://github.com/Tencent-TDS/KuiklyUI

环境配置

参考链接: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()
        }
    }
}

最终实现效果


遇到的问题

  1. 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
        }
    }
}
  1. 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)
                }
            }
        }
    }
}
Android
kmp
kuikly

关于作者

TimothyC
天不造人上之人,亦不造人下之人
获得点赞
文章被阅读