Files
WanAndroid/wanandroid-composeRicardoJiang/README.md
T
coco 7a4fb0e6ae a
2026-07-03 16:23:31 +08:00

219 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 前言
今年七月底,`Google` 正式发布了 `Jetpack Compose``1.0` 稳定版本,这说明`Google`认为`Compose`已经可以用于生产环境了。相信`Compose`的广泛应用就在不远的将来,现在应该是学习`Compose`的一个比较好的时机
在了解了`Compose`的基本知识与原理之后,通过一个完整的项目继续学习`Compose`应该是一个比较好的方式。本文主要基于`Compose`,`MVI`架构,单`Activity`架构等,快速实现一个`wanAndroid`客户端,如果对您有所帮助可以点个`Star`: [wanAndroid-compose](https://github.com/shenzhen2017/wanandroid-compose)
## 效果图
首先看下效果图
| ![请添加图片描述](https://raw.githubusercontents.com/shenzhen2017/resource/main/2021/november/p13.png) | ![在这里插入图片描述](https://raw.githubusercontents.com/shenzhen2017/resource/main/2021/november/p14.png) |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| ![请添加图片描述](https://raw.githubusercontents.com/shenzhen2017/resource/main/2021/november/p15.png) | ![在这里插入图片描述](https://raw.githubusercontents.com/shenzhen2017/resource/main/2021/november/p16.png) |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| ![请添加图片描述](https://raw.githubusercontents.com/shenzhen2017/resource/main/2021/november/p17.png) | ![请添加图片描述](https://raw.githubusercontents.com/shenzhen2017/resource/main/2021/november/p18.png) |
## 主要实现介绍
各个页面的具体实现可以查看源码,这里主要介绍一些主要的实现与原理
### 使用`MVI`架构
`MVI``MVVM` 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,架构图如下所示
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb3fe9361e244430bd2f69b70c7b0e75~tplv-k3u1fbpfcp-watermark.awebp)
其主要分为以下几部分
1. `Model`: 与`MVVM`中的`Model`不同的是,`MVI``Model`主要指`UI`状态(`State`)。例如页面加载状态、控件位置等都是一种`UI`状态
2. `View`: 与其他`MVX`中的`View`一致,可能是一个`Activity`或者任意`UI`承载单元。`MVI`中的`View`通过订阅`Model`的变化实现界面刷新
3. `Intent`: 此`Intent`不是`Activity``Intent`,用户的任何操作都被包装成`Intent`后发送给`Model`层进行数据请求
例如登录页面的`Model``Intent`定义如下
```kotlin
/**
* 页面所有状态
/
data class LoginViewState(
val account: String = "",
val password: String = "",
val isLogged: Boolean = false
)
/**
* 一次性事件
*/
sealed class LoginViewEvent {
object PopBack : LoginViewEvent()
data class ErrorMessage(val message: String) : LoginViewEvent()
}
/**
* 页面Intent,即用户的操作
/
sealed class LoginViewAction {
object Login : LoginViewAction()
object ClearAccount : LoginViewAction()
object ClearPassword : LoginViewAction()
data class UpdateAccount(val account: String) : LoginViewAction()
data class UpdatePassword(val password: String) : LoginViewAction()
}
```
如上所示
1. 通过`ViewState`定义页面所有状态
2. `ViewEvent`定义一次性事件如`Toast`,页面关闭事件等
3. 通过`ViewAction`定义所有用户操作
`MVI`架构与`MVVM`架构的主要区别在于:
1. `MVVM`并没有约束`View`层与`ViewModel`的交互方式,具体来说就是`View`层可以随意调用`ViewModel`中的方法,而`MVI`架构下`ViewModel`的实现对`View`层屏蔽,只能通过发送`Intent`来驱动事件。
2. `MVVM``ViewModle` 中分散定义了多个 `State` `MVI` 使用 `ViewState``State` 集中管理,只需要订阅一个 `ViewState` 便可获取页面的所有状态,相对 `MVVM` 减少了不少模板代码
`Compose` 的声明式`UI`思想来自 `React`,理论上同样来自 `Redux` 思想的 `MVI` 应该是 `Compose` 的最佳伴侣
但是`MVI`也只是在`MVVM`的基础上做了一定的改良,`MVVM` 也可以很好地配合 `Compose` 使用,各位可根据自己的需要选择合适的架构
关于`Compose`的架构选择可参考:[Jetpack Compose 架构如何选? MVP, MVVM, MVI](https://juejin.cn/post/6969382803112722446)
### 单`Activity`架构
早在`View`时代,就有不少推荐单`Activity`+多`Fragment`架构的文章,`Google`也推出了`Jetpack Navigation`库来支持这种单`Activity`架构
对于`Compose`来说,因为`Activity``Compose`是通过`AndroidComposeView`来中转的,`Activity`越多,就需要创建出越多的`AndroidComposeView`,对性能有一定影响
而使用单`Activity`架构,所有变换页面跳转都在`Compose`内部完成,可能也是出于这个原因,目前`Google`的示例项目都是基于单`Activity`+`Navigation`+多`Compose`架构的
但是使用单`Activity`架构也需要解决一些问题
1. 所有的`viewModel`都在一个`Activity``ViewModelStoreOwner`中,那么当一个页面销毁了,此页面用过的`viewModel`应该什么时候销毁呢?
2. 有时候页面需要监听自己这个页面的`onResume``onPause`等生命周期,单`Activity`架构下如何监听生命周期呢?
我们下面就一起来看下如何解决单`Activity`架构下的这两个问题
### 页面`ViewModel`何时销毁?
`Compose`中一般可以通过以下两种方式获取`ViewModel`
```kotlin
//方式1
@Composable
fun LoginPage(
loginViewModel: LoginViewModel = viewModel()
) {
//...
}
//方式2
@Composable
fun LoginPage(
loginViewModel: LoginViewModel = hiltViewModel()
) {
//...
}
```
如上所示:
1. 方式1将返回一个与`ViewModelStoreOwner`(一般是`Activity``Fragment`)绑定的`ViewModel`,如果不存在则创建,已存在则直接返回。很明显通过这种方式创建的`ViewModel`的生命周期将与`Activity`一致,在单`Activity`架构中将一直存在,不会释放。
2. 方式2通过`Hilt`实现,可以在`Composable`中获取`NavGraph Scope``Destination Scope``ViewModel`,并自动依赖 `Hilt` 构建。`Destination Scope``ViewModel` 会跟随 `BackStack` 的弹出自动 `Clear` ,避免泄露。
总得来说,通过`hiltViewModel``Navigation`配合,是一个更好的选择
### `Compose`如何获取生命周期?
为了在`Compose`中获取生命周期,我们需要先了解下[副作用](https://developer.android.google.cn/jetpack/compose/side-effects?hl=zh-cn)
用一句话概括副作用:一个函数的执行过程中,除了返回函数值之外,对调用方还会带来其他附加影响,例如修改全局变量或修改参数等。
副作用必须在合适的时机执行,我们首先需要明确一下`Composable`的生命周期:
1. `onActiveor onEnter`:当`Composable`首次进入组件树时
2. `onCommitor onUpdate``UI`随着`recomposition`发生更新时
3. `onDisposeor onLeave`:当`Composable`从组件树移除时
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/07432c8e4f5c4492baff11f3f1fb4802~tplv-k3u1fbpfcp-watermark.awebp)
了解了`Compose`的生命周期后,我们可以发现,如果我们在`onActive`时监听`Activity`的生命周期,在`onDispose`时取消监听,不就可以实现在`Compose`中获取生命周期了吗?
`DisposableEffect`可以帮助我们实现这个需求,`DisposableEffect`在其监听的`Key`发生变化,或`onDispose`时会执行
我们还可以通过添加参数,让其仅在`onActive``onDispose`时执行:例如`DisposableEffect(true)``DisposableEffect(Unit)`
通过以下方式,就可以实现在`Compose`中监听页面生命周期
```kotlin
@Composable
fun LoginPage(
loginViewModel: LoginViewModel = hiltViewModel()
) {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = Unit) {
val observer = object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
viewModel.dispatch(Action.Resume)
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause() {
viewModel.dispatch(Action.Pause)
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
```
当然有时也不需要这么复杂,比如我们需要在进入或返回`ProfilePage`页面时刷新登录状态,并根据登录状态确认页面`UI`,就可以通过以下方式实现
```kotlin
@Composable
fun ProfilePage(
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: ProfileViewModel = hiltViewModel()
) {
//...
DisposableEffect(Unit) {
Log.i("debug", "onStart")
viewModel.dispatch(ProfileViewAction.OnStart)
onDispose {
}
}
}
```
如上所示,每当进入页面或返回该页面时,我们就可以刷新页面登录状态了
### `Compose`如何保存`LazyColumn`列表状态
相信使用过`LazyColumn`的同学都碰到过下面的问题
> 使用`Paging3`加载分页数据,并显示到页面`A`的`LazyColumn`上,向下滑动`LazyColumn`,然后`navigation.navigate`跳转到页面`B`,接着再`navigatUp`回到页面`A`,页面`A`的`LazyColumn`又回到了列表顶部
但是我们可以看到,`LazyListState`其实是通过`rememberLazyListState`做了持久化保存的,如下图所示
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/291ab6de0d274da2bb1d875faa3c7d7e~tplv-k3u1fbpfcp-watermark.awebp?)
既然做了持久化保存,那为什么返回时的位置还有问题呢?其实纯粹使用 `Paging` + `LazyColumn`,当页面切换时,会记录当前页面位置,但如果通过`item`加上`Header``Footer`就不行了
这是因为`rememberLazyListState`会在列表中至少有一项时`restore`滚动位置,同时`Paging`是通过`Flow`获取数据的,当返回到页面重组时并不能马上获取到`Paging`数据,第一帧时`Paging``itemCount`为0
但同时因为`LazyColumn`中已经有了一个`Header`,这时便会还原保存的位置,但因为这时`Paging`中的数据还为空,不能滚动到正确的位置,于是便又滚动到顶部了
而当`LazyColumn`中没有`Header`时,列表中至少有一项时便是`Paging`数据成功填充的时候,这个时候还原的位置就是对的,所以没有问题
既然原因在于`LazyListState`没有在正确的时机被还原,那我们将`LazyListSate`保存在`ViewModel`中,并且在`Paging`中有数据时再还原`listState`,如下所示:
```kotlin
@HiltViewModel
class SquareViewModel @Inject constructor(
private var service: HttpService,
) : ViewModel() {
private val pager by lazy { simplePager { service.getSquareData(it) }.cachedIn(viewModelScope) }
val listState: LazyListState = LazyListState()
}
@Composable
fun SquarePage(
navCtrl: NavHostController,
scaffoldState: ScaffoldState,
viewModel: SquareViewModel = hiltViewModel()
) {
val squareData = viewStates.pagingData.collectAsLazyPagingItems()
// 当`Paging`有数据时,返回`ViewModel`中的`listState`
val listState = if (squareData.itemCount > 0) viewStates.listState else LazyListState()
RefreshList(squareData, listState = listState) {
itemsIndexed(squareData) { _, item ->
//...
}
}
}
```
总得来说,对于一般的页面,`rememberLazyListState`已经足够,但是对于有`Header``Footer``Paging`页面,需要一些特殊处理
关于`LazyColumn`滚动丢失的问题,更详细的讨论可参考:[Scroll position of LazyColumn built with collectAsLazyPagingItems is lost when using Navigation
](https://issuetracker.google.com/issues/177245496?pli=1)
## 总结
### 项目地址
[https://github.com/shenzhen2017/wanandroid-compose](https://github.com/shenzhen2017/wanandroid-compose)
开源不易,如果项目对你有所帮助,欢迎点赞,`Star`,收藏~
### 参考资料
[https://github.com/manqianzhuang/HamApp](https://github.com/manqianzhuang/HamApp)
[https://github.com/linxiangcheer/PlayAndroid](https://github.com/linxiangcheer/PlayAndroid)
[从零到一写一个完整的 Compose 版本的天气](https://juejin.cn/post/7030986229512404999)