Jetpack Compose 架构怎样选?MVP 、 MVVM 照旧 MVI?(1)
// build.gradleimplementation “androidx.navigation:navigation-compose:$latest_version”
@Composable
fun MvvmApp(
mvvmViewModel: MvvmViewModel
) {
val navController = rememberNavController()
LaunchedEffect(Unit) {
mvvmViewModel.navigateToResults
.collect {
navController.navigate(“result”) //订阅VM路由变乱通知,处理路由跳转
}
}
NavHost(navController, startDestination = “searchBar”) {
composable(“searchBar”) {
MvvmSearchBarScreen(
mvvmViewModel,
)
}
composable(“result”) {
MvvmSearchResultScreen(
mvvmViewModel,
)
}
}
}
[*] 在 root-level 的 MvvmApp 中界说 NavGraph, composable(“$dest_id”){} 中构造路由节点的各个子 Screen,构造时传入 ViewModel 用于 Screen 之间的通信
[*] 每个 Composable 都有一个 CoroutineScope 与其 Lifecycle 绑定,LaunchedEffect{} 可以在这个 Scope 中启动协程处理副作用。代码中使用了一个只实行一次的 Effect 订阅 ViewModel 的路由变乱通知
[*] 固然我们可以将 navConroller 也传给 MvvmSearchBarScreen ,在其内部直接发起路由跳转。但在较复杂的项目中,跳转逻辑与页面界说应该尽量保持解耦,这更利于页面的复用和测试。
[*] 我们也可以在 Composeable 中直接 mutableStateOf() 创建 state 来处理路由跳转,但是既然选择使用 ViewModel 了,那就应该尽可能将所有 state 集中到 ViewModle 管理。
注意: 上面例子中的处理路由跳转的 navigateToResults 是一个“变乱”而非“状态”,关于这部分区别,在后文在具体阐述
界说子 Screen
接下来看一下两个 Screen 的具体实现
@Composable
fun MvvmSearchBarScreen(
mvvmViewModel: MvvmViewModel,
) {
SearchBarScreen {
mvvmViewModel.searchKeyword(it)
}
}
@Composable
fun MvvmSearchResultScreen(
mvvmViewModel: MvvmViewModel
) {
val result by mvvmViewModel.result.collectAsState()
val isLoading by mvvmViewModel.isLoading.collectAsState()
SearchResultScreen(result, isLoading, mvvmViewModel.key.value)
}
大量逻辑都抽象到 ViewModel 中,以是 Screen 非常简洁
[*] SearchBarScreen 接受用户输入,将搜刮关键词发送给 ViewModel
[*] MvvmSearchResultScreen 作为效果页表现 ViewModel 发送的数据,包括 Loading 状态和搜刮效果等。
[*] collectAsState 用来将 Flow 转化为 Compose 的 state,每当 Flow 接收到新数据时会触发 Composable 重组。Compose 同时支持 LiveData、RxJava 等其他响应式库的collectAsState
UI层的更多内容可以查阅 SearchBarScreen 和 SearchResultScreen 的源码。颠末逻辑抽离后,这两个 Composable 只剩余结构相关的代码,可以在任何一种 MVX 中实现复用。
ViewModel 实现
末了看一下 ViewModel 的实现
class MvvmViewModel(
private val searchService: DataRepository,
) {
private val coroutineScope = MainScope()
private val _isLoading: MutableStateFlow = MutableStateFlow(false)
val isLoading = _isLoading.asStateFlow()
private val _result: MutableStateFlow<List> = MutableStateFlow(emptyList())
val result = _result.asStateFlow()
private val _key = MutableStateFlow(“”)
val key = _key.asStateFlow()
//使用Channel界说变乱
private val _navigateToResults = Channel(Channel.BUFFERED)
val navigateToResults = _navigateToResults.receiveAsFlow()
fun searchKeyword(input: String) {
coroutineScope.launch {
_isLoading.value = true
_navigateToResults.send(true)
_key.value = input
val result = withContext(Dispatchers.IO) { searchService.getArticlesList(input) }
_result.emit(result.data.datas)
_isLoading.value = false
}
}
}
[*] 接收到用户输入后,通过 DataRepository 发起搜刮请求
[*] 搜刮过程中依次更新 loading(loading表近况态)、navigateToResult(页面跳转变乱)、 key(搜刮关键词)、result(搜刮效果)等内容,不停驱动UI刷新
所有状态集中在 ViewModel 管理,乃至页面跳转、Toast弹出等变乱也由 ViewModel 负责通知,这对单元测试非常友爱,在单测中无需再 mock 各种UI相关的上下文。
Jetpack MVVM
========================================================================
Jeptack 的意义在于降低 MVVM 在 Android平台的落地成本。
引入 Jetpack 后的代码变革不大,主要变动在于 ViewModel 的创建。
Jetpack 提供了多个组件,降低了 ViewModel 的使用成本:
[*] 通过 hilt 的 DI 降低 ViewModel 构造成本,无需手动传入 DataRepository 等依赖
[*] 任意 Composable 都可以从最近的 Scope 中获取 ViewModel,无需层层传参。
@HiltViewModel
class JetpackMvvmViewModel @Inject constructor(
private val searchService: DataRepository // DataRepository 依靠DI注入
) : ViewModel() {
…
}
@Composable
fun JetpackMvvmApp() {
val navController = rememberNavController()
NavHost(navController, startDestination = “searchBar”, route = “root”) {
composable(“searchBar”) {
JetpackMvvmSearchBarScreen(
viewModel(navController, “root”) //viewModel 可以在需要时再获取, 无需实现创建好并通过参数传进来
)
}
composable(“result”) {
JetpackMvvmSearchResultScreen(
viewModel(navController, “root”) //可以获取跟同一个ViewModel实例
)
}
}
}
@Composable
inline fun viewModel(
navController: NavController,
graphId: String = “”
): VM =
//在 NavGraph 全局范围使用 Hilt 创建 ViewModel
hiltNavGraphViewModel(
backStackEntry = navController.getBackStackEntry(graphId)
)
Jetpack 乃至提供了 hilt-navigation-compose 库,可以在 Composable 中获取 NavGraph Scope 或 Destination Scope 的 ViewModel,并自动依赖 Hilt 构建。Destination Scope 的 ViewModel 会跟随 BackStack 的弹出自动 Clear ,克制泄露。
// build.gradle
implementation androidx.hilt:hilt-navigation-compose:$latest_versioin
“未来 Jetpack 各组件之间协同效应会变得越来越强。” 参考
https://developer.android.com/jetpack/compose/libraries#hilt
MVI
===============================================================
MVI 与 MVVM 很相似,其借鉴了前端框架的思想,更加夸大数据的单向活动和唯一数据源,可以看做是 MVVM + Redux 的联合。
MVI 的 I 指 Intent,这里不是启动 Activity 那个 Intent,而是一种对用户操作的封装形式,为克制肴杂,也可唤做 Action 等其他称呼。用户操作以 Action 的形式送给 Model层 举行处理。代码中,我们可以用 Jetpack 的 ViewModel 负责 Intent 的接受和处理,由于 ViewModel 可以在 Composable 中方便获取。
https://i-blog.csdnimg.cn/blog_migrate/9140dcf4acd6b0c7408124696a0dc35b.png
在 SearchBarScreen 用户输入关键词后通过 Action 通知 ViewModel 举行搜刮
@Composable
fun MviSearchBarScreen(
mviViewModel: MviViewModel,
onConfirm: () -> Unit
) {
SearchBarScreen {
mviViewModel.onAction(MviViewModel.UiAction.SearchInput(it))
}
}
通过 Action 通信,有利于 View 与 ViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控
@Composable
fun MviSearchResultScreen(
mviViewModel: MviViewModel
) {
val viewState by mviViewModel.viewState.collectAsState()
SearchResultScreen(
viewState.result, viewState.isLoading, viewState.key
)
}
MVVM 的 ViewModle 中分散界说了多个 State ,MVI 使用 ViewState 对 State 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 淘汰了不少模板代码。
相对于 MVVM,ViewModel 也有一些变革
class MviViewModel(
private val searchService: DataRepository,
) {
private val coroutineScope = MainScope()
private val _viewState: MutableStateFlow = MutableStateFlow(ViewState())
val viewState = _viewState.asStateFlow()
private val _navigateToResults = Channel(Channel.BUFFERED)
val navigateToResults = _navigateToResults.receiveAsFlow()
fun onAction(uiAction: UiAction) {
when (uiAction) {
is UiAction.SearchInput -> {
coroutineScope.launch {
_viewState.value = _viewState.value.copy(isLoading = true)
val result =
withContext(Dispatchers.IO) { searchService.getArticlesList(uiAction.input) }
_viewState.value =
_viewState.value.copy(result = result.data.datas, key = uiAction.input)
_navigateToResults.send(OneShotEvent.NavigateToResults)
_viewState.value = _viewState.value.copy(isLoading = false)
}
}
}
}
data class ViewState(
val isLoading: Boolean = false,
val result: List = emptyList(),
val key: String = “”
)
sealed class OneShotEvent {
object NavigateToResults : OneShotEvent()
}
sealed class UiAction {
class SearchInput(val input: String) : UiAction()
}
}
[*] 页面所有的状态都界说在 ViewState 这个 data class 中,状态的修改只能在 onAction 中举行, 其余场所都是 immutable 的, 包管了数据流只能单向修改。反观 MVVM ,MutableStateFlow 对外袒露时转成 immutable 才华包管这种安全性,需要增长不少模板代码且仍然轻易遗漏。
[*] 变乱则同一界说在 OneShotEvent中。Event 不同于 State,同一类型的变乱允许响应多次,因此界说变乱使用 Channel 而不是 StateFlow。
Compose 鼓励多使用 State 少使用 Event, Event 只适适用在弹 Toast 等少数场景中
通过欣赏 ViewModel 的 ViewState 和 Aciton 界说就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。
页面路由
================================================================
Sample 中之以是使用变乱而非状态来处理路由跳转,一个主要原因是由于使用了 Navigation。Navigation 有自己的 backstack 管理,当点击 back 键时会自动资助我们返回前一页面。倘若我们使用状态来描述当前页面,当点击 back时,没有时机更新状态,这将造成 ViewState 与 UI 的不一致。
关于路由方案的建议:简单项目使用变乱控制页面跳转没有问题,但是对于复杂项目,推荐使用状态举行页面管理,有利于逻辑层时刻感知到当前的UI状态。
我们可以将 NavController 的 backstack 状态 与 ViewModel 的状态创建同步:
class MvvmViewModel(
private val searchService: DataRepository,
) {
…
//使用 StateFlow 描述页面
private val _destination = MutableStateFlow(DestSearchBar)
val destination = _destination.asStateFlow()
fun searchKeyword(input: String) {
coroutineScope.launch {
…
_destination.value = DestSearchResult
…
}
}
fun bindNavStack(navController: NavController) {
//navigation 的状态时刻同步到 viewModel
navController.addOnDestinationChangedListener { _, _, arguments ->
run {
_destination.value = requireNotNull(arguments?.getString(KEY_ROUTE))
}
}
}
}
如上,当 navigation 状态变革时,会及时同步到 ViewModel ,这样就可以使用 StateFlow 而非 Channel 来描述页面状态了。
@Composable
fun MvvmApp(
mvvmViewModel: MvvmViewModel
) {
val navController = rememberNavController()
LaunchedEffect(Unit) {
with(mvvmViewModel) {
bindNavStack(navController) //创建同步
destination
.collect {
navController.navigate(it)
}
}
}
}
在入口处,为 NavController 和 ViewModel 创建同步绑定即可。
Clean Architecture
==============================================================================
更大型的项目中,会引入 Clean Architecture ,通过 Use Case 将 ViewModel 内的逻辑进一步分解。Compose 只是个 UI 框架,对于 ViewModle 以下的逻辑层的管理方式与传统的 Andorid 开发没有区别。以是 Clean Architecture 这样的复杂架构仍然可以在 Compose 项目中使用
总结
==============================================================
比力了这么多种架构,那种与 Compose 最契合呢?
Compose 的声明式UI思想来自 React,以是同样来自 Redux 思想的 MVI 应该是 Compose 的最佳朋友。固然 MVI 只是在 MVVM 的底子上做了一些改良,如果你已经有了一个 MVVM 的项目,只是想将 UI 部分改造成 Compose ,那么没必要为了改造成 MVI 而举行重构,MVVM 也可以很好地共同 Compose 使用的。但是如果你想将一个 MVP 项目改造成 Compose 可能成本就有点大了。
关于 Jetpack,如果你的项目只用于 Android,那么 Jetpack 无疑是一个好工具。但是 Compose 未来的应用场景将会很广泛,如果你有预期未来会共同 KMP 开发跨平台应用,那么就需要学会不依赖 Jetpack 的开发方式,这也是本文为什么要介绍非 Jetpack 下的 MVVM 的一个初衷。
末了
==============================================================
Android进阶资料
以下的资料是比年来,我和一些朋侪面试收集整理了很多大厂的面试真题和资料,还有来自若阿里、小米、爱奇艺等一线大厂的大牛整理的架构进阶资料。希望可以资助到大家。
Android进阶焦点笔记
https://i-blog.csdnimg.cn/blog_migrate/b167dc7e5e27a13bda40460894f38337.png
百万年薪必刷面试题
https://i-blog.csdnimg.cn/blog_migrate/88baa0269e714c5a6432847cf391d2d9.png
最全Android进阶学习视频
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
最佳朋友。固然 MVI 只是在 MVVM 的底子上做了一些改良,如果你已经有了一个 MVVM 的项目,只是想将 UI 部分改造成 Compose ,那么没必要为了改造成 MVI 而举行重构,MVVM 也可以很好地共同 Compose 使用的。但是如果你想将一个 MVP 项目改造成 Compose 可能成本就有点大了。
关于 Jetpack,如果你的项目只用于 Android,那么 Jetpack 无疑是一个好工具。但是 Compose 未来的应用场景将会很广泛,如果你有预期未来会共同 KMP 开发跨平台应用,那么就需要学会不依赖 Jetpack 的开发方式,这也是本文为什么要介绍非 Jetpack 下的 MVVM 的一个初衷。
末了
==============================================================
Android进阶资料
以下的资料是比年来,我和一些朋侪面试收集整理了很多大厂的面试真题和资料,还有来自若阿里、小米、爱奇艺等一线大厂的大牛整理的架构进阶资料。希望可以资助到大家。
Android进阶焦点笔记
[外链图片转存中…(img-UvPBiDTC-1715712908808)]
百万年薪必刷面试题
[外链图片转存中…(img-wp8vrbg7-1715712908809)]
最全Android进阶学习视频
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]