构建 compose 界面
在 jetpack compose 中构建界面,核心逻辑围绕“状态控制”展开,因为 compose 的界面本身是不可变的 : 一旦绘制完成,就没法直接修改。我们能操控的只有界面的状态,只要状态发生变化,compose 就会智能地重新创建界面树中那些已经改变的部分。像 textfield 这样的可组合项,就是典型的“接收状态、公开事件”的组件:它接收一个值来显示文本,同时通过 onvaluechange 回调这种事件,来处理用户修改文本的操作。比如下面这段常见代码:
var name by remember { mutablestateof("")}
outlinedtextfield(
value = name,
onvaluechange = { name = it },
label ={ text ("name" )}
)由于可组合项天生具备“接收状态、公开事件”的特性,单向数据流模式和 jetpack compose 可以说是完美适配。这篇指南就专门讲怎么在 compose 里用单向数据流模式、怎么实现事件和状态容器,还有如何结合 viewmodel 使用。
注意:用 jetpack compose 开发界面,并不会影响应用的其他层,比如负责数据存储的数据层和负责业务逻辑的业务层。如果想了解整个应用所有层级的构建方法,可以参考官方的应用架构指南。
单向数据流
单向数据流(udf)是一种简单清晰的设计模式,核心规则就两条:状态向下流动,事件向上传递。采用这种模式后,负责显示状态的可组合项,和负责存储、修改状态的应用部分就能彻底分离开,各司其职。
什么是状态向下流动,事件向上传递
- 状态向下流动
- 核心定义:状态(比如界面要显示的文本内容、按钮是否可点击、加载框是否显示等,通常存储在 viewmodel 或上层可组合项中),是从上层组件/数据层传递到下层组件/界面层的,下层组件只能被动接收和使用状态,不能直接修改上层传来的状态。
- compose 实际场景:
- 比如 viewmodel 中维护着一个
uistate状态(包含登录状态、用户昵称等数据),这个状态会向下传递给登录界面、个人中心界面等可组合项; - 父可组合项定义的
isbuttonenabled状态,向下传给子可组合项button,用于控制按钮是否可点击; - 下层组件拿到状态后,只能用它来渲染界面,比如根据
uistate中的昵称显示文本,根据isbuttonenabled决定按钮的交互状态。
- 比如 viewmodel 中维护着一个
- 事件向上流动
- 核心定义:事件(比如用户点击按钮、输入文本、下拉刷新等用户操作,或是系统通知等),是从下层组件/界面层传递到上层组件/数据层的,下层组件不处理事件逻辑,只负责将事件“上报”,由上层组件统一处理并更新状态。
- compose 实际场景:
- 用户点击登录按钮,这个点击事件不会在
button组件内部处理,而是通过onclick回调向上传递给 viewmodel,由 viewmodel 执行登录请求、校验账号密码等业务逻辑; - 用户在
textfield输入文本,通过onvaluechange回调将输入事件向上传递,由上层组件(或 viewmodel)更新对应的文本状态; - 事件处理完成后,上层组件会更新状态,而更新后的状态又会顺着“向下流动”的规则,重新传递给下层界面,触发界面重组。
- 用户点击登录按钮,这个点击事件不会在
这就是单向数据流界面更新的三步完整循环,逻辑一目了然 :
- 产生事件:要么是界面上的操作生成事件(比如用户点击按钮),并把事件向上传递给 viewmodel 处理;要么是应用其他层传来事件(比如提示用户登录会话已过期)。
- 更新状态:事件被处理后,对应的业务逻辑会修改相关的状态。
- 显示状态:存储状态的容器会把新状态向下传递给界面,界面再根据新状态刷新显示。
遵循该模式的三大优势
在 jetpack compose 中使用单向数据流,能解决开发中的不少麻烦,带来三个核心好处:
- 可测试性更强:状态和界面分离开,我们可以单独测试状态的逻辑是否正确,也能单独验证界面显示是否符合预期,不用混在一起测试。
- 状态封装更严谨:状态的更新被限制在一个固定位置,而且可组合项的状态只有一个可信来源,不会出现多个地方改状态导致的状态不一致问题,能大幅减少 bug。
- 界面显示更一致:只要用
stateflow或者livedata这类可观察的状态容器,任何状态更新都会立刻反映在界面上,不会出现“状态变了,界面没更”的情况。
jetpack compose 中的单向数据流
可组合项的工作核心就是“状态”和“事件”。比如 textfield,只有当它的 value 参数更新,并且通过 onvaluechange 回调(本质是请求改值的事件)触发时,它才会更新显示内容。
在 compose 里,state 对象是专门用来存值的容器,而且它有个关键特性:状态值一旦改变,就会触发读取该值的可组合函数重组。我们存储状态有两种常用方式,选哪种取决于需要保留状态的时长:
remember { mutablestateof(value) }remembersaveable { mutablestateof(value) }
textfield 的值是 string 类型,这个值的来源很灵活,可以是硬编码的固定值、viewmodel 中的数据,也可以是从父级可组合项传过来的。虽然不一定非要把它存在 state 对象里,但必须注意:当 onvaluechange 事件触发时,一定要手动更新这个值。
三个核心要点
- mutablestateof(value):会创建一个
mutablestate对象,这是 compose 里的可观察类型。只要它的值变了,系统就会安排所有读取过这个值的可组合函数进行重组。 - remember:负责把对象存储在组合中。如果调用
remember的可组合项从组合中被移除了,那它存储的这个对象也会被忘记。 - remembersaveable:比
remember多了个“持久化”能力——它会把状态存在bundle里,就算应用遇到屏幕旋转这种配置更改,状态也不会丢失。
注意:如果想深入了解 compose 中的状态以及状态提升的相关知识,可以参考专门的“状态和 jetpack compose”指南。
定义可组合项参数
给可组合项定义状态参数时,别盲目添加,先想清楚两个问题:这个可组合项需要有多高的可重用性和灵活性?这些状态参数会对它的性能产生什么影响?
核心原则是:为了方便分离逻辑和重复使用,每个可组合项只包含最必要的信息。举个例子,我们要做一个显示新闻标题的可组合项 header,两种写法差别很大:
@composable
fun header(title: string, subtitle: string) {
// 只有 title 或 subtitle 变化时,才会重组
}
@composable
fun header(news: news) {
// 只要传入新的 news 实例,不管标题副标题变没变化,都会重组
}有时候用独立的参数还能提升性能。比如 news 类里除了标题和副标题,还有作者、时间等很多其他信息,这时用 header(news) 的写法就很不划算——哪怕只有无关信息变了,生成了新的 news 实例,header 也会无辜重组。
另外还要注意参数数量:如果一个可组合函数的参数太多,使用起来会很麻烦。这种情况下,建议把这些参数整合到一个类里,再传递这个类的对象。
compose 中的事件
应用里所有用户输入或系统通知,都应该被当作事件来处理——比如按钮点击、文本输入变化,甚至是计时器触发、网络请求结果通知等。这些事件触发后,不能由界面层直接修改状态,而是要交给 viewmodel 处理,再由 viewmodel 去更新界面状态。
这里有个重要原则:界面层绝对不能在事件处理逻辑之外修改状态,不然很容易导致应用状态混乱,出现各种难以排查的 bug。
传递不可变值的优势
给状态和事件处理的 lambda 传递参数时,最好用不可变值。这么做有四个明显好处:
- 提升可重用性:不可变参数让可组合项在不同场景下都能使用。
- 防止界面乱改状态:确保界面只能通过事件回调触发状态更新,不能直接修改状态值。
- 避免并发问题:能保证不会有其他线程偷偷修改这个状态。
- 降低代码复杂度:不可变值的状态变化更可控,代码逻辑更容易理解和维护。
通用可组合项示例
比如应用的顶部导航栏(topappbar),通常都要显示文本,还要有个返回按钮。我们可以做一个通用的 myapptopappbar 可组合项,专门接收文本内容和返回按钮的点击事件,这样在整个应用里都能复用:
@composable
fun myapptopappbar(topappbartext: string, onbackpressed: () -> unit) {
topappbar(
title = {
text(
text = topappbartext,
textalign = textalign.center,
modifier = modifier.fillmaxsize().wrapcontentsize(alignment.center)
)
},
navigationicon = {
iconbutton(onclick = onbackpressed) {
icon(
icons.filled.arrowback,
contentdescription = localizedstring
)
}
},
// ...
)
}viewmodel、状态和事件:示例
结合 viewmodel 和 mutablestateof,我们就能在应用中完整实现单向数据流。核心要求有两个:一是界面状态要通过 stateflow 或 livedata 这类可观察的状态容器公开;二是 viewmodel 要负责处理来自界面或其他层的事件,并根据事件更新状态容器。
登录屏幕的状态与事件建模
我们以登录屏幕为例,看看具体怎么实现。一个登录屏幕通常有四种状态,而且状态之间是互斥的,用密封类(密封类能限制状态的取值范围,很适合这种场景)建模最合适:
- 退出登录状态:用户还没登录的时候。
- 进行中状态:应用正在发起网络请求,尝试让用户登录的时候(比如显示加载转圈)。
- 错误状态:登录过程中出现问题(比如网络错误、账号密码错误)。
- 登录成功状态:用户顺利登录后。
viewmodel 里会维护一个私有状态 _uistate,再对外提供一个只读的 uistate 供界面访问。同时,viewmodel 还会提供 onsignin() 这样的方法,专门处理登录事件。代码示例如下:
class myviewmodel : viewmodel() {
private val _uistate = mutablestateof<uistate>(uistate.signedout)
val uistate: state<uistate> get() = _uistate
// ...
}其他状态容器的用法
除了 mutablestateof,compose 还为 livedata、flow、observable 这些常用组件提供了扩展,让它们能注册为监听器,把自身的值当作 compose 状态来使用。比如用 livedata 的写法:
class myviewmodel : viewmodel() {
private val _uistate = mutablelivedata<uistate>(uistate.signedout)
val uistate: livedata<uistate> get() = _uistate
// ...
}
@composable
fun mycomposable(viewmodel: myviewmodel) {
val uistate = viewmodel.uistate.observeasstate()
// ...
}到此这篇关于android compose 界面架构 : 基于单向数据流的文章就介绍到这了,更多相关android compose 单向数据流内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论