Android的MVI架构最佳实践(一):Model和Intent封装
预告:
- Android的MVI架构最佳实践(二):View封装和repeatOnLifecycle
- Android的MVI架构最佳实践(三):Compose封装
- Android的MVI架构最佳实践(四):单元测验
- Android的MVI架构最佳实践(四):UI测验
前语
在此篇中咱们会简单介绍MVI的规划思维,并基于Android Jetpack Components
完成能够用于Activity、Fragment、Compose的MVI的架构规划。主旨在简化许多的MVI模版代码,提高开发效率和一致代码结构,并且会供给单元测验和UI测验攻略,可帮助咱们更好地安排代码以创建健壮且可维护的应用程序。你需求储备的知识点androidx.lifecycle和kotlin-coroutines
,以及Flow和Channel
。
MVI简介
与MVC,MVP或MVVM相同,MVI是一种体系结构规划形式,与Flux或Redux属于同一宗族。提倡一种单向可信任数据流的规划思维,十分适合数据驱动型的UI展现项目。当然MVI也有许多缺陷,能够在其他博客中了解。因为MVI和声明式UI是绝配,所以在Android的Compose中将很有远景,咱们必需求掌握。
MVI即“模型”(Model),“视图”(View)和“目的”(Intent)单词词缩写而成:
- Model: 与其他MVVM中的Model不同的是,MVI的Model主要指UI状况(State)。当时界面展现的内容无非就是UI状况的一个快照:例如数据加载进程、控件方位等都是一种UI状况
- View: 与其他MVX中的View一致,可能是一个Activity、Fragment或许恣意UI承载单元。MVI中的View经过订阅State的改变完成界面刷新
- Intent: 此Intent不是Activity的Intent,用户的任何操作都被包装成UserIntent后发送给Model进行数据请求
MVI的Model和Intent封装
从android单向数据流(UDF)界面层攻略图中能够看到,为了一致办理这个单线的数据流咱们运用ViewModel
来作为封装容器和UI交互。UI上的一些点击或许用户事情,都会封装成events,发送给ViewModel,再由ViewModel
转化data为UI state
传递给UI。
M
、I
的结构
为了防止从字面上混杂Model和Intent的概念,咱们在这里给他们分别起别名用于区别。Action代Intent或许图中的events
。Model(State)
一分为二:UIState一般是一种持久的UI形状,在发生生命周期改变时分需求回放。UIEffect一般是一次性消费UI事情,如弹窗、toast、导航等。所以咱们拆分Model为State和Effect
。
/** 用户与ui的交互事情*/
interface Action
/** ui响应的状况*/
interface State
/** ui响应的事情*/
interface Effect
ViewModel中M
、I
的办理
Model(State)
State需求订阅观察者形式给view供给数据,在非Compose中咱们能够运用LiveData和StateFlow
, 在Compose中咱们能够直接运用State
。为了兼容性咱们挑选StateFlow或许自界说SharedFlow
。
abstract class BaseViewModel<S : State> : ViewModel() {
/**承继BaseViewModel需求完成state默许值*/
abstract fun initialState(): S
private val _state by lazy {
MutableStateFlow(value = initialState())
}
/**在view中用于订阅*/
val state: StateFlow<S> by lazy { _state.asStateFlow() }
protected fun emitState(builder: suspend () -> S?) = viewModelScope.launch {
builder()?.let { _state.emit(it) }
}
/**suspend 函数在flow或许scope中emit状况*/
protected suspend fun emitState(state: S) = _state.emit(state)
}
假如咱们想运用livedata
相同不需求默许值,咱们能够自界说SharedFlow能够完成相同的效果,但是因为SharedFlow默许是不防抖的,所以咱们要凭借函数kotlinx.coroutines.flow.distinctUntilChanged()
,最终完成如下,后续代码展现中咱们运用这种:
private val _state = MutableSharedFlow<S>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val state: Flow<S> by lazy { _state.distinctUntilChanged() }
Model(Effect)
Effect 指Android中的一次性事情,比如toast、navigation、backpress、click等等,因为这些状况都是一次性的消费所以不能运用livedata和StateFlow,咱们能够运用SharedFlow或许Channel,考虑多个Composable中要共享viewmodel获取sideEffect,这里运用SharedFlow更方便。
abstract class BaseViewModel<S : State, E : Effect> : ViewModel() {
.... state 代码
/**
* [effect]事情带来的副效果,通常是一次性事情 例如:弹Toast、导航Fragment等
*/
private val _effect = MutableSharedFlow<E>()
val effect: SharedFlow<E> by lazy { _effect.asSharedFlow() }
protected fun emitEffect(builder: suspend () -> E?) = viewModelScope.launch {
builder()?.let { _effect.emit(it) }
}
protected suspend fun emitEffect(effect: E) = _effect.emit(effect)
}
UserIntent(Action)
action用于描述各种请求State或许Effect的动作,由View发送ViewModel订阅消费,典型的生产者消费者形式,考虑是1对1的联系咱们运用Channel来完成,有些开发者喜欢直接调用ViewModel方法,假如方法还有回来值,就破坏了数据的单向流动。
abstract class BaseViewModel<A : Action, S : State, E : Effect> : ViewModel() {
private val _action = Channel<A>()
init {
viewModelScope.launch {
_action.consumeAsFlow().collect {
/*replayState:许多时分咱们需求经过上个state的数据来处理这次数据,所以咱们要获取当时状况传递*/
onAction(it, replayState)
}
}
}
/** [actor] 用于在非viewModelScope外运用*/
val actor: SendChannel<A> by lazy { _action }
fun sendAction(action: A) = viewModelScope.launch {
_action.send(action)
}
/** 订阅事情的传入 onAction()分发处理事情 */
protected abstract fun onAction(action: A, currentState: S?)
....上面完成的代码部分
}
Sample
需求: 登录页面点击登录按钮,请求网络回来登录结果,登录成功跳转,登录失利展现过错页面。 action: 登录按钮点击OnLogonClicked;
sealed class LogonAction : Action {
object OnLogOnClicked : LogonAction()
}
state: 登录中Loading, 失利过错页面 Error
sealed class LogonState : State {
object Loading : LogOnState()
data class Error(val ex: Throwable) : LogonState()
}
effect: 登录成功跳转页面NavigationToHost
sealed class LogonEffect : Effect {
data class NavigationToHost(val response: Int) : LogonEffect()
}
ViewModel在onAction中分发处理 action, repository获取数据。
class LogonViewModel(
private val repo: LogonRepo
) : BaseViewModel<LogonAction, LogonState, LogonEvent>() {
override fun onAction(action: LogonAction, currentState: LogonState?) {
when (action) {
LogonAction.OnLogonClicked -> logon()
else ->{}
}
}
private fun logon() {
repo.fetchLogon()
.onStart {
emitState(LogonState.Loading)
}.catch { ex ->
emitState(LogonState.Error(ex))
}.onEach { result ->
emitEffect(LogonEvent.NavigationToHost(result))
}.launchIn(viewModelScope)
}
}
object LogonRepo {
fun fetchLogon() = flow {
delay(2500)
emit(Random.nextInt(3))
}.flowOn(Dispatchers.IO)
}
总结和补充
- 界说的Action,State,Effect中需求数据传递的,建议运用data class,而且的一切字段必须是val的,因为MVI需求单一的可信任的数据源。假如要对特点进行修正,能够运用copy函数。
- 因为一切的UI state都需求在viewModle中emit一个State目标会耗费资源,假如遇到频频修正某个UI组件的需求,应该独自界说一个数据流给它独自运用,避免呈现频频GC内存抖动和卡顿问题。
- 因为state每次的改变都新创建目标,不要直接把repository的杂乱的数据回来给view层处理,在这种情况咱们一般对View所需的状况进行抽象一个View的model, 在ViewModel中经过reducer来做mapping,reducer的效果专门用来把remote或许local的data转化为state或许sideEffect。这样在单元测验时分能够独自对reducer和ViewModel覆盖。当逻辑改变时分独自修正reducer即可满足要求,相同的数据核算逻辑也能够进行复用reducer。
- 项目很杂乱则需求对模块的功用愈加单一,例如ViewModel只是负责办理,内部不包括任何判断和核算逻辑,运用reducer来mapping, CoroutineDispatcherProvider来办理线程切换,并运用hilt或许koin来辅佐解耦。
jetpack结合MVI的规划思维很简单完成MVI的建议框架,在后续的coding中还是有许多细节要坚持。下一篇中咱们会封装Activity和Fragment来支撑MVI中的View层,简化View中的模版代码并且处理flow替换livedata的生命周期感知问题。