0%

Android UI架构MVI

关于优化UI层的代码设计,从MVC,MVP,MVVM再到MVI,这四个模式围绕如何管理UI问题采用的都是“关注点分离”,实现细节稍有不同,但最终目的仍是解耦,提高维护性和可读性,从架构层面明确职责划分,约束开发者写出漂亮的代码,这里先回顾一下Android UI架构的演进过程与自己的一些思考并详细记录MVI的具体实现以及解决的相关痛点问题,欢迎一起学习讨论~

UI架构演进

MVC

Android的默认设计,最原始的UI架构,拆分为View,Model,Controller
View:布局xml文件
Model:管理业务数据逻辑,网络请求,数据库处理
Controller: 在Activity处理表现层逻辑

效果图

  • 存在问题
    Activity臃肿,本身不可避免的要处理UI和用户交互,又要处理表现层逻辑,职责划分不清晰,分离程度不够

MVP

为了减轻Activity的负担,进化到了MVP架构,拆分为View,Model,Presenter
View:Activity和布局xml文件
Model:管理业务数据逻辑,网络请求,数据库处理,职责不变
Presenter:分担Activity一部分工作,处理表现层逻辑,其中View和Presenter会定义协议接口Contract,约定View调Presenter发送指令的接口方法和Presenter调callback返回结果给View的接口方法

效果图

  • 存在问题
  • 协议接口Contract膨胀
    当交互负责时会定义很多的发送指令和callback回调接口方法,不好维护
  • 内存泄漏
    生命周期无法绑定,Presenter持有View层的引用,当View层被关闭销毁时Model层有耗时操作存在内存泄漏的风险,当然可以在onDestory的生命周期中释放Presenter并采用弱引用的方式,但处理起来比较繁琐
  • 双向依赖
    View层有变动时Presenter也需要做出相应的调整,在实际开发中View层是容易变动的,一定程度上影响开发效率

MVVM

为了解决以上的三个问题,进化到MVVM的架构,把Presenter改为ViewModel
View:Activity和布局xml文件
Model:管理业务数据逻辑,网络请求,数据库处理,职责不变
ViewModel:存储UI状态,处理表现层逻辑,将数据返回给观察者更新UI,这里View和Presenter的双向依赖变为View向ViewModel发指令,但ViewModel不直接回调返回结果而是通过观察者模式利用LiveData监听数据变化,返回给相应的观察者更新UI,解决了生命周期感知问题,同时也解决了手机旋转等配置变更数据丢失问题

效果图

  • 存在问题
  • 多数据流
    View与ViewModel的交互分散,不易于追踪
  • LiveData膨胀
    复杂页面交互需要定义多个LiveData,模糊了状态和事件的界限,粘性机制可能会导致一些问题,用在UI状态时如横竖屏切换时会重新执行一次observe会再次收到一个事件,对于UI来说只需关心最终状态,多次postValue可能会丢失数据也可能导致一些问题,用在事件发送时可能只执行第二个事件,对于事件来说希望每条事件都能被执行

MVI

是什么

把View和ViewModel之间的多数据流改为基于ViewState和Intent的单数据流替换LiveData
View:Activity和布局xml文件
Model:管理业务数据逻辑,网络请求,数据库处理,职责不变
Intent:操作事件,将UI层的操作事件和携带的数据传到Model
ViewState:数据类,包含页面状态和对应的数据
ViewModel:存储UI状态,处理表现层逻辑,通过ViewState设置给UI

效果图

解决痛点

  • 单一数据流便于追踪问题
  • 清晰的划分状态和事件的界限,避免LiveData滥用导致的问题

    MVI封装流程

声明State和Intent接口

所有的业务层UiState都需要实现IState接口,UiIntent都需要实现IIntent接口

1
2
3
interface IState

interface IIntent

声明业务层ViewModel基类接口

业务层ViewModel基类需要初始化stateFlow和intentFlow

1
2
3
4
interface IStateViewModel<I: IIntent, S: IState> {
val stateFlow: Flow<S>
val intentFlow: Flow<I>
}

封装View层生命周期事件

初始化默认为IDLE空闲状态

1
2
3
4
5
6
enum class WidgetLifeEvent {
IDLE, // 空闲
ON_SHOW, // 显示
ON_HIDE, // 隐藏
ON_DESTROY // 销毁
}

封装观察者接口

观察者可通过回调监测到生命周期变化

1
2
3
4
5
interface WidgetLifecycleObserver {
fun onShow()
fun onHide()
fun onDestroy()
}

生命周期观察者管理类

  • 生命周期观察者被存储到一个列表中,当前的生命周期默认为IDLE
  • onShow/onHide/onDestroy事件会更新当前的生命周期状态,遍历所有的观察者调对应的接口方法
  • onDestroy时会清空列表所有的观察者
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    class WidgetLifecycle {
    var currentWidgetLifeEvent: WidgetLifeEvent = WidgetLifeEvent.IDLE // 当前的生命周期

    private val widgetLifeList = mutableListOf<WidgetLifecycleObserver>()

    /**
    * 添加生命周期观察者
    */
    fun addWidgetLife(widgetLifecycleObserver: WidgetLifecycleObserver){
    synchronized(widgetLifeList){
    if (widgetLifecycleObserver !in widgetLifeList){
    widgetLifeList.add(widgetLifecycleObserver)
    }
    }
    }

    /**
    * 移除生命周期观察者
    */
    fun removeWidgetLife(widgetLifecycleObserver: WidgetLifecycleObserver){
    synchronized(widgetLifeList){
    widgetLifeList.remove(widgetLifecycleObserver)
    }
    }

    /**
    * 调所有观察者的onShow
    */
    fun onShow(){
    synchronized(widgetLifeList){
    currentWidgetLifeEvent = WidgetLifeEvent.ON_SHOW
    widgetLifeList.onEach {
    kotlin.runCatching {
    it.onShow()
    }.onFailure {
    "WidgetLifecycleObserver onShow异常".logD()
    }
    }
    }
    }

    /**
    * 调所有观察者的onHide
    */
    fun onHide(){
    synchronized(widgetLifeList){
    currentWidgetLifeEvent = WidgetLifeEvent.ON_HIDE
    widgetLifeList.onEach {
    kotlin.runCatching {
    it.onHide()
    }.onFailure {
    "WidgetLifecycleObserver onHide异常".logD()
    }
    }
    }
    }

    /**
    * 调所有观察者的onDestory
    */
    fun onDestroy(){
    synchronized(widgetLifeList){
    currentWidgetLifeEvent = WidgetLifeEvent.ON_DESTROY
    widgetLifeList.forEach {
    kotlin.runCatching {
    it.onDestroy()
    }.onFailure {
    "WidgetLifecycleObserver onDestroy异常".logD()
    }
    }
    widgetLifeList.clear()
    "widgetLifeList clear".logD()
    }
    }
    }

    暴露生命周期观察者管理对象

    实现了WidgetLifecycleOwner接口的类会提供命周期观察者管理对象
    1
    2
    3
    interface WidgetLifecycleOwner {
    fun getWidgetLifecycle(): WidgetLifecycle
    }

    封装BaseViewModel

    继承ViewModel的同时实现了WidgetLifecycleOwner接口,提供生命周期观察者管理对象,BaseStateFragment通过channel分发生命周期变化事件给BaseViewModel,BaseViewModel再把对应的事件分发给所有的观察者
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    abstract class BaseViewModel : ViewModel(), WidgetLifecycleOwner{
    private val widgetLifecycle = WidgetLifecycle() // 生命周期观察者管理对象
    private val lifeEventChannel: Channel<WidgetLifeEvent> = Channel()
    private val lifecycleFlow: Flow<WidgetLifeEvent> = lifeEventChannel.consumeAsFlow()

    override fun getWidgetLifecycle(): WidgetLifecycle {
    return widgetLifecycle
    }

    protected fun launch(callback: suspend () -> Unit){
    viewModelScope.launch {
    callback.invoke()
    }
    }

    init {
    viewModelScope.launch {
    lifecycleFlow.collect{
    if (it == WidgetLifeEvent.ON_SHOW){
    onShow()
    }
    if (it == WidgetLifeEvent.ON_HIDE){
    onHide()
    }
    }
    }
    }

    /**
    * 发送生命周期事件
    */
    fun sendLifeEvent(widgetLifeEvent: WidgetLifeEvent){
    launch {
    lifeEventChannel.send(widgetLifeEvent)
    }
    }

    open fun onShow(){
    widgetLifecycle.onShow()
    "onShow".logD()
    }

    open fun onHide(){
    widgetLifecycle.onHide()
    "onHide".logD()
    }

    override fun onCleared() {
    super.onCleared()
    widgetLifecycle.onDestroy()
    "onCleared".logD()
    }
    }

    封装加载视图和toast事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    sealed class SingleEvent {
    companion object {
    const val LOADING_TYPE_DIALOG = 1
    const val LOADING_TYPE_VIEW = 2
    }

    object HideLoadingDialog: SingleEvent()
    object ShowLoadingDialog: SingleEvent()
    object ShowLoadingView: SingleEvent()
    object ShowContentView: SingleEvent()
    object HideSwipeLayout: SingleEvent()

    data class ShowToast(val msg: String): SingleEvent()
    }

    封装BaseStateViewModel

  • 继承BaseViewModel,实现IStateViewModel接口
  • 需要传入意图和视图状态的具体类型
  • 初始化intentFlow和stateFlow,intentFlow通过channel转换实现,stateFlow需要子类返回具体状态类,初始化状态对象再初始化StateFlow实现
  • 加载视图和toast事件通过singleEventChannel发送,在BaseStateFragment中绑定事件并处理视图展示逻辑
  • 更新State时刷新stateFlow的value,比较当前stateFlow的value是否和需要更新的State一致,如果一致则不会发送事件,因为StateFlow具有数据防抖功能
  • 根据ViewModel的scope封装子线程切换和flow的子线程切换方法
  • 声明dispatchIntentOnIO抽象方法,在子线程中分发意图事件并处理,不阻塞主线程,避免UI卡顿
  • 封装加载状态展示和自动隐藏的方法,通过singleEventChannel发送相应的加载视图事件
  • 封装checkIntent优化switch判断类型逻辑便于链式回调
  • 初始化代码块中绑定intentFlow切换到子线程分发事件
  • dispatchIntentOnIO中的异常在needCatchException为true且release包下会被catch住
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    abstract class BaseStateViewModel<I: IIntent, S: IState>: BaseViewModel(), IStateViewModel<I,S> {
    // Channel发送Intent
    private val intentChannel = Channel<I> (Channel.UNLIMITED)
    final override val intentFlow: Flow<I>
    get() = intentChannel.consumeAsFlow()

    protected abstract val stateClass: KClass<S> // 状态类

    open val needCatchException = false // 是否需要try/catch

    // stateflow发送state
    private val initState = createInitState() // 状态对象初始化

    private val mutableStatedFlow = MutableStateFlow(initState) // stateFlow初始化

    /**
    * 可变stateflow转成只读流
    */
    override val stateFlow: Flow<S>
    get() = mutableStatedFlow.asStateFlow()

    private val singleEventChannel = Channel<SingleEvent>()

    protected val tag: String get() = javaClass.name

    /**
    * SingleEvent flow
    */
    val singleEvent get() = singleEventChannel.consumeAsFlow()

    /**
    * 发送intent
    */
    fun sendIntent(intent: I){
    viewModelScope.launch {
    intentChannel.send(intent)
    }
    }

    /**
    * 获取当前state对象
    */
    val state: S get() = mutableStatedFlow.value

    @Synchronized
    private fun emitState(state: S){
    (if (state == this.state) "界面不会刷新" else "界面【会刷新】").apply {
    "$tag -> 发送状态 -> ${state.javaClass.name} $this".logD()
    if (isDebug()){
    "$tag->发送状态数据->$this 旧数据:${this@BaseStateViewModel.state} 新数据:${state}".logD()
    }
    }
    mutableStatedFlow.value = state
    }

    /**
    * 更新state
    */
    protected fun updateState(reducer: S.() -> S){
    emitState(state.reducer())
    }

    /**
    * 开启子线程执行
    */
    protected fun launchOnIO(callback: suspend () -> Unit): Job{
    return viewModelScope.launch(Dispatchers.IO) { callback.invoke() }
    }

    /**
    * flow切换子线程执行
    */
    private fun <T> Flow<T>.collectOnIO(block: suspend CoroutineScope.(T) -> Unit){
    viewModelScope.launch(Dispatchers.IO){
    this@collectOnIO.onEach {
    block.invoke(this, it)
    }.launchIn(this)
    }
    }

    /**
    * 在IO协程分发Intent,这个方法只会在IO协程里面执行
    * 非阻塞的,不影响其他intent
    */
    protected abstract suspend fun dispatchIntentOnIO(intent: I)


    /**
    * 初始化默认状态,子类可以实现自己的默认状态,比如读取缓存之类的
    */
    protected open fun createInitState(): S{
    return stateClass.createInstance()
    }

    /**
    * 发送一个单一事件
    */
    protected fun sendSingleEvent(singleEvent: SingleEvent){
    launch {
    singleEventChannel.send(singleEvent)
    }
    }

    protected fun showLoadingDialog(){
    sendSingleEvent(SingleEvent.ShowLoadingDialog)
    }

    protected fun hideLoadingDialog(){
    sendSingleEvent(SingleEvent.HideLoadingDialog)
    }

    protected fun showLoadingView(){
    sendSingleEvent(SingleEvent.ShowLoadingView)
    }

    protected fun showContentView(){
    sendSingleEvent(SingleEvent.ShowContentView)
    }

    protected fun showToast(msg: String){
    sendSingleEvent(SingleEvent.ShowToast(msg))
    }

    /**
    * 显示加载状态
    * @param type 加载状态类型 对话框 加载View
    * @param needAutoHide 是否需要自动触发隐藏
    */
    protected suspend fun <T> Flow<T>.startLoading(
    type: Int = SingleEvent.LOADING_TYPE_DIALOG,
    needAutoHide: Boolean = true
    ): Flow<T>{
    return this.onStart {
    if (type == SingleEvent.LOADING_TYPE_DIALOG){
    singleEventChannel.send(SingleEvent.ShowLoadingDialog)
    } else {
    singleEventChannel.send(SingleEvent.ShowLoadingView)
    }
    }.onCompletion {
    if (needAutoHide){
    if (type == SingleEvent.LOADING_TYPE_DIALOG){
    singleEventChannel.send(SingleEvent.HideLoadingDialog)
    } else {
    singleEventChannel.send(SingleEvent.ShowContentView)
    }
    }
    }
    }

    /**
    * 加载对话框
    */
    protected suspend fun <T> Flow<T>.startLoadingDialog(): Flow<T> {
    return startLoading(SingleEvent.LOADING_TYPE_DIALOG)
    }

    protected suspend fun <T> Flow<T>.startLoadingView(): Flow<T>{
    return startLoading(SingleEvent.LOADING_TYPE_VIEW)
    }

    protected suspend fun <T> Flow<T>.startLoadingViewWithCondition(
    needShowLoadingView: Boolean = true
    ): Flow<T> {
    return if (needShowLoadingView) startLoading(SingleEvent.LOADING_TYPE_VIEW) else this
    }

    protected suspend fun <T> Flow<T>.startLoadingByCondition(
    needShowLoading: Boolean = true,
    type: Int = SingleEvent.LOADING_TYPE_VIEW
    ): Flow<T> {
    return if (needShowLoading) startLoading(type) else this
    }

    /**
    * 判断Intent类型,如果是该类型就回调
    *
    * @param T
    * @param block
    * @receiver
    */
    protected inline fun <reified T: IIntent> IIntent.checkIntent(
    block: (T) -> Unit
    ): IIntent{
    if (this is T){
    block.invoke(this)
    }
    return this
    }



    init {
    collectIntent()
    }

    /**
    * 异步订阅Intent,所有的intent的执行都是在IO协程里面执行
    *
    */
    private fun collectIntent() {
    intentFlow.collectOnIO {
    val intent = it
    val intentName = intent.javaClass.name
    "$tag->分发Intent->$intentName".logD()
    "$tag->分发Intent详细数据->$it".logD()

    callbackByCondition {
    launch {
    dispatchIntentOnIO(it)
    "异步分发Intent完成->$intentName".logD()
    }
    }
    }
    }

    protected open fun isRelease(): Boolean{
    return !BuildConfig.DEBUG
    }

    protected open fun isDebug(): Boolean {
    return BuildConfig.DEBUG
    }

    /**
    *
    * release环境下,needCatchException=true会捕获所有intent异常
    * @param block
    * @receiver
    */
    private suspend fun callbackByCondition(block: suspend () -> Unit) {
    if (needCatchException && isRelease()) {
    kotlin.runCatching {
    block.invoke()
    }.onFailure {
    "callbackByCondition异常".logD()
    }
    } else {
    block.invoke()
    }
    }
    }

    封装BaseFragment

  • 对加载视图进行封装,把包含layout的content视图包裹在加载视图中
  • 提供显示空视图,错误视图,内容视图,弹出toast,展示加载弹窗和隐藏加载弹窗的方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    abstract class BaseFragment : Fragment() {
    private var loadingView: WrapperLoadingView? = null
    private var loadingDialogFragment: LoadingDialogFragment? = null
    open var needLoadingView = true

    /**
    * 获取layout资源
    *
    * @return
    */
    @LayoutRes
    protected abstract fun getLayout(): Int


    /**
    * 初始化加载弹窗和加载视图
    */
    final override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
    ): View? {
    if (loadingDialogFragment == null) {
    loadingDialogFragment = LoadingDialogFragment()
    }
    return if (needLoadingView) {
    if (loadingView == null) {
    loadingView = WrapperLoadingView(
    inflater.context,
    inflater.inflate(getLayout(), container, false)
    )
    } else {
    (loadingView?.parent as? ViewGroup)?.removeView(loadingView)
    }
    loadingView
    } else {
    inflater.inflate(getLayout(), container, false)
    }
    }

    fun showContentView(){
    loadingView?.showContent()
    }

    fun showLoadingView(@ColorInt color: Int = Color.TRANSPARENT){
    loadingView?.showLoading(Gravity.CENTER, color)
    }

    fun showLoadingDialog(){
    loadingDialogFragment?.show(childFragmentManager,"loading_dialog")
    }

    fun hideLoadingDialog(){
    loadingDialogFragment?.dismiss()
    }

    fun showEmptyView(
    tips: Int = R.string.no_more_data,
    icon: Int = R.mipmap.icon_empty_default,
    click: View.OnClickListener? = null
    ) {
    loadingView?.showEmpty(tips, icon, click)
    }

    fun showEmptyView(
    tips: String,
    icon: Int = R.mipmap.icon_empty_default,
    click: View.OnClickListener? = null
    ) {
    loadingView?.showEmpty(tips, icon, click)
    }

    fun showFailView(
    tips: Int = R.string.load_failed_click_retry,
    icon: Int = R.mipmap.icon_error_retry,
    click: View.OnClickListener? = null
    ) {
    loadingView?.showEmpty(tips, icon, click)
    }

    fun showFailView(
    tips: String,
    icon: Int = R.mipmap.icon_empty_default,
    click: View.OnClickListener? = null
    ) {
    loadingView?.showEmpty(tips, icon, click)
    }

    fun showErrorView(tips: String, click: View.OnClickListener? = null) {
    loadingView?.showError(tips, click)
    }

    fun showErrorView(tips: String, color: Int, click: View.OnClickListener? = null) {
    loadingView?.showError(tips, color, click)
    }

    fun showToast(errorMsg: String?) {
    ToastUtils.showShort(errorMsg)
    }

    fun getViewProvider(): View {
    return if (needLoadingView){
    (view as WrapperLoadingView).contentView
    } else requireView()
    }
    }

    封装BaseStateFragment

  • 继承BaseFragment,子类需要传入具体的意图类型,视图状态类型,绑定的ViewModel的类型
  • 懒初始化ViewModelProvider的Factory,暴露出方法给子类重写默认返回空,暴露出方法给子类返回具体的ViewModel的Class对象
  • 暴露绑定state和初始化View的方法,在onViewCreated中调用
  • 绑定singleEventChannel并处理对应的UI事件
  • 在onResume和onPause中发送对应的生命周期事件给生命周期观察者
  • stateFlow绑定具体的state属性,在生命周期started后回调变更后的state,distinctUntilChanged避免多次不必要的刷新
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    abstract class BaseStateFragment<I : IIntent, S : IState, VM : BaseStateViewModel<I, S>> :
    BaseFragment() {
    private val vmFactory: ViewModelProvider.Factory? by lazy { getViewModelFactory() }
    protected abstract val viewModelClass: KClass<VM>

    /**
    * 如果ViewModelProvider.Factory不为空则用它初始化否则直接初始化
    */
    private val viewModel: VM by lazy {
    vmFactory?.run {
    ViewModelProvider(this@BaseStateFragment, this)[viewModelClass.java]
    } ?: ViewModelProvider(this@BaseStateFragment)[viewModelClass.java]
    }

    /**
    * ViewModelProvider.Factory,如果需要可以自行实现
    * @return
    */
    protected open fun getViewModelFactory(): ViewModelProvider.Factory? {
    return null
    }

    /**
    * fragment布局创建完成
    * 绑定state
    * 绑定SingleEvent并处理
    * 初始化View
    */
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    bindState()
    handleSingleEvent(this)
    initView()
    }

    protected open fun bindState() {}

    protected open fun initView() {}

    override fun onResume() {
    super.onResume()
    viewModel.sendLifeEvent(WidgetLifeEvent.ON_SHOW)
    }

    override fun onPause() {
    super.onPause()
    viewModel.sendLifeEvent(WidgetLifeEvent.ON_HIDE)
    }

    /**
    * 分发singleEvent
    */
    protected open fun dispatchSingleEvent(singleEvent: SingleEvent) {}

    /**
    * 绑定singleEvent并处理
    */
    private fun handleSingleEvent(lifecycleOwner: LifecycleOwner){
    viewModel.singleEvent.collectWhenStart(lifecycleOwner){
    "handleSingleEvent->$it".logD()
    when(it) {
    is SingleEvent.HideLoadingDialog -> {
    hideMviLoadingDialog()
    }
    is SingleEvent.ShowLoadingDialog -> {
    showMviLoadingDialog()
    }
    is SingleEvent.ShowContentView -> {
    showMviContentView()
    }
    is SingleEvent.ShowLoadingView -> {
    showMviLoadingView()
    }
    is SingleEvent.ShowToast -> {
    showMviToast(it.msg)
    }
    else -> {
    dispatchSingleEvent(it)
    }
    }
    }
    }


    private fun showMviContentView(){
    "showMviContentView".logD()
    showContentView()
    }

    private fun showMviLoadingView() {
    "showMviLoadingView".logD()
    showLoadingView()
    }

    private fun showMviLoadingDialog(){
    "showMviLoadingDialog".logD()
    showLoadingDialog()
    }

    private fun hideMviLoadingDialog(){
    "hideMviLoadingDialog".logD()
    hideLoadingDialog()
    }

    private fun showMviToast(toast: String){
    "showMviToast:$toast".logD()
    showToast(toast)
    }

    /**
    * state刷新绑定
    */
    protected fun <A> bindPropertyState(kp: KProperty1<S, A>, action: suspend (A) -> Unit){
    viewModel.stateFlow.map {
    data -> kp.get(data)
    }.distinctUntilChanged()
    .collectWhenStart(this) { a ->
    "${this@BaseStateFragment} -> UI收到局部状态 -> ${kp.getter.property}".logD()
    action.invoke(a)
    }
    }


    /**
    * state绑定扩展
    */
    protected fun <A> KProperty1<S, A>.bindState(action: suspend (A) -> Unit) {
    bindPropertyState(this, action)
    }


    /**
    * 返回viewModel的state对象
    */
    protected fun getState(): S{
    return viewModel.state
    }

    /**
    * intent发送扩展
    */
    protected fun <T: I> T.sendToIntent() {
    viewModel.sendIntent(this)
    }

    }

在业务场景中的使用

定义登录接口

  • 这里是Flow + retrofit的风格
    1
    2
    3
    4
    5
    6
    7
    8
    9
    interface LoginApi {

    @POST("/user/login")
    @FormUrlEncoded
    fun realLogin(
    @Field("username") code: String,
    @Field("password") token: String,
    ): Flow<LoginEntity?>
    }

    登录Contract

  • 这里定义UiIntent和UIState
  • UiIntent是一个密封类,每一个Intent都是内部的一个类,内部所有的类均继承UiIntent
  • UiIntent是一个数据类,实现IState接口,每一个State都是内部的一个属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    interface LoginContract {
    sealed class UiIntent: IIntent {
    class DealLogin(val username: String, val password: String): UiIntent()
    class CheckEnable(val username: String, val password: String): UiIntent()
    }

    data class UIState(val loginEntity: LoginEntity? = null,
    val enable: Boolean? = null): IState
    }

    登录接口数据Bean类

  • 根据后端返回的数据结构封装Bean
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    data class LoginEntity(
    val admin: Boolean? = null,
    val chapterTops: List<Any>? = null,
    val coinCount: Int? = null,
    val collectIds: List<Int>? = null,
    val email: String? = null,
    val icon: String? = null,
    val id: Int? = null,
    val nickname: String? = null,
    val password: String? = null,
    val publicName: String? = null,
    val token: String? = null,
    val type: Int? = null,
    val username: String? = null
    )

    登录ViewModel

  • 扩展ViewModel可接收初始化参数,传入SavedStateHandle
  • 重写stateClass返回LoginContract的UIState的class对象
  • 懒加载初始化FlowApi,关于FlowApi的封装在后面的文章中会详细讨论
  • dispatchIntentOnIO中根据当前的Intent类型去执行对应的表现层逻辑,这里值得注意的是由于是子线程执行,需要在设计时考虑线程安全的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class LoginViewModel(savedStateHandle: SavedStateHandle): BaseStateViewModel<LoginContract.UiIntent, LoginContract.UIState>() {

override val stateClass: KClass<LoginContract.UIState>
get() = LoginContract.UIState::class

private val loginApi by lazy {
LoginApi::class.java.createFlowApi()
}

override suspend fun dispatchIntentOnIO(intent: LoginContract.UiIntent) {
intent.checkIntent<LoginContract.UiIntent.CheckEnable> {
handleCheckEnable(it.username, it.password)
}.checkIntent<LoginContract.UiIntent.DealLogin> {
handleDealLogin(it.username, it.password)
}
}

private fun handleCheckEnable(username: String, password: String) {
updateState {
copy(enable = username.isNotEmpty() && password.isNotEmpty())
}
}

/**
* 处理登录请求
*/
private suspend fun handleDealLogin(username: String, password: String) {
loginApi.realLogin(username, password).startLoading()
.onSuccess {
updateState { copy(loginEntity = it,) }
}
.onFail {
Log.d("loginApi", it.message.toString())
}
}

}

登录界面

  • 这里就不放xml文件了,主要是展示Fragment和ViewModel之间的交互过程,ViewBinding的封装涉及到了属性代理,在后面的文章中会详细讨论
  • 重写getViewModelFactory方法,返回SavedStateViewModelFactory,可传递参数到ViewModel初始化
  • initView中CheckEnable的逻辑和登录逻辑在ViewModel中实现,通过发送Intent事件到ViewModel走交互逻辑
  • bindState中监听视图状态数据enable获取CheckEnable的结果刷新UI,监听loginEntity获取登录逻辑的数据结果反馈到UI
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    class LoginFragment :
    BaseStateFragment<LoginContract.UiIntent, LoginContract.UIState, LoginViewModel>() {
    override val viewModelClass: KClass<LoginViewModel>
    get() = LoginViewModel::class

    private val viewBinding by viewBinding1(
    FragmentLoginBinding::bind,
    viewProvider = {
    getViewProvider()
    },
    onViewDestroyed = { _: FragmentLoginBinding ->
    // reset view
    })

    override fun getLayout(): Int {
    return R.layout.fragment_login
    }

    override fun getViewModelFactory(): ViewModelProvider.Factory {
    return SavedStateViewModelFactory(
    BaseApplication.get(), this,
    arguments)
    }

    override fun initView() {
    super.initView()
    viewBinding.apply {
    etAccount.doAfterTextChanged {
    LoginContract.UiIntent.CheckEnable(it.toString(),
    viewBinding.passwordToggleView.editText.text.toString()).sendToIntent()
    }
    passwordToggleView.editText.doAfterTextChanged {
    LoginContract.UiIntent.CheckEnable(viewBinding.etAccount.text.toString(),
    it.toString())
    .sendToIntent()
    }
    tvLogin.clickWithTrigger {
    val username = viewBinding.etAccount.text.toString()
    val password = viewBinding.passwordToggleView.editText.text.toString()

    LoginContract.UiIntent.DealLogin(username, password).sendToIntent()
    }
    }
    }


    override fun bindState() {
    super.bindState()
    LoginContract.UIState::loginEntity.bindState {
    it?.let {
    Log.d(TAG, GsonUtils.toJson(it))
    ToastUtils.showShort("登录成功")
    }
    }
    LoginContract.UIState::enable.bindState {
    it?.let {
    viewBinding.tvLogin.isEnabled = it
    }
    }
    }
    }