Android MVC、MVP、MVVM、MVI架构对比及示例

发布于:2022-11-09 ⋅ 阅读:(11) ⋅ 点赞:(0) ⋅ 评论:(0)

随着Android应用开发技术的不断成熟,应用功能越来越丰富,迭代速度要求的越来越高,应用的开发架构也在不断演进、优化,从MVC、MVP到MVVM,到如今的MVI。谷歌官方也在不断推广、优化适合Android平台的开发架构,并推出了一系列的组件如Jetpack来支撑其架构的演进。

但不管架构如何演进,其本质目的就是尽量解耦各模块、各业务之间的依赖,消除样板代码,让开发人员专注于业务开发,快速、高效、高质量完成应用的开发。

所以在了解各架构之前,我们还是要先回顾一下面向对象六大原则

面向对象六大原则

单一职责原则 Single Responsibility Principle

一个类中应该是一组相关性很高的函数、数据的封装

开闭原则 Open Close Principle

软件中的对象(类、模块、函数等)应该对于扩展是开发的,但是对于修改是封闭的。

里氏替换原则 Liskov Substitution Principle

所有引用基类的地方必须能透明地使用其子类地对象。

依赖倒置原则 Dependence Inversion Principle

指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。

  1.  高层模块不应该依赖低层模块,两者都应该依赖其抽象
  2.  抽象不应该依赖细节
  3.  细节应该依赖抽象

在java语音中,抽象就是指接口或抽象类,细节就是实现类,高层模块就是调用端,低层模块就是具体实现类
依赖倒置:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。

接口隔离原则 Interface Segregation Principe

客户端不应该依赖它不需要的接口。
类间的依赖关系应该建立在最小的接口上。

迪米特原则 Law of Demeter

一个对象应该对其他对象有最少的了解。

MVC

视图层(View)对应于XML布局文件和java代码动态添加、删除view的部分

控制层(Controller)主要负责业务逻辑,在android中由Activity、Fragment、Service等承担,同时因为XML视图功能太弱,所以Activity/Fragment等既要负责视图的显示,还要加入控制逻辑,业务处理等,承担了太多的功能。

模型层(Model):主要负责网络请求,数据库处理,I/O的操作,即页面的数据来源 

存在问题:

  1. Activity/Fragment同时负责ViewController层的工作,违背了单一职责原则
  2. Model层与View层存在耦合,存在互相依赖,违背了迪米特原则

以下都是以登录功能为例

class MVCDemoActivity : AppCompatActivity(), CoroutineScope by MainScope() {

    private lateinit var usernameEdit: EditText
    private lateinit var passwordEdit: EditText
    private lateinit var usernameText: TextView
    private lateinit var passwordText: TextView
    private lateinit var loginBtn: Button

    private var username: String = ""
    private var password: String = ""

    private var loadingDialog: LoadingDialog? = null

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

        initViews()
    }

    private fun initViews() {

        usernameText = findViewById(R.id.tv_username)
        passwordText = findViewById(R.id.tv_password)

        usernameEdit = findViewById(R.id.edit_username)
        usernameEdit.afterTextChanged {
            username = it
        }

        passwordEdit = findViewById(R.id.edit_password)
        passwordEdit.afterTextChanged {
            password = it
        }

        loginBtn = findViewById(R.id.btn_login)
        loginBtn.setOnClickListener {
            doLogin()
        }
    }

    @SuppressLint("SetTextI18n")
    private fun doLogin() {
        showLoading(true)
        launch(Dispatchers.IO) {
            when (val loginResult = HttpUtil.login(username, password)) {
                is LoginResult.Success -> {
                    runOnUiThread {
                        usernameEdit.visibility = View.GONE
                        passwordEdit.visibility = View.GONE
                        loginBtn.visibility = View.GONE

                        usernameText.text =
                            getString(R.string.username) + loginResult.userInfo.userName
                        passwordText.text =
                            getString(R.string.password) + loginResult.userInfo.password

                        showLoading(false)
                        showToast("登录成功")
                    }

                }
                is LoginResult.Fail -> {
                    runOnUiThread {
                        showLoading(false)
                        showToast(loginResult.message)
                    }
                }
            }
        }

    }

}

<--activity_mvc.xml-->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_username"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginBottom="20dp"
        android:text="@string/username"
        android:textSize="24sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@+id/tv_password"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed" />

    <EditText
        android:id="@+id/edit_username"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="16dp"
        app:layout_constraintBottom_toBottomOf="@+id/tv_username"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/tv_username"
        app:layout_constraintTop_toTopOf="@+id/tv_username" />

    <TextView
        android:id="@+id/tv_password"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginBottom="20dp"
        android:text="@string/password"
        android:textSize="24sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@+id/btn_login"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_username" />

    <EditText
        android:id="@+id/edit_password"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="@+id/tv_password"
        app:layout_constraintEnd_toEndOf="@+id/edit_username"
        app:layout_constraintStart_toStartOf="@+id/edit_username"
        app:layout_constraintTop_toTopOf="@+id/tv_password" />

    <Button
        android:id="@+id/btn_login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="16dp"
        android:text="@string/login"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_password" />

</androidx.constraintlayout.widget.ConstraintLayout>

从上面的代码可以看出,所有的业务逻辑以及UI更新都放在了Activity中,如果功能简单还比较容易维护,但随着功能越来越复杂,Activity的代码量会成倍的膨胀,并且业务逻辑会交织在一起,越来混乱。

为了解决上面的问题,MVP就推广开了。

MVP

 

View层:应于ActivityXML,只负责显示UI,只与Presenter层交互,与Model层没有耦

Presenter层 主要负责处理业务逻辑,通过接口调用Model层获取数据,并回调View

Model层主要负责网络请求,数据库处理,I/O的操作,即页面的数据来源

存在问题:

  1. Presenter层通过接口与View通信,实际上持有了View的引用
  2. 着业务逻辑的增加,页面的复杂度及页面的数量增加,会造成View的接口很庞大。

 登录示例代码

class MVPDemoActivity : AppCompatActivity(), LoginContact.LoginView {

    private lateinit var loginPresenter: LoginContact.LoginPresenter

    private lateinit var usernameEdit: EditText
    private lateinit var passwordEdit: EditText
    private lateinit var usernameText: TextView
    private lateinit var passwordText: TextView
    private lateinit var loginBtn: Button

    private var username: String = ""
    private var password: String = ""

    private var loadingDialog: LoadingDialog? = null

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

        loginPresenter = LoginPresenterImp(this)

        initViews()
    }

    private fun initViews() {

        usernameText = findViewById(R.id.tv_username)
        passwordText = findViewById(R.id.tv_password)

        usernameEdit = findViewById(R.id.edit_username)
        usernameEdit.afterTextChanged {
            username = it
        }

        passwordEdit = findViewById(R.id.edit_password)
        passwordEdit.afterTextChanged {
            password = it
        }

        loginBtn = findViewById(R.id.btn_login)
        loginBtn.setOnClickListener {
            loginPresenter.login(username, password)
        }
    }

    override fun showSuccess(userInfo: UserInfo) {
        runOnUiThread {
            usernameEdit.visibility = View.GONE
            passwordEdit.visibility = View.GONE
            loginBtn.visibility = View.GONE

            usernameText.text = getString(R.string.username) + userInfo.userName
            passwordText.text = getString(R.string.password) + userInfo.password

            showToast("登录成功")
        }
    }

    override fun showFail(message: String) {
        runOnUiThread {
            showToast(message)
        }
    }

    override fun showLoading(show: Boolean) {
        if (show) {
            loadingDialog = LoadingDialog(this)
            loadingDialog?.show()
        } else {
            loadingDialog?.cancel()
            loadingDialog = null
        }
    }
}
interface LoginContact {

    interface LoginModel {
        suspend fun login(username: String, password: String)
    }

    interface LoginView {
        fun showSuccess(userInfo: UserInfo)
        fun showFail(message: String)
        fun showLoading(show: Boolean)
    }

    interface LoginPresenter {
        fun login(username: String, password: String)
        fun onLoginSuccess(userInfo: UserInfo)
        fun onLoginFail(message: String)
    }
}
class LoginPresenterImp(private val loginView: LoginContact.LoginView) :
    LoginContact.LoginPresenter,
    CoroutineScope by MainScope() {

    private val loginModel: LoginContact.LoginModel

    init {
        loginModel = LoginModelImpl(this)
    }

    override fun login(username: String, password: String) {
        loginView.showLoading(true)
        launch(Dispatchers.IO) {
            loginModel.login(username, password)
        }
    }

    override fun onLoginSuccess(userInfo: UserInfo) {
        loginView.showLoading(false)
        loginView.showSuccess(userInfo)
    }

    override fun onLoginFail(message: String) {
        loginView.showLoading(false)
        loginView.showFail(message)
    }
}
class LoginModelImpl(private val loginPresenter: LoginContact.LoginPresenter) : LoginContact.LoginModel {

    override suspend fun login(username: String, password: String) {
        when (val loginResult = HttpUtil.login(username, password)) {
            is LoginResult.Success -> {
                loginPresenter.onLoginSuccess(loginResult.userInfo)
            }
            is LoginResult.Fail -> {
                loginPresenter.onLoginFail(loginResult.message)
            }
        }
    }
}

从上面的代码可以看出,Activity不再与Model模块有交互,只和Presenter进行接口调用,所有的业务逻辑、UI刷新逻辑都由Presenser去实现,但这样就导致了Presenter持有了Activity的引用,存在耦合关系;同时可以看到LoginContact中定义了大量的接口类,随着业务的增加,接口类会几何倍数的增加,维护起来会越发困难。

为了解决上面的问题,Google开始推广MVVM架构,并为其定制实现了Jetpack组件,为开发人员快速实现MVVM架构提供强大的支持。

MVVM

 

View: 对应于ActivityXML,负责View的绘制以及与用户交互

Model:主要负责网络请求,数据库处理,I/O的操作,即页面的数据来源

ViewModel: 负责完成ViewModel间的交互,负责业务逻辑

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向数据绑定Data-BindingView的变动,自动反映在 ViewModel,反之亦

MVVM的两种实现方案:

  1. 使用DataBinding库:使用DataBinding将数据Bean与xml直接绑定在一起,从而实现双向绑定,一方改变就会影响另一方,不需要手动更新UI;同时不再需要fingViewById或ButterKnife,DataBinding可以直接完成;还支持生命周期检测,不需担心界面销毁重建的问题;消除了大量的接口,降低耦合性。
  2. 使用AAC(LiveData):使用LiveData、LifeCycle、ViewModel、Room等组件,ViewModel中进行数据请求,通过LiveData发送到Activity中,而在Activity中通过LiveData的obseve接收ViewModel穿过来的数据、事件更新UI。

 Jetpack是Google为了解决Android架构问题而引入的,Google官方说的说法:“Jetpack是一套库、工具和指南,可以帮助开发者更轻松地编写应用程序。Jetpack中的组件可以帮助开发者遵循最佳做法、摆脱编写样板代码的工作并简化复杂的任务,以便他们能将精力集中放在业务所需的代码上。”

Jetpack学习icon-default.png?t=M85Bhttps://blog.csdn.net/gxlgxjhll/category_10836219.html

Google官方推荐的MVVM架构

 

登录示例,采用AAC方案(Google官方推荐)

class MVVMDemoActivity : AppCompatActivity() {

    private val viewModel: LoginViewModel by viewModels()

    private var loadingDialog: LoadingDialog? = null

    private lateinit var binding: ActivityMvvmBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMvvmBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.vm = viewModel

        initObserver()
    }

    @SuppressLint("SetTextI18n")
    private fun initObserver() {

        binding.editUsername.afterTextChanged {
            viewModel.userName = it
        }

        binding.editPassword.afterTextChanged {
            viewModel.password = it
        }

        viewModel.loginMessage.observe(this) { message ->
            showToast(message)
        }

        viewModel.userInfo.observe(this) { userInfo ->
            binding.editUsername.visibility = View.GONE
            binding.editPassword.visibility = View.GONE
            binding.btnLogin.visibility = View.GONE

            binding.tvUsername.text = getString(R.string.username) + userInfo.userName
            binding.tvPassword.text = getString(R.string.password) + userInfo.password
        }

        viewModel.isLoading.observe(this) { show ->
            showLoading(show)
        }
    }

}
  1. <--activity_mvvm.xml-->
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    
        <data>
    
            <variable
                name="vm"
                type="com.example.architecture.mvvm.LoginViewModel" />
        </data>
    
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <TextView
                android:id="@+id/tv_username"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                android:layout_marginBottom="20dp"
                android:text="@string/username"
                android:textSize="24sp"
                android:textStyle="bold"
                app:layout_constraintBottom_toTopOf="@+id/tv_password"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintVertical_chainStyle="packed" />
    
            <EditText
                android:id="@+id/edit_username"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginEnd="16dp"
                app:layout_constraintBottom_toBottomOf="@+id/tv_username"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@+id/tv_username"
                app:layout_constraintTop_toTopOf="@+id/tv_username" />
    
            <TextView
                android:id="@+id/tv_password"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                android:layout_marginBottom="20dp"
                android:text="@string/password"
                android:textSize="24sp"
                android:textStyle="bold"
                app:layout_constraintBottom_toTopOf="@+id/btn_login"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tv_username" />
    
            <EditText
                android:id="@+id/edit_password"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                app:layout_constraintBottom_toBottomOf="@+id/tv_password"
                app:layout_constraintEnd_toEndOf="@+id/edit_username"
                app:layout_constraintStart_toStartOf="@+id/edit_username"
                app:layout_constraintTop_toTopOf="@+id/tv_password" />
    
            <Button
                android:id="@+id/btn_login"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginHorizontal="16dp"
                android:onClick="@{() -> vm.login()}"
                android:text="@string/login"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tv_password" />
        </androidx.constraintlayout.widget.ConstraintLayout>
    
    </layout>
    class LoginViewModel : ViewModel() {
    
        private val repository = LoginRepository()
    
        var userName: String = ""
        var password: String = ""
    
        private val _loginMessage = MutableLiveData<String>()
        val loginMessage: LiveData<String> = _loginMessage
    
        private val _userInfo = MutableLiveData<UserInfo>()
        val userInfo: LiveData<UserInfo> = _userInfo
    
        private val _isLoading = MutableLiveData<Boolean>()
        val isLoading: LiveData<Boolean> = _isLoading
    
        fun login() {
            viewModelScope.launch(Dispatchers.IO) {
                _isLoading.postValue(true)
                when (val loginResult = repository.login(userName, password)) {
                    is LoginResult.Success -> {
                        _isLoading.postValue(false)
                        _userInfo.postValue(loginResult.userInfo)
                        _loginMessage.postValue("登录成功")
                    }
                    is LoginResult.Fail -> {
                        _isLoading.postValue(false)
                        _loginMessage.postValue(loginResult.message)
                    }
                }
            }
        }
    }
    class LoginRepository {
    
        suspend fun login(username: String, password: String): LoginResult {
            return HttpUtil.login(username, password)
        }
    }

    从上面的代码可以看到,Activity只进行UI更新,Repository负责数据获取,而ViewModel负责业务逻辑,通过LiveData将数据发送到Activity中,各组件已最小的持有其他引用。

  2. 但这种架构还是存在一定的问题,一是为了保证LiveData数据的更新只在ViewModel中,则必须定义一个私有和一个公开的属性,随着数据的增多,属性会成倍的增加;二是Activity中是通过LiveData的observe方法来更新UI的,而在ViewModel中LiveData数据的更新并没有约束,各种方法里都可能进行更新,使用混乱,难以管理。

  3. 而为了解决上面的问题,Google官方推出了最新的架构,我们姑且称之为MVI吧(我觉得这种说法并不准确)。

  4. MVI

  5. 要了解MVI架构,首先要先了解两个架构原则。
  6. 单一数据源

  7. Goolge官方介绍:

在应用中定义新数据类型时,您应为其分配单一数据源 (SSOT)。SSOT 是该数据的所有者,而且只有此 SSOT 可以修改或转变该数据。为了实现这一点,SSOT 会以不可变类型公开数据;而且为了修改数据,SSOT 会公开函数或接收其他类型可以调用的事件。

此模式具有多种优势:

  • 将对特定类型数据的所有更改集中到一处。
  • 保护数据,防止其他类型篡改此数据。
  • 更易于跟踪对数据的更改。因此,更容易发现 bug。

在离线优先应用中,应用数据的单一数据源通常是数据库。在其他某些情况下,单一数据源可以是 ViewModel 甚至是界面。

单向数据流 

单一数据源原则常常与单向数据流 (UDF) 模式一起使用。在 UDF 中,状态仅朝一个方向流动。修改数据的事件朝相反方向流动。

在 Android 中,状态或数据通常从分区层次结构中较高的分区类型流向较低的分区类型。事件通常在分区层次结构中较低的分区类型触发,直到其到达 SSOT 的相应数据类型。例如,应用数据通常从数据源流向界面。用户事件(例如按钮按下操作)从界面流向 SSOT,在 SSOT 中应用数据被修改并以不可变类型公开。

此模式可以更好地保证数据一致性,不易出错、更易于调试,并且具备 SSOT 模式的所有优势。

然后我们再看一下Google官方推荐的架构

基于上一部分提到的常见架构原则,每个应用应至少有两个层:

  • 界面层 - 在屏幕上显示应用数据。
  • 数据层 - 包含应用的业务逻辑并公开应用数据。

您可以额外添加一个名为“网域层”的架构层,以简化和重复使用界面层与数据层之间的交互。

 

界面层

界面层(或呈现层)的作用是在屏幕上显示应用数据。每当数据发生变化时,无论是因为用户互动(例如按了某个按钮),还是因为外部输入(例如网络响应),界面都应随之更新,以反映这些变化。

界面层由以下两部分组成:

  • 在屏幕上呈现数据的界面元素。您可以使用 View 或 Jetpack Compose 函数构建这些元素。
  • 用于存储数据、向界面提供数据以及处理逻辑的状态容器(如 ViewModel 类)。

 

数据层

应用的数据层包含业务逻辑。业务逻辑决定应用的价值,它包含决定应用如何创建、存储和更改数据的规则。

数据层由多个代码库组成,其中每个代码库可包含零到多个数据源。您应该为应用中处理的每种不同类型的数据分别创建一个存储库类。例如,您可以为与电影相关的数据创建一个 MoviesRepository 类,或者为与付款相关的数据创建一个 PaymentsRepository 类。
 

代码库类负责以下任务:

  • 向应用的其余部分公开数据。
  • 集中处理数据变化。
  • 解决多个数据源之间的冲突。
  • 对应用其余部分的数据源进行抽象化处理。
  • 包含业务逻辑。

每个数据源类应仅负责处理一个数据源,数据源可以是文件、网络来源或本地数据库。数据源类是应用与数据操作系统之间的桥梁。

根据官方推荐的架构,转化成我们实际开发架构,可以用下图来表示:

 

数据是从界面发出的事件(意图),即 MVI 中 I(Intent),ViewModel根据业务对这个Intent进行处理(数据请求、业务逻辑),将处理的数据封装成State,即UI状态,发送给View进行UI更新。

而这个State通常是UI所有元素显示状态的集合,通过其对UI进行统一的管理。

登录示例代码

class MVIDemoActivity : AppCompatActivity() {

    private val viewModel: LoginViewModel by viewModels()

    private lateinit var binding: ActivityMviBinding

    private var loadingDialog: LoadingDialog? = null

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

        binding = ActivityMviBinding.inflate(layoutInflater)
        binding.vm = viewModel

        setContentView(binding.root)

        initObserver()
    }

    @SuppressLint("SetTextI18n")
    private fun initObserver() {

        binding.editUsername.afterTextChanged {
            viewModel.userName = it
        }

        binding.editPassword.afterTextChanged {
            viewModel.password = it
        }

        lifecycleScope.launch {
            this@MVIDemoActivity.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.loginStateFlow.collect { loginState ->
                    loginState.userInfo?.let { userInfo ->
                        binding.tvUsername.text = getString(R.string.username) + userInfo.userName
                        binding.tvPassword.text = getString(R.string.password) + userInfo.password
                    }

                    if (loginState.isLogin) {
                        binding.editUsername.visibility = View.GONE
                        binding.editPassword.visibility = View.GONE
                        binding.btnLogin.visibility = View.GONE
                    } else {
                        binding.editUsername.visibility = View.VISIBLE
                        binding.editPassword.visibility = View.VISIBLE
                        binding.btnLogin.visibility = View.VISIBLE
                    }

                    if (loginState.isLoading) {
                        loadingDialog = LoadingDialog(this@MVIDemoActivity)
                        loadingDialog?.show()
                    } else {
                        loadingDialog?.cancel()
                        loadingDialog = null
                    }
                }
            }
        }

        lifecycleScope.launch {
            this@MVIDemoActivity.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.loginEventFlow.collect { event ->
                    when (event) {
                        is LoginViewModel.LoginEvent.ToastEvent -> {
                            showToast(event.message, event.isShort)
                        }
                    }
                }
            }
        }
    }

}
class LoginViewModel : ViewModel() {

    private val repository = LoginRepository()

    var userName: String = ""
    var password: String = ""

    private val _loginStateFlow = MutableStateFlow(LoginState())
    private val _loginEventFlow = MutableSharedFlow<LoginEvent>()

    val loginStateFlow: StateFlow<LoginState> = _loginStateFlow
    val loginEventFlow: SharedFlow<LoginEvent> = _loginEventFlow

    fun login() {
        viewModelScope.launch(Dispatchers.IO) {
            _loginStateFlow.update { loginState ->
                loginState.copy(isLoading = true)
            }
            when (val loginResult = repository.login(userName, password)) {
                is LoginResult.Success -> {
                    _loginStateFlow.update { loginState ->
                        loginState.copy(
                            userInfo = loginResult.userInfo,
                            isLoading = false,
                            isLogin = true
                        )
                    }

                    _loginEventFlow.emit(LoginEvent.ToastEvent("登录成功"))
                }
                is LoginResult.Fail -> {
                    _loginStateFlow.update { loginState ->
                        loginState.copy(isLoading = false, isLogin = false)
                    }
                    _loginEventFlow.emit(LoginEvent.ToastEvent(loginResult.message))
                }
            }
        }
    }

    data class LoginState(
        val userInfo: UserInfo? = null,
        val isLoading: Boolean = false,
        val isLogin: Boolean = false
    )

    sealed class LoginEvent {
        class ToastEvent(val message: String, val isShort: Boolean = true) : LoginEvent()
    }

}

从上面的代码可以看出,State对UI的状态进行了约束,所有出口都由其提供,并且其只和UI显示的元素相关,和业务逻辑解耦。

 AndroidMVC、MVP、MVVM、MVI架构示例icon-default.png?t=M85Bhttps://download.csdn.net/download/gxlgxjhll/86937904