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中将很有远景,咱们必需求掌握。

Android的MVI架构最佳实践(一):Model和Intent封装

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的MVI架构最佳实践(一):Model和Intent封装

从android单向数据流(UDF)界面层攻略图中能够看到,为了一致办理这个单线的数据流咱们运用ViewModel来作为封装容器和UI交互。UI上的一些点击或许用户事情,都会封装成events,发送给ViewModel,再由ViewModel转化data为UI state传递给UI。

MI的结构

为了防止从字面上混杂Model和Intent的概念,咱们在这里给他们分别起别名用于区别。ActionIntent或许图中的eventsModel(State)一分为二:UIState一般是一种持久的UI形状,在发生生命周期改变时分需求回放。UIEffect一般是一次性消费UI事情,如弹窗、toast、导航等。所以咱们拆分Model为State和Effect

/** 用户与ui的交互事情*/
interface Action
/** ui响应的状况*/
interface State
/** ui响应的事情*/
interface Effect

ViewModel中MI的办理

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的生命周期感知问题。