从0使用Kuikly框架写一个小红书Demo-Day4

消息页面与卡片详情页的组件化开发

在学习了ComposeView的概念和使用方法后,我们可以来实践一下。

在首页瀑布流的基础上,我们还需要开发消息页面和卡片详情页,当页面更多更复杂时,我们就需要对代码进行拆分、封装组件。随着业务逻辑与页面数量的增加,为保证代码的可维护性与可扩展性,我们将采用组件化方案进行开发。利用 Kuikly 框架的ComposeView,将各功能模块封装为独立、可复用的组件,以应对日益增长的项目复杂度。

4.1 Kuikly框架的特性

首先先来了解一下Kuikly框架的特性

声明式UI开发

Kuikly采用声明式UI开发模式,让开发者专注于描述UI的最终状态,而不是具体的实现过程:

kotlin 复制代码
override fun body(): ViewBuilder {
    val ctx = this
    return {
        attr {
            flex(1f)
            backgroundColor(Color.WHITE)
            flexDirectionColumn()
        }
        
        // 组件内容描述
        WaterfallList {
            // 瀑布流配置
        }
    }
}
 

响应式数据绑定

通过observable和observableList,实现数据与UI的自动同步,当数据发生变化时,UI会自动更新,无需手动操作DOM:

kotlin 复制代码
private var messageList: ObservableList<MessageItem> by observableList<MessageItem>()
private var selectedTabIndex: Int by observable(0)

在Kuikly中,你可以通过by observable将一个字段变成响应式字段,然后绑定到UI组件的属性,这样UI组件的属性就能自动监听数据变化而自动更新

组件化架构

每个组件都继承自ComposeView,具有独立的属性(Attr)和事件(Event)系统:

kotlin 复制代码
internal class MessageItemView(private val messageItem: MessageItem) : 
    ComposeView<MessageItemViewAttr, MessageItemViewEvent>()

4.2 组件开发实践

消息项组件(MessageItemView)

这是一个可复用的组件,用于展示单条消息,基于传入的MessageItem数据渲染

根据前文教程,我们首先要定义组件的属性和事件,新建两个类,一个继承ComposeAttr,一个继承ComposeEvent。

kotlin 复制代码
internal class MessageItemViewAttr : ComposeAttr()
internal class MessageItemViewEvent : ComposeEvent()
 
internal fun ViewContainer<*, *>.MessageItemView(messageItem: MessageItem, init: MessageItemView.() -> Unit) {
    addChild(MessageItemView(messageItem), init)
}

然后是组件内容,主要是实现body方法

kotlin 复制代码
/**
 * 消息项组件
 */
internal class MessageItemView(private val messageItem: MessageItem) : ComposeView<MessageItemViewAttr, MessageItemViewEvent>() {
    
    override fun createEvent(): MessageItemViewEvent {
        return MessageItemViewEvent()
    }
 
    override fun createAttr(): MessageItemViewAttr {
        return MessageItemViewAttr()
    }
 
    override fun body(): ViewBuilder {
        val ctx = this
        return {
            View {
                attr {
                    flexDirectionRow()
                    padding(12f, 16f, 12f, 24f)
                    alignItemsCenter()
                    backgroundColor(Color.WHITE)
                }
 
                // 头像容器
                View {
                    attr {
                        width(50f)
                        height(50f)
                        marginRight(12f)
                    }
 
                    // 使用Stack布局来叠加头像和在线状态指示器
                    View {
                        attr {
                            width(50f)
                            height(50f)
                        }
 
                        // 头像
                        Image {
                            attr {
                                width(50f)
                                height(50f)
                                borderRadius(25f)
                                src(ctx.messageItem.userAvatar)
                                backgroundColor(Color(0xFFF0F0F0))
                            }
                        }
                    }
 
                    // 在线状态指示器 - 使用绝对定位的替代方案
                    if (ctx.messageItem.isOnline) {
                        View {
                            attr {
                                width(12f)
                                height(12f)
                                borderRadius(6f)
                                backgroundColor(Color(0xFF00C851))
                                marginTop(-12f) // 向上偏移
                                marginLeft(38f) // 向右偏移到头像右下角
                            }
                        }
                    }
                }
 
                // 消息内容
                View {
                    attr {
                        flex(1f)
                        flexDirectionColumn()
                        justifyContentCenter()
                    }
 
                    // 用户名和时间
                    View {
                        attr {
                            flexDirectionRow()
                            alignItemsCenter()
                            justifyContentSpaceBetween()
                            marginBottom(4f)
                        }
 
                        Text {
                            attr {
                                text(ctx.messageItem.userName)
                                fontSize(16f)
                                color(Color(0xFF333333))
                                fontWeightBold()
                            }
                        }
 
                        Text {
                            attr {
                                text(ctx.messageItem.time)
                                fontSize(12f)
                                color(Color(0xFF999999))
                            }
                        }
                    }
 
                    // 最后一条消息
                    View {
                        attr {
                            flexDirectionRow()
                            alignItemsCenter()
                            justifyContentSpaceBetween()
                        }
 
                        Text {
                            attr {
                                text(ctx.messageItem.lastMessage)
                                fontSize(14f)
                                color(Color(0xFF666666))
                                flex(1f)
                            }
                        }
 
                        // 未读消息数量
                        if (ctx.messageItem.unreadCount > 0) {
                            View {
                                attr {
                                    // 根据数字长度动态调整宽度
                                    val count = ctx.messageItem.unreadCount
                                    val displayText = if (count > 99) "99+" else count.toString()
                                    
                                    // 单个数字使用圆形,多个数字使用椭圆形
                                    if (displayText.length == 1) {
                                        width(18f)
                                        height(18f)
                                    } else {
                                        minWidth(20f)
                                        height(18f)
                                        padding(left = 4f, right = 4f)
                                    }
                                    
                                    borderRadius(9f)
                                    backgroundColor(Color(0xFFFF2442))
                                    justifyContentCenter()
                                    alignItemsCenter()
                                    marginLeft(8f)
                                }
 
                                Text {
                                    attr {
                                        text(if (ctx.messageItem.unreadCount > 99) "99+" else ctx.messageItem.unreadCount.toString())
                                        fontSize(10f)
                                        color(Color.WHITE)
                                        fontWeightBold()
                                        textAlignCenter() // 确保文字居中对齐
                                    }
                                }
                            }
                        }
                    }
                }
            }
 
            // 分隔线
            View {
                attr {
                    height(0.5f)
                    backgroundColor(Color(0xFFEEEEEE))
                    marginLeft(78f) // 对齐消息内容
                }
            }
        }
    }
}

最后我们把MessageItemView导出

kotlin 复制代码
internal fun ViewContainer<*, *>.MessageItemView(messageItem: MessageItem, init: MessageItemView.() -> Unit) {
    addChild(MessageItemView(messageItem), init)
}

这样,一个可复用的消息元素组件就完成了,接下来我们会在消息页面中使用这个组件

页面组件实践

消息页面组件(WaterfallMessagePage)

参考前文NavBarView的创建方式,首先定义属性和事件

kotlin 复制代码
internal class WaterfallMessagePageAttr : ComposeAttr()
 
internal class WaterfallMessagePageEvent : ComposeEvent() {
    var onMessageClick: ((MessageItem) -> Unit)? = null
    var onTabChanged: ((Int) -> Unit)? = null
}

然后写一个类继承ComposeView,并把定义好的事件和属性传入

kotlin 复制代码
internal class WaterfallMessagePage : ComposeView<WaterfallMessagePageAttr, WaterfallMessagePageEvent>() {
    
    private var messageList: ObservableList<MessageItem> by observableList<MessageItem>()
    private var selectedTabIndex: Int by observable(0)
    private val tabTitles = listOf("聊天", "赞和收藏", "新增关注")
    
    override fun body(): ViewBuilder {
        val ctx = this
        return {
            attr {
                flex(1f)
                backgroundColor(Color(0xFFF5F5F5))
                flexDirectionColumn()
            }
 
            // 分类标签栏
            View {
                // 标签栏实现
                for (i in ctx.tabTitles.indices) {
                    // 每个标签项的实现
                }
            }
 
            // 消息列表
            Scroller {
                // 根据选中标签显示不同内容
                when (ctx.selectedTabIndex) {
                    0 -> {
                        // 聊天列表
                        filteredMessages.forEachIndexed { index, message ->
                            MessageItemView(message) {
                                event {
                                    click { /* 处理点击事件 */ }
                                    longPress { /* 处理长按事件 */ }
                                }
                            }
                        }
                    }
                    // 其他标签页内容
                }
            }
        }
    }
}
 

最后把WaterfallMessagePage导出,供外部使用

kotlin 复制代码
internal fun ViewContainer<*, *>.WaterfallMessagePage(init: WaterfallMessagePage.() -> Unit) {
    addChild(WaterfallMessagePage(), init)
}

页面运行效果:

卡片详情页组件(CardDetailPage)

卡片详情页也是同理,定义好属性页面,传入CardDetailPage,最后导出

kotlin 复制代码
/**
 * 卡片详情页面组件
 */
internal class CardDetailPage(
    private val item: WaterFallItem,
    private val pageViewWidth: Float
) : ComposeView<CardDetailPageAttr, CardDetailPageEvent>() {
 
    private var likeCount by observable((100..9999).random())
    private var collectCount by observable((50..999).random())
    
    override fun createEvent(): CardDetailPageEvent {
        return CardDetailPageEvent()
    }
 
    override fun createAttr(): CardDetailPageAttr {
        return CardDetailPageAttr()
    }
 
    override fun body(): ViewBuilder {
        val ctx = this
        return {
            attr {
                flex(1f)
                backgroundColor(Color.WHITE)
                flexDirectionColumn()
            }
 
            // 顶部导航栏
            View {
                attr {
                    height(88f)
                    backgroundColor(Color.TRANSPARENT)
                    flexDirectionRow()
                    alignItemsCenter()
                    justifyContentSpaceBetween()
                    paddingTop(44f)
                    paddingLeft(16f)
                    paddingRight(16f)
                }
 
                // 返回按钮
                View {
                    attr {
                        width(32f)
                        height(32f)
                        backgroundColor(Color.WHITE)
                        borderRadius(16f)
                        allCenter()
                    }
 
                    Text {
                        attr {
                            text("<")
                            fontSize(18f)
                            color(Color.BLACK)
                        }
                    }
 
                    event {
                        click {
                            ctx.event.onBackClick?.invoke()
                        }
                    }
                }
            }
 
            // 主内容区域
            Scroller {
                attr {
                    flex(1f)
                    backgroundColor(Color.WHITE)
                }
 
                View {
                    attr {
                        flexDirectionColumn()
                        alignItemsCenter()
                    }
 
                    // 主图片
                    Image {
                        attr {
                            src(ctx.item.imageUrl)
                            width(ctx.pageViewWidth)
                            height((ctx.item.imageHeight / ctx.item.imageWidth) * ctx.pageViewWidth)
                            borderRadius(0f)
                        }
                    }
 
                    // 内容区域
                    View {
                        attr {
                            width(ctx.pageViewWidth)
                            backgroundColor(Color.WHITE)
                            borderRadius(20f, 20f, 0f, 0f)
                            padding(20f)
                            flexDirectionColumn()
                            marginTop(-20f)
                        }
 
                        // 用户信息区域
                        View {
                            attr {
                                height(50f)
                                flexDirectionRow()
                                alignItemsCenter()
                                justifyContentSpaceBetween()
                                marginBottom(16f)
                            }
 
                            // 左侧用户信息
                            View {
                                attr {
                                    flexDirectionRow()
                                    alignItemsCenter()
                                    flex(1f)
                                }
 
                                // 用户头像
                                Image {
                                    attr {
                                        width(40f)
                                        height(40f)
                                        src(ctx.item.userAvatar)
                                        borderRadius(20f)
                                    }
                                }
 
                                View {
                                    attr {
                                        flexDirectionColumn()
                                        marginLeft(12f)
                                        flex(1f)
                                    }
 
                                    // 用户昵称
                                    Text {
                                        attr {
                                            text(ctx.item.userNick)
                                            fontSize(16f)
                                            color(Color.BLACK)
                                            fontWeightBold()
                                        }
                                    }
 
                                    // 发布时间
                                    Text {
                                        attr {
                                            text("2小时前")
                                            fontSize(12f)
                                            color(Color(0xFF999999))
                                            marginTop(2f)
                                        }
                                    }
                                }
                            }
 
                            // 关注按钮
                            View {
                                attr {
                                    width(60f)
                                    height(32f)
                                    backgroundColor(Color(0xFFFF2442))
                                    borderRadius(16f)
                                    allCenter()
                                }
 
                                Text {
                                    attr {
                                        text("关注")
                                        fontSize(14f)
                                        color(Color.WHITE)
                                    }
                                }
 
                                event {
                                    click {
                                        println("点击关注按钮")
                                    }
                                }
                            }
                        }
 
                        // 内容文字
                        Text {
                            attr {
                                text(ctx.item.content)
                                fontSize(16f)
                                color(Color.BLACK)
                                lineHeight(24f)
                                marginBottom(20f)
                            }
                        }
 
                        // 标签区域
                        View {
                            attr {
                                flexDirectionRow()
                                marginBottom(20f)
                            }
 
                            // 示例标签
                            listOf("生活", "美食", "分享").forEach { tag ->
                                View {
                                    attr {
                                        backgroundColor(Color(0xFFF5F5F5))
                                        borderRadius(12f)
                                        marginRight(8f)
                                        marginBottom(8f)
                                    }
 
                                    Text {
                                        attr {
                                            text("#$tag")
                                            fontSize(14f)
                                            color(Color(0xFF666666))
                                        }
                                    }
                                }
                            }
                        }
 
                        // 互动数据
                        View {
                            attr {
                                flexDirectionRow()
                                alignItemsCenter()
                                marginBottom(40f)
                            }
 
                            Text {
                                attr {
                                    text("${ctx.likeCount}次点赞 · ${ctx.collectCount}次收藏")
                                    fontSize(14f)
                                    color(Color(0xFF999999))
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
 
internal class CardDetailPageAttr : ComposeAttr()
 
internal class CardDetailPageEvent : ComposeEvent() {
    var onBackClick: (() -> Unit)? = null
}
 
internal fun ViewContainer<*, *>.CardDetailPage(
    item: WaterFallItem,
    pageViewWidth: Float,
    init: CardDetailPage.() -> Unit
) {
    addChild(CardDetailPage(item, pageViewWidth), init)
}

页面效果:

页面组合与状态管理

有了页面组件,下一步就是使用这些页面组件了,在Kuikly中,我们可以使用PageList组件来管理多个页面的切换

scss 复制代码
@Page("waterfallapp")
internal class WaterfallAPP : BasePager() {
    private var currentTabIndex by observable(0)
    private var isShowingCardDetail by observable(false)
    private var currentDetailItem: WaterFallItem? by observable(null)
 
    override fun body(): ViewBuilder {
        val ctx = this
        return {
            // 主页面内容 - 使用PageList实现页面切换
            PageList {
                attr {
                    flex(1f)
                    flexDirectionRow()
                    pageItemWidth(ctx.pagerData.pageViewWidth)
                    scrollEnable(false) // 禁用手势滑动
                    keepItemAlive(true) // 保持页面状态
                }
 
                // 首页 - 瀑布流内容
                WaterfallHomePage(ctx.pagerData.pageViewWidth, 2) {
                    event {
                        onCardClick = { item ->
                            ctx.showCardDetail(item)
                        }
                    }
                }
 
                // 消息页面
                WaterfallMessagePage {}
 
                // 个人资料页面
                WaterfallProfilePage {}
            }
 
            // 底部导航栏
            BottomNavigationBar(ctx.pageNames) {
                event {
                    onTabClick = { index, title ->
                        ctx.pageListRef?.view?.scrollToPageIndex(index)
                        ctx.currentTabIndex = index
                    }
                }
            }
 
            // 卡片详情页面 - 使用vif指令控制显示
            vif({ ctx.isShowingCardDetail }) {
                ctx.currentDetailItem?.let { item ->
                    View {
                        attr {
                            absolutePosition(0f, 0f, 0f, 0f) // 全屏覆盖
                            backgroundColor(Color.WHITE)
                        }
                        
                        CardDetailPage(item, ctx.pagerData.pageViewWidth) {
                            event {
                                onBackClick = {
                                    ctx.hideCardDetail()
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
 

4.3 小结

通过本次开发实践,我们成功利用 Kuikly 框架的组件化思想,高效地构建了消息页面与卡片详情页。

实践证明,Kuikly 的核心特性:声明式 UI、响应式数据绑定和独立的组件架构,极大地简化了开发流程。我们通过拆分页面级组件(如 WaterfallMessagePage)和可复用组件(如 MessageItemView),构建了清晰、可维护的代码结构。最终,利用 PageList 进行页面组合与状态管理,实现了流畅的应用体验。

在整个开发过程中,Kuikly 框架给我的感受非常舒适。除了核心的声明式 UI、响应式数据绑定和组件化设计大大提升了开发效率之外,得益于Kuikly详细的官方教程,我们能够快速的上手开发、顺利搭建项目,遇到问题总能很快找到解答,运行起来也非常稳定。

作为个人开发者,我尤其感受到 Kuikly 的跨平台能力带来的轻松和高效,让我可以用一套代码同时覆盖多个平台,节省了大量时间和精力。整体来说,Kuikly 框架在各个方面都给了我很不错的体验,非常适合个人开发者快速迭代和产品落地。

相关推荐
我有与与症6 小时前
从0使用Kuikly框架写一个小红书Demo-Day3
客户端
我有与与症2 天前
从0使用Kuikly框架写一个小红书Demo-Day2
客户端
我有与与症2 天前
从0使用Kuikly框架写一个小红书Demo-Day1
客户端
赴3353 天前
基于pth模型文件,使用flask库将服务端部署到开发者电脑
人工智能·flask·客户端·模型部署·服务端
程序员老刘4 天前
2025年Flutter状态管理新趋势:AI友好度成为技术选型第一标准
flutter·ai编程·客户端
奔跑吧邓邓子8 天前
【C++实战(63)】C++ 网络编程实战:UDP客户端与服务端的奥秘之旅
网络·c++·udp·实战·客户端·服务端
程序员老刘15 天前
Flutter版本选择指南:避坑3.27 | 2025年9月
flutter·客户端
charlie11451419119 天前
Chrome View渲染机制学习小记
前端·chrome·学习·渲染·gpu·客户端
程序员老刘24 天前
跨平台开发地图:客户端技术选型指南 | 2025年9月
flutter·客户端