消息页面与卡片详情页的组件化开发
在学习了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 框架在各个方面都给了我很不错的体验,非常适合个人开发者快速迭代和产品落地。