尘埃落地 😛 遍历全网Android-MVI架构,学习总结一波

从简单到复杂,MVI 架构定义与封装使用总结

前言

时间回到一年多前讨论度很高的 MVI 架构,现在也已尘埃落地,没有什么争议并各自都有自己的一套实现方案了,接下来我们就看看这些网上各种各样的 MVI 架构是如何从简单到复杂,从 Java 到 Kotlin 到协程再到 Compose 的各个场景的应用。

首先你能点进来看到这篇文章,说明你或多或少都对 MVI 架构有些了解,我这里就不贴一些重复的图去解释 MVI 架构原理流程什么的了。

你只需要知道,不管怎么它怎么变化,应用到哪一种语言,哪一种架构,其本质就是 MVVM 架构,它来自 MVVM 架构又扩展自 MVVM 架构。

由于 MVVM 架构的状态与行为的管理混乱导致多人协同开发过程或者历史问题或架构问题导致直接操作 ViewModel 中的数据源导致可能的数据混乱。

所以 MVI 架构本质就是在 MVVM 架构的基础上进行了行为和数据上的 约束,把数据流变成了单向流动,把状态集中管理形成唯一可信数据源。

从谷歌的安卓应用架构指南中可以看出谷歌已经推荐开发者使用 MVI 架构开发应用了。

(PS:谷歌要淘汰 LiveData ???)

更多的代码可以去看源码,不贴图了。

当然除了谷歌推荐的 MVI 架构还有一些开源的一站式方案的如 Mosby MVI 库,如mavericks MVI 库配合 epoxy 架构快速实现页面。等等不少的一站式 MVI 组合式架构。这些就不在本文的探讨范围了!

接下来我们还是说回正文,看看目前主流的 MVI 架构从易到难都做了哪些进化?

一、不使用 Kotlin 协程行不行?

现在的文章看都看不懂,都是 Kotlin 语言和协程Flow,不会 Kotlin 就不能开发 Android 了吗?我们项目还是 Java 项目,难道我们就不配使用 MVI 了吗?

当然不是!当然可以用!MVI 只是架构层面,Java 语言虽然做起来麻烦一点但也能实现,虽然目前主流都是使用协程Flow 来驱动数据,但是 LiveData 通过一番自定义之后也能实现类似的逻辑。

(PS:现在还不会 Kotlin 语言的话虽说可以开发 Android 但是真的是越来越难了,就算不会写至少也希望大家能读懂吧!)

一般来说 MVI 我们都会定义 Intent 与 State 。

Intent 一般我们都是密封类:

kotlin 复制代码
    sealed class DemoIntent {
        object RequestIndustry : DemoIntent()
        object RequestSchool : DemoIntent()
        object RequestAllData : DemoIntent()

        data class UpdateChanged(val isChange: Boolean) : DemoIntent()
    }

如果 Java 语言没有密封类,也可用抽象类写,例如:

scala 复制代码
public abstract class DemoIntent {
    private DemoIntent() {}

    public static final class RequestIndustry extends DemoIntent {
        public RequestIndustry() {}
    }

    public static final class RequestSchool extends DemoIntent {
        public RequestSchool() {}
    }

    public static final class RequestAllData extends DemoIntent {
        public RequestAllData() {}
    }

    public static final class UpdateChanged extends DemoIntent {
        private final boolean isChange;

        public UpdateChanged(boolean isChange) {
            this.isChange = isChange;
        }

        public boolean getIsChange() {
            return isChange;
        }
    }
}

State 可以是密封类,也可以是普通类。现在都推荐密封类,这里就以普通类来写。

kotlin 复制代码
    data class Demo14ViewState(
        val industrys: List<Industry> = emptyList(),
        val schools: List<SchoolBean> = emptyList(),
        var isChanged: Boolean = false
    ) : BaseViewState()

然后我们在 ViewModel 中定义数据源 MutableLiveData 与 手动的 intent 分发入口:

kotlin 复制代码
@HiltViewModel
class Damo14ViewModel @Inject constructor(
    private val mRepository: Demo5Repository,
     val savedState: SavedStateHandle
) : BaseViewModel() {

    private val _viewStates: MutableLiveData<Demo14ViewState> = MutableLiveData(Demo14ViewState())
    //只需要暴露一个LiveData,包括页面所有状态
    val viewStates: LiveData<Demo14ViewState> = _viewStates

    //Action分发入口
    fun dispatch(intent: DemoIntent) {
        when (intent) {
            is DemoIntent.RequestIndustry -> requestIndustry()
            is DemoIntent.RequestSchool -> requestSchool()
            is DemoIntent.RequestAllData -> getTotalData()
            is DemoIntent.UpdateChanged -> changeData(intent.isChange)
        }
    }


    ...

    //以获取全部数据为示例
    private fun getTotalData() {

        //默认执行在主线程的协程-必须用(可选择默认执行在IO线程的协程)
        launchOnUI {

            //开始Loading
            loadStartProgress()

            val industryResult = async {
                mRepository.getIndustry()
            }

            val schoolResult = async {
                mRepository.getSchool()
            }

            //一起处理数据
            val industry = industryResult.await()
            val school = schoolResult.await()

            //如果都成功了才一起返回
            if (industry is OkResult.Success && school is OkResult.Success) {
                loadHideProgress()

                //设置多种LiveData
                _viewStates.setState {
                    copy(industrys = industry.data ?: emptyList(), schools = school.data ?: emptyList())
                }
            }
        }
    }

}

接下来我们在 Activity 就能监听我们在 ViewModel 中定义的 MutableLiveData 值。

kotlin 复制代码
    override fun startObserve() {
        //监听两者数据变化
        mViewModel.viewStates.observeState(
            this,
            Damo14ViewModel.Demo14ViewState::industrys,
            Damo14ViewModel.Demo14ViewState::schools
        ) { industry, school ->

            YYLogUtils.w("industry: $industry ; school: $school")
        }

        //只监听changed的变换
        mViewModel.viewStates.observeState(this, Damo14ViewModel.Demo14ViewState::isChanged) {
            if (it) {
                val industry = mViewModel.viewStates.value?.industrys
                val school = mViewModel.viewStates.value?.schools
                mBinding.tvMessage.text = "industry: $industry ; school: $school"
            }
        }
    }

    

需要注意的是自定义的 setState 方法与 observeState 方法是我们自定义的,因为我们有可能只需要设置 State 数据源中的一个字段,或者只监听 MutableLiveData 中的一个字段的变化。

LiveData 的扩展方法如下:

kotlin 复制代码
//监听一个属性
fun <T, A> LiveData<T>.observeState(
    lifecycleOwner: LifecycleOwner,
    prop1: KProperty1<T, A>,
    action: (A) -> Unit
) {
    this.map {
        StateTuple1(prop1.get(it))
    }.distinctUntilChanged().observe(lifecycleOwner) { (a) ->
        action.invoke(a)
    }
}

//监听两个属性
fun <T, A, B> LiveData<T>.observeState(
    lifecycleOwner: LifecycleOwner,
    prop1: KProperty1<T, A>,
    prop2: KProperty1<T, B>,
    action: (A, B) -> Unit
) {
    this.map {
        StateTuple2(prop1.get(it), prop2.get(it))
    }.distinctUntilChanged().observe(lifecycleOwner) { (a, b) ->
        action.invoke(a, b)
    }
}

//监听三个属性
fun <T, A, B, C> LiveData<T>.observeState(
    lifecycleOwner: LifecycleOwner,
    prop1: KProperty1<T, A>,
    prop2: KProperty1<T, B>,
    prop3: KProperty1<T, C>,
    action: (A, B, C) -> Unit
) {
    this.map {
        StateTuple3(prop1.get(it), prop2.get(it), prop3.get(it))
    }.distinctUntilChanged().observe(lifecycleOwner) { (a, b, c) ->
        action.invoke(a, b, c)
    }
}


internal data class StateTuple1<A>(val a: A)
internal data class StateTuple2<A, B>(val a: A, val b: B)
internal data class StateTuple3<A, B, C>(val a: A, val b: B, val c: C)


//更新State
fun <T> MutableLiveData<T>.setState(reducer: T.() -> T) {
    //拿到T去处理,处理完成之后的T,再次赋值给LiveData
    this.value = this.value?.reducer()
}

最后就是 Intent 的事件集中管理。由于我们没有 Flow 的流监听,这里最简单的做法是直接在 Activity 中调用分发接口:

kotlin 复制代码
   override fun init() {
        //发送Intent指令,具体的实现由ViewModel实现
        mViewModel.dispatch(Damo14ViewModel.DemoAction.RequestAllData)
    }

    fun fetchData() {
        //发送Intent指令,具体的实现由ViewModel实现
        mViewModel.dispatch(Damo14ViewModel.DemoIntent.UpdateChanged(true))
    }

这样就完成了一个最简单的单向流与数据统一的State管理。虽然有了最简单的实现,但是如果用 Kotlin 的 Flow 来做的话,会更加的优雅。

二、使用协程 Flow 之后的使用与封装

使用一个标准的 Kotlin Flow 的定义的 MVI 架构,我们把 Intent 与 State 都使用封装类。

csharp 复制代码
/**
 * 页面意图
 */
sealed class MVI3Intent {

    //行为- 想要获取行业数据
    object GetIndustry : MVI3Intent()

    //行为- 想要获取学校数据
    object GetSchool : MVI3Intent()
    
}

关于 State 我们封装了页面状态与数据在一起:

kotlin 复制代码
/**
 * 页面状态
 */
sealed class MVI3State {

    //默认空闲
    object Idle : MVI3State()

    //加载
    object Loading : MVI3State()

    //错误信息
    data class Error(val error: String) : MVI3State()

    //成功的行业数据
    data class Industries(val indusory: List<Industry>) : MVI3State()

    //成功的学校数据
    data class Schools(val schools: List<SchoolBean>) : MVI3State()
    
}

在 ViewModel 中我们就可以通过 Channel 来分发 Intent 事件。

scss 复制代码
   //创建意图管道,容量无限大 (可以用Flow的监听效果,观察者模式改变之后就自动更新)
    //为什么用Channel不用Flow,是因为只需要单向流动就行了,Channel是单对单,Flow的单对多
    //Channel的发送send和接收receive,发出的事件只能被接收一次,接收之后就不能再次接收了,很适合这个场景。 万一屏幕旋转重建了也不会再度触发事件。
    val mainIntentChannel = Channel<MVI3Intent>(Channel.UNLIMITED)
    val mainIntentChannel = Channel<MVI3Intent>(Channel.UNLIMITED)

    init {
        //之前我们是用dispatch主动分发,这里是通过Channel的方式自动分发的。
        viewModelScope.launch {
            //收集意图 (观察者模式改变之后就自动更新)用于协程通信的,所以需要在协程中调用
            mainIntentChannel.consumeAsFlow().collect { value ->
                when (value) {
                    //根据意图事件分别调用不同的方法
                    is MVI3Intent.GetIndustry -> requestIndustry()
                    is MVI3Intent.GetSchool -> requestSchool()
                    else -> {}
                }
            }
        }
    }

我们用 StateFlow 来定义和接收页面的状态与数据,整个 ViewModel 的数据如下:

kotlin 复制代码
@HiltViewModel
class Demo14MVI3ViewModel @Inject constructor(
    private val mRepository: Demo5Repository,
    val savedState: SavedStateHandle
) : BaseViewModel() {

    val mainIntentChannel = Channel<MVI3Intent>(Channel.UNLIMITED)

    private val _uiState = MutableStateFlow<MVI3State>(MVI3State.Idle)
    val uiState: StateFlow<MVI3State> get() = _uiState


    init {
        viewModelScope.launch {

            mainIntentChannel.consumeAsFlow().collect { value ->
                when (value) {
            
                    is MVI3Intent.GetIndustry -> requestIndustry()
                    is MVI3Intent.GetSchool -> requestSchool()
                    else -> {}
                }
            }
        }
    }


    //获取行业数据
    private fun requestIndustry() {

        viewModelScope.launch {
            //请求Loading
            _uiState.value = MVI3State.Loading

            val result = mRepository.getIndustry()

            result.checkResult(success = {
                //请求成功
                _uiState.value = MVI3State.Industries(it!!)

            }, error = {
                //请求失败
                MVI3State.Error(it ?: "UnKnown Error")
            })

        }

    }

    //获取学校数据
    private fun requestSchool() {

        viewModelScope.launch {
            //请求Loading
            _uiState.value = MVI3State.Loading

            val result = mRepository.getSchool()

            result.checkResult(success = {
                //请求成功
                _uiState.value = MVI3State.Schools(it!!)

            }, error = {
                //请求失败
                MVI3State.Error(it ?: "UnKnown Error")
            })

        }
    }

}

在 Activity 中的 Intent 发送与 State 接收和之前的比较类似,只是把事件的手动调用变为了 Channel 分发,LiveData 的监听变为 StateFlow 的监听。

kotlin 复制代码
  override fun init() {
        observeViewModel()

        mBinding.btnGetData.text = "点击获取数据"
        mBinding.btnGetData.setOnClickListener { view ->

            lifecycleScope.launch {
                mViewModel.mainIntentChannel.send(MVI3Intent.GetIndustry)
            }
        }
    }

    /**
     * 观察ViewModel
     */
    private fun observeViewModel() {
        lifecycleScope.launch {
            //状态收集,(实际就是StateFlow的监听)
            mViewModel.uiState.collect {
                when (it) {
                    is MVI3State.Idle -> {
                    }
                    is MVI3State.Loading -> {
                        LoadingDialogManager.get().showLoading(this@Demo14MVI3Activity)
                    }
                    is MVI3State.Industries -> {
                        LoadingDialogManager.get().dismissLoading()
                        YYLogUtils.w("indusory:$it.indusory")
                    }
                    is MVI3State.Schools -> {
                        LoadingDialogManager.get().dismissLoading()
                        YYLogUtils.w("schools:${it.schools}")
                    }
                    is MVI3State.Error -> {
                        LoadingDialogManager.get().dismissLoading()
                        YYLogUtils.d("错误: $it.error")
                    }
                }
            }
        }
    }

这样就是最常见的 MVI 格式,也是大家推荐比较多的方案了。既然是最常见的方案了,我们就能封装一下方便使用了。

三、配合 ViewBinding 与 ViewModel 基类封装

我们可以把 Intent 与 State 抽取出接口类,方便继承泛型,并且在 ViewModel 中封装对应的代码逻辑,并暴露每个 ViewModel 需要处理的 State 即可。

而页面的封装,我们可以结合 ViewBinding 来封装,毕竟已经是 MVI 单向数据流了,那么使用 ViewBinding 就比 DataBinding 更加合适了,不然就失去单向数据的意义了。

先定义 State 与 Intent 的接口限定类:

less 复制代码
//MVI 页面事件管理的基类
//在R8代码混淆压缩时必须保留被标记的类或方法,以防止代码出现因混淆而导致的崩溃。
@Keep
interface IUiIntent

//MVI 页面状态管理的基类
@Keep
interface IUiState

基于此接口我们就能封装 ViewModel 的逻辑,把一些公共的代码封装起来,并暴露每个页面不同的处理:

kotlin 复制代码
abstract class BaseVB2ViewModel<UiIntent : IUiIntent, UiState : IUiState> : BaseViewModel() {

    private val _uiStateFlow = MutableStateFlow(initUiState())
    val uiStateFlow: StateFlow<UiState> = _uiStateFlow

    //页面事件的 Channel 分发
    private val _uiIntentFlow = Channel<UiIntent>(Channel.UNLIMITED)

    //更新页面状态
     fun updateUiState(reducer: UiState.() -> UiState) {
        _uiStateFlow.update { reducer(_uiStateFlow.value) }
    }

    //更新State
    fun <T> sendUiState2(reducer: T.() -> T) {

    }

    //发送页面事件
    fun sendUiIntent(uiIntent: UiIntent) {
        viewModelScope.launch {
            _uiIntentFlow.send(uiIntent)
        }
    }

    init {
        // 这里是通过Channel的方式自动分发的。
        viewModelScope.launch {
            //收集意图 (观察者模式改变之后就自动更新)用于协程通信的,所以需要在协程中调用
            _uiIntentFlow.consumeAsFlow().collect { intent ->
                handleIntent(intent)
            }
        }

    }

    //每个页面的 UiState 都不相同,必须实自己去创建
    protected abstract fun initUiState(): UiState

    //每个页面处理的 UiIntent 都不同,必须实现自己页面对应的状态处理
    protected abstract fun handleIntent(intent: UiIntent)

}

使用的时候,我们定义自己页面的 State 与 Intent 如下:

kotlin 复制代码
sealed class Demo14Intent : IUiIntent {
    object GetIndustry : Demo14Intent()
    object GetSchool : Demo14Intent()
}

由于我自己的页面 Load 逻辑是单独封装处理的,这里只是演示一下如何自己手动处理UI加载的状态。如果你也自行处理了 UI 加载状态,那么是可以不需要的。

kotlin 复制代码
data class Demo14State(val industryUiState: IndustryUiState, val schoolUiState: SchoolUiState, val loadUiState: LoadUiState) : IUiState {}


// 内部分子类,定义了常见的四种加载状态
sealed class LoadUiState {
    object Idle : LoadUiState()  //默认空闲
    data class Loading(var isShow: Boolean) : LoadUiState()  //展示Loading
    object ShowContent : LoadUiState()           //展示布局
    data class Error(val msg: String) : LoadUiState()  //失败
}

// 内部分子类,并实现初始化值,成功值,失败值
sealed class IndustryUiState {
    object INIT : IndustryUiState()
    data class SUCCESS(val industries: List<Industry>) : IndustryUiState()
}

// 内部分子类,并实现初始化值,成功值,失败值
sealed class SchoolUiState {
    object INIT : SchoolUiState()
    data class SUCCESS(val schooles: List<SchoolBean>) : SchoolUiState()
}

那么使用的时候,我们初始化自己的 ViewModel 的时候就把自己的 Intent 与 State 传入泛型即可:

kotlin 复制代码
@HiltViewModel
class Demo14JMVI2ViewModel @Inject constructor(
    private val mRepository: Demo5Repository,
    val savedState: SavedStateHandle
) : BaseVB2ViewModel<Demo14Intent, Demo14State>() {

    override fun initUiState(): Demo14State {
        return return Demo14State(IndustryUiState.INIT, SchoolUiState.INIT, LoadUiState.Idle);
    }

    override fun handleIntent(intent: Demo14Intent) {
        when (intent) {
            Demo14Intent.GetIndustry -> requestIndustry()
            Demo14Intent.GetSchool -> requestSchool()
            else -> {}
        }
    }

    //获取行业数据
    private fun requestIndustry() {

        viewModelScope.launch {

            //请求Loading
            updateUiState {
                copy(loadUiState = LoadUiState.Loading(true))
            }

            val result = mRepository.getIndustry()

            result.checkResult(success = {

                //请求成功
                updateUiState {
                    //调用data class 的copy 赋值更方便
                    copy(
                        industryUiState = IndustryUiState.SUCCESS(it ?: emptyList()),
                        loadUiState = LoadUiState.ShowContent
                    )
                }

            }, error = {
                //请求失败
                updateUiState {
                    copy(loadUiState = LoadUiState.Error(it ?: "未知错误"))
                }
            })

        }

    }

    //获取学校数据
    private fun requestSchool() {

        viewModelScope.launch {

            //请求Loading
            updateUiState {
                copy(loadUiState = LoadUiState.Loading(true))
            }

            val result = mRepository.getSchool()

            result.checkResult(success = {

                //请求成功
                updateUiState {
                    //调用data class 的copy 赋值更方便
                    copy(
                        schoolUiState = SchoolUiState.SUCCESS(it ?: emptyList()),
                        loadUiState = LoadUiState.ShowContent
                    )
                }

            }, error = {
                //请求失败
                updateUiState {
                    copy(loadUiState = LoadUiState.Error(it ?: "未知错误"))
                }
            })

        }

    }


}

关于 Activity 的封装,也是比较简单,我们把 ViewBinding 的代码封装一下:

可以用构造传参高阶函数处理:

kotlin 复制代码
abstract class BaseVB2Activity<VM : ViewModel, VB : ViewBinding>(
    val block: (LayoutInflater) -> VB
) : AbsActivity() {

    protected lateinit var mViewModel: VM

    private var _binding: VB? = null
    protected val mBinding: VB
        get() = requireNotNull(_binding) { "ViewBinding对象为空" }

    open protected fun createViewModel(): VM {
        return ViewModelProvider(this).get(getVMCls(this))
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        _binding = block(layoutInflater)
        super.onCreate(savedInstanceState)
    }

    override fun setContentView() {
        setContentView(mBinding.root)
        mViewModel = createViewModel()
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }

}

也可以暴露方法去处理:

kotlin 复制代码
abstract class BaseVB2Activity<VM : ViewModel, VB : ViewBinding>() : AbsActivity() {

    protected lateinit var mViewModel: VM

    private var _binding: VB? = null
    protected val mBinding: VB
        get() = requireNotNull(_binding) { "ViewBinding对象为空" }

    abstract fun createViewBinding(): VB

    open protected fun createViewModel(): VM {
        return ViewModelProvider(this).get(getVMCls(this))
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        _binding = createViewBinding()
        super.onCreate(savedInstanceState)
    }

    override fun setContentView() {
        setContentView(mBinding.root)
        mViewModel = createViewModel()
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }

}

逻辑是一样的,使用的完整 Activity 为:

kotlin 复制代码
@AndroidEntryPoint
class Demo14MVIActivity : BaseVB2Activity<Demo14JMVI2ViewModel, ActivityDemo14JavaMviBinding>(
    ActivityDemo14JavaMviBinding::inflate
) {

    override fun init() {
        w("init - mBinding.btnGetData:" + mBinding.btnGetData)
        mBinding.btnGetData.text = "点击获取数据"
        mBinding.btnGetData.setOnClickListener { view ->
            w("点击到按钮:$view")
            mViewModel.sendUiIntent(Demo14Intent.GetSchool)
        }


        lifecycleScope.launchWhenStarted {
            //已经是StateFlow了,还需要防抖去重吗?
            mViewModel.uiStateFlow.map { it.schoolUiState }
                .distinctUntilChanged()
                .collect { schoolState ->
                    when (schoolState) {
                        is SchoolUiState.INIT -> {}
                        is SchoolUiState.SUCCESS -> {
                            YYLogUtils.w("加载学校数据成功")
                        }
                    }
                }
        }
    }

}

这样我们在之前的 Kotlin 使用的基础上就结合 ViewModel 与 ViewBinding完成了一个简单的封装。

四、State 继续细分为 UIEffect 和 UIState,继续封装

State 是我们的UI状态我们都知道了,但是其中如果要区分持久性UI状态,与一次性UI状态,怎么办?

很典型的例子,横竖屏切换在发生生命周期变化时候需要回放数据,UIState作为持久性UI状态是没问题的,但是一些一次性消费UI事件,如弹窗,吐司,导航等状态我们并不好处理,所以为了区分持久性状态与一次性状态,一些 MVI 架构继续演变,把之前的 State 继续细分为 UIEffect 和 UIState。

用什么数据来实现一次性状态呢? StateFlow 肯定不行,我们得用 SharedFlow 。这下 Channel StateFlow SharedFlow 都齐活了,为什么这么用关于它们之间的区别如果不了解可以看看我早期的文章 【传送门】

我们在上章的基础上加上 Effect 的类型:

kotlin 复制代码
@Keep
interface IUIEffect

BaseViewModel修改如下:

kotlin 复制代码
abstract class BaseVB2ViewModel<UiIntent : IUiIntent, UiState : IUiState, UIEffect : IUIEffect> : BaseViewModel() {

 ...

    //一次性事件,无需更新
    private val _effectFlow = MutableSharedFlow<UIEffect>()
    val uiEffectFlow: SharedFlow<UIEffect> by lazy { _effectFlow.asSharedFlow() }

    //两种方式发射
    protected fun sendEffect(builder: suspend () -> UIEffect?) = viewModelScope.launch {
        builder()?.let { _effectFlow.emit(it) }
    }

    //两种方式发射
    protected suspend fun sendEffect(effect: UIEffect) = _effectFlow.emit(effect)

}

定义我们页面的 Effect 类:

kotlin 复制代码
sealed class Demo14Effect : IUIEffect {
    data class NavigationToSchoolDetail(val id: Int) : Demo14Effect()
}

那么具体的 ViewModel 我们就改为:

kotlin 复制代码
@HiltViewModel
class Demo14JMVI2ViewModel @Inject constructor(
    private val mRepository: Demo5Repository,
    val savedState: SavedStateHandle
) : BaseVB2ViewModel<Demo14Intent, Demo14State, Demo14Effect>() {

    override fun initUiState(): Demo14State {
        return return Demo14State(IndustryUiState.INIT, SchoolUiState.INIT, LoadUiState.Idle);
    }

    override fun handleIntent(intent: Demo14Intent) {
        when (intent) {
            Demo14Intent.GetSchool -> requestSchool()
            else -> {}
        }
    }

    //获取学校数据
    private fun requestSchool() {

        viewModelScope.launch {

            //请求Loading
            updateUiState {
                copy(loadUiState = LoadUiState.Loading(true))
            }

            val result = mRepository.getSchool()

            if (result is OkResult.Success) {

                val data = result.data

                //请求成功
                updateUiState {
                    //调用data class 的copy 赋值更方便
                    copy(
                        schoolUiState = SchoolUiState.SUCCESS(data ?: emptyList()),
                        loadUiState = LoadUiState.ShowContent
                    )
                }

                sendEffect(Demo14Effect.NavigationToSchoolDetail(data!![0].school_id))

            } else {

                val message = (result as OkResult.Error).exception.message
                updateUiState {
                    copy(loadUiState = LoadUiState.Error(message ?: "未知错误"))
                }

            }
        }
    }

}

关于 Activity 的封装我们也能修改,之前我们需要在 Activity 中调用 ViewBinding 的 inflate 方法。现在我们可以在基类中像 ViewModel 一样的反射调用初始化。

kotlin 复制代码
abstract class BaseVB2Activity<VM : ViewModel, VB : ViewBinding> : AbsActivity() {

    protected lateinit var mViewModel: VM

    private var _binding: VB? = null
    protected val mBinding: VB
        get() = requireNotNull(_binding) { "ViewBinding对象为空" }

    //反射创建ViewModel
    open protected fun createViewModel(): VM {
        return ViewModelProvider(this).get(getVMCls(this))
    }

    //反射创建ViewBinding
    open protected fun createViewBinding() {

        val clazz: Class<*> =  (this.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[1] as Class<VB>

        try {
            _binding = clazz.getMethod(
                "inflate", LayoutInflater::class.java
            ).invoke(null, layoutInflater) as VB

        } catch (e: Exception) {
            e.printStackTrace()
            throw IllegalArgumentException("无法通过反射创建ViewBinding对象")
        }

    }

    override fun onCreate(savedInstanceState: Bundle?) {
        createViewBinding()
        super.onCreate(savedInstanceState)
    }

    override fun setContentView() {
        setContentView(mBinding.root)
        mViewModel = createViewModel()
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }

}

使用 Activity 起来就更加简单啦:

Activity 就简单啦:

kotlin 复制代码
@AndroidEntryPoint
class Demo14MVIActivity : BaseVB2Activity<Demo14JMVI2ViewModel, ActivityDemo14JavaMviBinding>() {

    override fun init() {
        w("init - mBinding.btnGetData:" + mBinding.btnGetData)
        mBinding.btnGetData.text = "点击获取数据"
        mBinding.btnGetData.setOnClickListener { view ->
            w("点击到按钮:$view")
            mViewModel.sendUiIntent(Demo14Intent.GetSchool)
        }


        lifecycleScope.launchWhenStarted {
            mViewModel.uiStateFlow
                .map { it.schoolUiState }
                .distinctUntilChanged()
                .collect { schoolState ->
                    when (schoolState) {
                        is SchoolUiState.INIT -> {}
                        is SchoolUiState.SUCCESS -> {
                            YYLogUtils.w("Flow回调 -  加载学校数据成功")
                        }
                    }
                }
        }

        lifecycleScope.launchWhenResumed {
            //一次性状态的接收与处理
            mViewModel.uiEffectFlow
                .collect {
                    when (it) {
                        is Demo14Effect.NavigationToSchoolDetail -> {
                            ToastUtils.makeText(mActivity, "Flow回调 - 跳转到学校详情:${it.id}")
                        }
                    }
                }
        }

    }

虽然代码是越分越细了,但是我们可以通过封装的方式尽量让一些重复的逻辑与代码简单化。

其实可以看出 MVI 架构一步步也是从开始的简单到后面的复杂了,责任划分越来越细致,封装也更加复杂了。

其实不止这一点所有的架构都是这样,包括我们 Android 开发架构从 MVC -> MVP -> MVVP(Lite版) -> MVVM(DataBinding) -> MVI 它们的变化过程其实越是一步步的复杂起来的,从之前代码都在一个类里面到后面的按职责划分功能一步步的细化再细化。

总结

虽然说 MVI 架构有很多的实现方案,但是我并不是说推荐越复杂越好,也不是说推荐大家一定就用 MVI 架构,架构越是复杂,层级越多,责任划分越是细致,那么就会导致类越多,逻辑越多,其实并不适合每一个人或开发团队。还是那句话老话按需选择即可。

从后面好几种 Kotlin 语言与协程架构包括 Flow 的结合之后组建一个 MVI 架构可以说是充分发挥了他们的特点。如果是使用 MVI 架构的更推荐使用 Kotlin 语言实现,相对优雅。

关于 Compose 这种声明式 UI 与 MVI 架构搭配就更舒服了,由于我还没开源 Compose 项目就不写啦,其实写法都是大差不差了的,大家可以自行尝试、

关于本文的内容如果想查看源码可以点击这里 【传送门】。你也可以关注我的这个Kotlin项目(虽然是两年前的老项目了),我有时间都会持续更新。

如果有更多的更好的其他方式,也希望大家能评论区交流一起学习进步。如果我的代码或者注释、讲解等不到位或错漏的地方,希望同学们可以指出。

如果感觉本文对你有一点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。

相关推荐
Estar.Lee42 分钟前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh1 小时前
uiautomator案例
android
工业甲酰苯胺2 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3432 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee4 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯4 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey6 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!7 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟8 小时前
Android音频采集
android·音视频
小白也想学C9 小时前
Android 功耗分析(底层篇)
android·功耗