如何应对Android面试官 -> 玩转 MVx(MVC、MVP、MVVM、MVI)

前言


image.png

本章主要基于以下几个方向进行 MVx 的讲解,带你玩转 MVx;

MVC、MVP、MVVM、MVI 它们到底是什么?


分文件、分模块、分模式

一个文件打天下

为什么不要用一个页面打天下?

页面是给用户看的,随着版本的迭代,页面上的交互需求也在不断的变化,导致页面不断的在修改,页面在不断的修改,那么这个文件中就不能放太多的代码,否则导致后续迭代起来就很困难;

页面很复杂怎么办?

一个复杂页面,也是由一个一个的 View 组成的,那么我们就可以将 View 拆成自定义 View。同时将这个 View 的相关逻辑,拆分到对应的逻辑处理层;

应用开发原则

  • 遵循面向对象的SOLID原则;
  • 视图、数据、逻辑分离;

六大原则

image.png

  • 单一职责

一个 class 完成一件事情;一个 class 只做一件事情,当有更多事情的时候,使用继承,那么就引申出了『开闭原则』;

  • 开闭原则

对继承开放,对修改关闭(不能改变基类中的逻辑);多个类文件完成多件事情的时候,使用继承,对文件尽量不做修改;

  • 里氏替换原则

子类覆写父类函数的时候,不能改变父类的逻辑;例如:「基类车 有一个 run 方法,子类覆写之后不能改成 fly 方法」

  • 依赖倒置原则

不依赖实现,只依赖接口;「例如车在路上跑,车不应该依赖路的具体实现,而是一个路的接口」;

  • 接口隔离原则

接口的粒度要小,接口的最小化;『例如,人和路的接口要分别定义,而不是定义成一个接口』;

  • 迪米特原则

最小支持原则;

视图、数据、逻辑分离

从静态角度的一个分离,以及生命周期的控制,数据需要在什么情况下进行销毁;

App 架构设计

image.png

整体遵循一个 层次化、模块化、控件化;

而模块化又可以细分成:组件化、插件化;

MacHi 2024-06-05 20-48-18.png
image.png
组件化一般针对的都是业务组件,业务组件之间互相不依赖,公共部分抽象成接口,下沉到通用组件;组件化的意义是通过 App 模块把这些组件拼装起来,组装成一个 APK,这种就是在编译期间,它们是一个一个的组件;

而插件化,本质上可以理解为,把这一个一个的组件打包成一个一个的无图标的 APK,然后通过后下载的方式集成到应用中,以此来缩小安装包的体积;

MVx

在 MVx 中,这个 M 基本都是一样的,V 的角色其实是有一些差异的;

MVC

数据层、视图层、控制层(逻辑层)如何定义?

image.png

  • 数据层

对数据 + 对数据进行的操作(不依赖视图的操作);例如我们利用 RxJava 对数据进行变换的操作,map、flatMap等等,放到 model 层执行;

  • 视图层

不同的模式有不同的定义,在 Android 中就是 Activity + xml + fragment;

  • 控制层

view 和 model 之间的通信和交互;

在 Andriod 中 xml 布局的功能性太弱, Activity 实际上负责了 View 层与 Controller 层两者的工作,所以在 Android 中 MVC 更像是这种形式,而 Model 层好多人直接把 bean 写在了 model 下,它俩并不能完全相等;

image.png

MVC 相对一个文件打天下

优点是:抽离了 model;

缺点是:controller 的权力太大,在 Android 中 Activity 承担了 Controller 的角色,如果这个 Activity 页面逻辑太多的话,这个 Activity 依然会变得特别臃肿,就又回到了一个文件打天下的场景了;

那么,怎么办呢?变! MVP 来了

MVP

image.png

  • Model

主要负责数据的处理,这个没有什么变化;

  • View

对应于 Activity 与 XML,只负责显示 UI,只与 Presenter 层交互,与 Model 层没有耦合;

  • Presenter

主要负责处理业务逻辑,通过接口回调 View 层;

MVP 相对 MVC 的

优点是:在 MVC 的基础上通过 Interface 彻底分离了 View 和 Model,Activity 只剩下了 View,Presenter 承担了 View 和 Model 之间的交互,满足了单一职责,对视图数据的逻辑分离是清晰的

缺点是:引入了 interface,方法增多,引入一个方法要改很多地方,并且要面临着回调地狱;

那怎么办呢?变,MVVM 来了;

MVVM

引入 MVVM 的模式,本质上就是在 MVP 的基础上的一次变革,就是要把引入的 interface 给干掉!

image.png

可以看出 MVVM 与 MVP 最大的区别是,不用主动去刷新 UI 了,只要 model 层数据变了,就会自动更新到 UI 上;

那么在 Android 上实现数据的双向绑定是通过 DataBinding 实现的,通过 DataBinding 就是要把数据自动刷新到 View 上,View 上面有什么事件,要绑定到 ViewModel 上来;

不过,很多开发者并不喜欢使用 DataBinding,因为它需要你在 xml 中写一些逻辑,并且要在 gradle 中引入 DataBinding 的支持,就会导致文件路径的更改,DataBinding 不能自动更新等一些 ide 上比较难用的问题;

引入 DataBinding 很简单,只需要在使用它的 module 中加入下面这个代码就行:

dataBinding {
    enable true
}

这里额外插入一个 viewBinding,viewBinding 和 DataBinding 还是有区别的,viewBinding 只能省略 findViewById 的操作;DataBinding 除了 viewBinding 的能力之外,还能绑定数据,同时需要修改 xml,而 viewBinding 不需要修改 xml;

Android 上的 MVVM 一定要结合 DataBinding 来实现,否则就会变成下面的这种

image.png

View 观察 ViewModel 的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定,所以其实 MVVM 的这一大特性并没有用到;

View 通过调用 ViewModel 提供的方法来与 ViewModel 交互;

View 通过 LiveData 来观察 ViewModel 的数据变化并自我更新,但这是单一数据源而不是双向数据绑定;

MVVM 相对 MVP 的

优点是:更彻底的解放了Activity,数据驱动是 MVVM 的关键词,ViewModel 不再主动调用方法去更新界面,而是主动更新数据,同时界面采用观察数据的方式,等待被更新,由于MVVM不需要写契约类Contract,也不存在Presenter层,也就不存在接口方法众多的问题;

缺点是:有问题的时候 不好找到原因;对外暴露的 LiveData 是不可变的,需要添加不勺模板代码且容易被遗忘;View 层与 ViewModel 层的交互比较分散零乱,不成体系;并且编译的时候会导致编译速度变慢,会额外生成ViewModel 文件;

这里额外插两个问题:

  • Jetpack 中的 ViewModel 和 MVVM 中的 ViewModel 它俩是一回事吗?

Jetpack 的 ViewModel 并不是 MVVM 中的 ViewModel,简单来说,MVVM 和 MVP 的最大区别在于数据绑定,而数据绑定本质上一种框架特性而非框架风格,区别在于数据绑定不能用一句简单的『建议大家使用』来推荐,只能由推荐方去实现这么一个功能,然后告诉大家『快用这个功能,你就能 MVVM 了』,也就是说,对于 Android 开发者来说,用了 JetPack 的 DataBinding 库,你才能叫用了 MVVM,光用 JetPack 的 ViewModel 是没用的,只要用了 DataBinding 库,基本架构也做了完美拆分(而不是把业务代码和界面逻辑全部放进 Activity),那么就算你不用 Jetpack 的 ViewModel,也是 MVVM (注意,不是勉强算 MVVM)而是确确实实的就是 MVVM。

  • 在 MVVM 模式中,ViewModel 是与 View 一一对应还是可以被其他 View 复用?

ViewModel 在 MVVM 中起到的作用是数据源,它内部包含的数据源恰好就是它所对应的 View 中所需要的数据。View 不需要对数据的获取做管理,也不需要对用户交互所导致的数据变化做管理,只用展示就可以;

所以,MVVM 中的 ViewModel 在原则是不被复用的;

同时,这里的 View 是 MVVM 中的 View,而不是 Android 系统的中的 View.java,每个 ViewModel 应该和每个 Activity/Fragment 做一一对应,并让它们在把数据分发到各个 View,

针对 MVVM 的缺陷,怎么办呢?变,MVI 来了;

MVI

说 MVI 之前,先来说两个概念,什么是响应式编程、命令式编程?

  • 响应式编程

持续性地赋值;响应式编程是一种面向数据流和变化传播的声明式编程范式 "数据流"和"变化传播"是相互解释的:有数据流动,就意味着变化会从上游传播到下游,变化从上游传播到下游,就形成了数据流。

  • 命令式编程

一次性赋值;

MVI 与 MVVM 比较相似,借鉴了前端框架思想,更加强调数据的单向流动和唯一数据源;

MVI 强调用响应式变成的方式进行事件到状态的变换,并且还得保证界面状态有唯一的可信的数据源,这样界面的刷新就形成了单向数据流,数据永远在一个环形结构中单向流动,不能反向流动;不管使用的是 ViewModel 还是 Presenter,MVI 关心的不是界面状态的持有者,而是整个更新界面数据链路的流动方式和方向;

image.png

单向数据流

单向数据流,界面变化是数据流的末端,界面消费上游产生的数据,并随上游数据的变化进行刷新;

用户操作以 Intent 的形式通知 Model;

Model 基于 Intent 更新 State;

View 接收到 State 的变化刷新 UI;

整个流程中包含两个数据:

数据一:从界面发出的事件(意图),即 MVI 中 I(Intent)。在 MVP 和 MVVM 中,界面发出的事件是通过一个 Presenter/ViewModel 的函数调用实现的,这是命令式的。为了实现响应式编程,需把这个函数调用转换成一个数据,即 Intent。

数据二:返回给界面的状态,即 MVI 中的 M(Model),它通常被称为状态 State,从字面就可以感觉到界面状态是会时刻发生变化的;

MVI 相对 MVVM 的

优点是:强调数据单向流动,很容易对状态变化进行跟踪和回溯;State 的集中管理,只需要订阅一个 ViewState 就可以获取界面的所有状态;

基于 MVI + Flow 的实战登录模块

image.png

intent 模块定义了 LoginViewEvent 用来分发数据,LoginViewState 用来响应数据,LoginViewAction 来更新 ViewState;

sealed class LoginViewEvent {
    data class ShowToast(val message: String): LoginViewEvent()
    object ShowLoadingDialog: LoginViewEvent()
    object DismissLoadingDialog: LoginViewEvent()
}

LoginViewState

data class LoginViewState(val userName: String= "", val password: String = "") {
    val isLoginEnable: Boolean
        get() = userName.isNotEmpty() && password.length >= 6

    val passwordTipVisible: Boolean
        get() = password.length in 1..5
}

LoginViewAction

sealed class LoginViewAction {
    data class UpdateUserName(val userName: String): LoginViewAction()
    data class UpdatePassword(val password: String): LoginViewAction()
    object Login: LoginViewAction()
}

我们使用 ViewModel 来承载 MVI 的 model 层,总结结构和 MVVM 也比较类似,主要区别在于 Model 与 View 的交互部分;

LoginViewModel 中承载 UI 状态,并暴露 ViewState 供 View 订阅;

View 层通过 LoginViewAction 更新 LoginViewState;

class LoginViewModel: ViewModel() {

    private val _viewState =  MutableStateFlow(LoginViewState())
    val viewStates = _viewState.asStateFlow()

    private val _viewEvent = MutableSharedFlow<LoginViewEvent>()
    val viewEvents = _viewEvent.asSharedFlow()


    fun dispatch(loginViewAction: LoginViewAction) {
        when(loginViewAction) {
            is LoginViewAction.UpdateUserName -> {
                updateUserName(loginViewAction.userName)
            }

            is LoginViewAction.UpdatePassword -> {
                updatePassword(loginViewAction.password)
            }

            is LoginViewAction.Login -> {
                login()
            }
        }
    }

    private fun updateUserName(userName: String) {
        _viewState.value = _viewState.value.copy(userName = userName)
    }

    private fun updatePassword(passWord: String) {
        _viewState.value = _viewState.value.copy(password = passWord)
    }

    private fun login() {
        viewModelScope.launch(Dispatchers.Main) {
            flow {
                if (loginLogic()){
                    emit("登录成功")
                } else {
                    emit("登录失败")
                }
            }.flowOn(Dispatchers.IO).onStart {
                _viewEvent.emit(LoginViewEvent.ShowLoadingDialog)
            }.onEach {
                _viewEvent.emit(LoginViewEvent.DismissLoadingDialog)
                _viewEvent.emit(LoginViewEvent.ShowToast(it))
            }.catch {
                _viewState.value = _viewState.value.copy(userName = "")
                _viewState.value = _viewState.value.copy(password = "")
                _viewEvent.emit(LoginViewEvent.DismissLoadingDialog)
                _viewEvent.emit(LoginViewEvent.ShowToast(it.message.toString()))
            }.collect()
        }
    }

    private suspend fun loginLogic(): Boolean {
        viewStates.value.let {
            // 执行 http 请求
            if (it.userName == "Kobe" && it.password == "123456") {
                delay(2000)
                return true
            } else {
                return false
            }
        }
    }
}

image.png

登录页面也比较简单,就是两个输入框和一个登录按钮;

class LoginActivity: AppCompatActivity() {

    private val userName: EditText by lazy { findViewById(R.id.userName) }
    private val passWord: EditText by lazy { findViewById(R.id.passWord) }
    private val login: Button by lazy { findViewById(R.id.login) }
    private val pwdTips: TextView by lazy { findViewById(R.id.pwdTips) }
    private val viewModel: LoginViewModel = LoginViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        initView()
        initViewStates()
        initViewEvents()
    }

    private fun initView() {
        userName.addTextChangedListener {
            viewModel.dispatch(LoginViewAction.UpdateUserName(userName = it.toString()))
        }
        passWord.addTextChangedListener {
            viewModel.dispatch(LoginViewAction.UpdatePassword(password = it.toString()))
        }
        login.setOnClickListener {
            viewModel.dispatch(LoginViewAction.Login)
        }
    }

    private fun initViewStates() {
        viewModel.viewStates.let { states ->
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    states.map {
                        it.userName
                    }.distinctUntilChanged().collect{
                        userName.setText(it)
                        userName.setSelection(it.length)
                    }
                }
            }

            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    states.map {
                        it.password
                    }.distinctUntilChanged().collect{
                        passWord.setText(it)
                        passWord.setSelection(it.length)
                    }
                }
            }

            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    states.map {
                        it.isLoginEnable
                    }.distinctUntilChanged().collect {
                        login.isEnabled = it
                        login.alpha = if (it) 1f else 0.5f
                    }
                }
            }

            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    states.map {
                        it.passwordTipVisible
                    }.distinctUntilChanged().collect {
                        pwdTips.visibility = if (it) { View.VISIBLE } else { View.GONE }
                    }
                }
            }
        }
    }

    private fun initViewEvents() {
        viewModel.viewEvents.let {
            lifecycleScope.launchWhenStarted {
                it.collect {
                    when(it) {
                        is LoginViewEvent.ShowLoadingDialog -> {
                            showLoadingDialog()
                        }
                        is LoginViewEvent.DismissLoadingDialog -> {
                            dismissLoadingDialog()
                        }
                        is LoginViewEvent.ShowToast -> {
                            Toast.makeText(this@LoginActivity, it.message, Toast.LENGTH_SHORT).show()
                        }
                    }
                }
            }
        }
    }

    private var progressDialog: ProgressDialog? = null

    private fun showLoadingDialog() {
        if (progressDialog == null)
            progressDialog = ProgressDialog(this)
        progressDialog?.show()
    }

    private fun dismissLoadingDialog() {
        progressDialog?.takeIf { it.isShowing }?.dismiss()
    }
}

好了,今天 NVx 就到这里吧,感谢您的观看

下一章预告


基于 MVI 的一个新闻列表客户端实战;

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力

  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值