前言
在不断发展的Android应用程序开发世界中,了解最新的趋势和方法是构建高质量、可维护、可扩展应用程序的关键。其中一个在最近几年中获得了显著流行的方法是将MVI (Model-View-Intent)架构模式与Clean Architecture原则相结合。
虽然很多文章介绍如何在Android中实现MVI和Clean Architecture,但是本文采用了不同的方法。
本文不是关注如何实现MVI方法,而是强调即使使用MVI和Clean Architecture也可能出现的问题。然后,根据我的个人经验和见解,提供了一个潜在的解决方案。
通过分享我的经验并提供一个潜在的解决方案,本文提供了一个独特的视角,展示了在Android开发中使用MVI和Clean Architecture所面临的挑战和好处。这对于已经熟悉这些概念并正在寻求改进实现的开发人员来说非常有价值。
问题
尽管使用Clean Architecture,但由于ViewModel的责任是管理用例、映射器和更新视图状态,因此ViewModel可能会变得复杂、庞大和难以管理。
虽然每个功能或屏幕可能有多个需要在ViewModel中处理的功能,但这可能会导致ViewModel中有太多的用例、函数、变量和映射器,它们可能不能协同工作但仍必须驻留在ViewModel中。
问题在于,ViewModel通常变成一个微观管理器,负责一切,导致代码在一个位置积累。随着使用案例和功能的数量增加,ViewModel变得更难维护和测试。
虽然使用用例确实可以简化ViewModel中的过程,但仔细检查后,您会发现ViewModel仍然有许多责任,例如将实体映射到状态模型、处理状态、处理副作用等等。
如果将这些责任乘以每个屏幕或特征中存在的功能数量,很快就会变得难以掌控和管理。
解决
为了解决这个问题,考虑将这些责任分离到它们自己的专用类中可能会有所帮助。
例如,映射器可以移到单独的类中,状态处理和副作用可以通过中间件层来管理,状态的更新可以委托给专门的类或函数。
通过遵循单一责任原则并分离这些责任,ViewModel可以变得更加简洁流畅、更易于管理。这种方法可以使代码库更加模块化,提高可维护性和可测试性。
图解
我试图创建一个清晰的图示,展示这种方法的流程。你可以按照编号的步骤来了解这个过程的展开。
与其直接连接视图和ViewModel,视图将一个Action Model发送给ViewModel。
通过将每个Action分解成它自己的ActionHandler,ViewModel作为一个终端,负责处理动作、处理器,并保存状态和效果。
下面,你可以阅读一下我在这种方法中所使用的ViewModel Scope部分的不同职责:
- 视图仅负责观察状态和效果,并将动作模型发送给ViewModel。
- ViewModel负责将接收到的动作指向其自身的ActionHandler。
- ActionHandler负责连接Usecase以检索实体并相应地更新StateMapper、Reducer或EffectHandler。
- EffectHandler负责处理副作用操作,例如导航、日志记录和分析、显示Toast消息或更新效果以在视图中呈现某些内容。
- Reducer负责单独更新新状态。
这种方法有助于保持关注点的清晰分离,并降低ViewModel、View和动作逻辑之间的耦合。
代码结构如何工作的?
利用我在下面创建的泛型,您可以实现完全基于关注点分离的解耦方法。
// Reducer.kt
fun interface Reducer<S> {
fun reduce(state: S): S
}
// ActionHandler.kt
interface ActionHandler<in A, S, E, R: Reducer<S>> {
fun handle(action: A, state: S, effect: suspend (E) -> Unit): Flow<R>
}
// EffectHandler.kt
abstract class EffectHandler<E> {
val effects = MutableSharedFlow<E>(replay = 0)
abstract suspend fun handleEffect(effect: E)
}
// BaseViewModel.kt
abstract class BaseViewModel<A : Any, S : Any, E : Any, R : Reducer<S>>(
private val effectHandler: EffectHandler<E>,
initialState: S
) : ViewModel() {
abstract fun getActionHandlers(): Set<Pair<KClass<out A>, ActionHandler<*, S, E, R>>>
private val _state = MutableStateFlow(initialState)
val state: StateFlow<S>
get() = _state
val effect: Flow<E>
get() = effectHandler.effects
@Suppress("UNCHECKED_CAST")
suspend fun action(action: A) {
(getActionHandlers()
.find { it.first == action::class }
?.second as? ActionHandler<A, S, E, R>)
?.handle(action, _state.value) { effect ->
effectHandler.handleEffect(effect)
}?.cancellable()
?.collect { reducer ->
_state.update { reducer.reduce(_state.value) }
}
}
}
要将动作连接到相应的ActionHandler,您需要提供必要的ActionHandler,并使用它们链接每个动作,如下面的示例所示:
@HiltViewModel
class HomeViewModel @Inject constructor(
homeEffectHandler: HomeEffectHandler,
private val getWeatherActionHandler: GetWeatherActionHandler,
private val getTempActionHandler: GetTempActionHandler
) : BaseViewModel<HomeAction, HomeState, HomeEffect, HomeReducer>(
homeEffectHandler,
HomeState.Default
) {
override fun getActionHandlers(): Set<Pair<KClass<out HomeAction>, ActionHandler<*, HomeState, HomeEffect, HomeReducer>>> {
return setOf(
HomeAction.LoadWeatherButtonClicked::class to getWeatherActionHandler,
HomeAction.LoadTempButtonClicked::class to getTempActionHandler
)
}
}
如果自己创建不是很清晰,我已经开发了一个小应用程序,遵循这种方法,您可以从中学习,或者克隆到自己的项目中。
总之,解耦的MVI方法为构建可维护和可扩展的Android应用程序提供了一个极好的解决方案。通过分离每个组件的责任并实现响应式编程概念,解耦的MVI方法使开发人员能够构建模块化、可测试和强大的应用程序,以适应需求和用户反馈的变化。
尽管实施解耦的MVI方法可能需要一些初步的努力,但它可以帮助简化您的代码库,并提高应用程序的整体质量和性能。
因此,无论您是经验丰富的Android开发人员还是刚开始您的旅程,解耦的MVI方法都值得探索。尝试一下,看看它如何改变您构建Android应用程序的方式吧。
个人认为 复杂的逻辑页面可以这么封装,如果是简单的页面这么封装就走了mvp的老路,添加一大堆文件 ,代码一点点,反而增加了开发的难度,结构上是保持了一致,但开发时间上升了。