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


    随着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层存在耦合,存在互相依赖,违背了迪米特原则

    以下都是以登录功能为例

    1. class MVCDemoActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    2. private lateinit var usernameEdit: EditText
    3. private lateinit var passwordEdit: EditText
    4. private lateinit var usernameText: TextView
    5. private lateinit var passwordText: TextView
    6. private lateinit var loginBtn: Button
    7. private var username: String = ""
    8. private var password: String = ""
    9. private var loadingDialog: LoadingDialog? = null
    10. override fun onCreate(savedInstanceState: Bundle?) {
    11. super.onCreate(savedInstanceState)
    12. setContentView(R.layout.activity_mvc)
    13. initViews()
    14. }
    15. private fun initViews() {
    16. usernameText = findViewById(R.id.tv_username)
    17. passwordText = findViewById(R.id.tv_password)
    18. usernameEdit = findViewById(R.id.edit_username)
    19. usernameEdit.afterTextChanged {
    20. username = it
    21. }
    22. passwordEdit = findViewById(R.id.edit_password)
    23. passwordEdit.afterTextChanged {
    24. password = it
    25. }
    26. loginBtn = findViewById(R.id.btn_login)
    27. loginBtn.setOnClickListener {
    28. doLogin()
    29. }
    30. }
    31. @SuppressLint("SetTextI18n")
    32. private fun doLogin() {
    33. showLoading(true)
    34. launch(Dispatchers.IO) {
    35. when (val loginResult = HttpUtil.login(username, password)) {
    36. is LoginResult.Success -> {
    37. runOnUiThread {
    38. usernameEdit.visibility = View.GONE
    39. passwordEdit.visibility = View.GONE
    40. loginBtn.visibility = View.GONE
    41. usernameText.text =
    42. getString(R.string.username) + loginResult.userInfo.userName
    43. passwordText.text =
    44. getString(R.string.password) + loginResult.userInfo.password
    45. showLoading(false)
    46. showToast("登录成功")
    47. }
    48. }
    49. is LoginResult.Fail -> {
    50. runOnUiThread {
    51. showLoading(false)
    52. showToast(loginResult.message)
    53. }
    54. }
    55. }
    56. }
    57. }
    58. }
    1. <--activity_mvc.xml-->
    2. "1.0" encoding="utf-8"?>
    3. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    4. xmlns:app="http://schemas.android.com/apk/res-auto"
    5. android:layout_width="match_parent"
    6. android:layout_height="match_parent">
    7. <TextView
    8. android:id="@+id/tv_username"
    9. android:layout_width="wrap_content"
    10. android:layout_height="wrap_content"
    11. android:layout_marginStart="16dp"
    12. android:layout_marginBottom="20dp"
    13. android:text="@string/username"
    14. android:textSize="24sp"
    15. android:textStyle="bold"
    16. app:layout_constraintBottom_toTopOf="@+id/tv_password"
    17. app:layout_constraintStart_toStartOf="parent"
    18. app:layout_constraintTop_toTopOf="parent"
    19. app:layout_constraintVertical_chainStyle="packed" />
    20. <EditText
    21. android:id="@+id/edit_username"
    22. android:layout_width="0dp"
    23. android:layout_height="wrap_content"
    24. android:layout_marginStart="8dp"
    25. android:layout_marginEnd="16dp"
    26. app:layout_constraintBottom_toBottomOf="@+id/tv_username"
    27. app:layout_constraintEnd_toEndOf="parent"
    28. app:layout_constraintStart_toEndOf="@+id/tv_username"
    29. app:layout_constraintTop_toTopOf="@+id/tv_username" />
    30. <TextView
    31. android:id="@+id/tv_password"
    32. android:layout_width="wrap_content"
    33. android:layout_height="wrap_content"
    34. android:layout_marginStart="16dp"
    35. android:layout_marginBottom="20dp"
    36. android:text="@string/password"
    37. android:textSize="24sp"
    38. android:textStyle="bold"
    39. app:layout_constraintBottom_toTopOf="@+id/btn_login"
    40. app:layout_constraintStart_toStartOf="parent"
    41. app:layout_constraintTop_toBottomOf="@+id/tv_username" />
    42. <EditText
    43. android:id="@+id/edit_password"
    44. android:layout_width="0dp"
    45. android:layout_height="wrap_content"
    46. app:layout_constraintBottom_toBottomOf="@+id/tv_password"
    47. app:layout_constraintEnd_toEndOf="@+id/edit_username"
    48. app:layout_constraintStart_toStartOf="@+id/edit_username"
    49. app:layout_constraintTop_toTopOf="@+id/tv_password" />
    50. <Button
    51. android:id="@+id/btn_login"
    52. android:layout_width="match_parent"
    53. android:layout_height="wrap_content"
    54. android:layout_marginHorizontal="16dp"
    55. android:text="@string/login"
    56. app:layout_constraintBottom_toBottomOf="parent"
    57. app:layout_constraintTop_toBottomOf="@+id/tv_password" />
    58. 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的接口很庞大。

     登录示例代码

    1. class MVPDemoActivity : AppCompatActivity(), LoginContact.LoginView {
    2. private lateinit var loginPresenter: LoginContact.LoginPresenter
    3. private lateinit var usernameEdit: EditText
    4. private lateinit var passwordEdit: EditText
    5. private lateinit var usernameText: TextView
    6. private lateinit var passwordText: TextView
    7. private lateinit var loginBtn: Button
    8. private var username: String = ""
    9. private var password: String = ""
    10. private var loadingDialog: LoadingDialog? = null
    11. override fun onCreate(savedInstanceState: Bundle?) {
    12. super.onCreate(savedInstanceState)
    13. setContentView(R.layout.activity_mvp)
    14. loginPresenter = LoginPresenterImp(this)
    15. initViews()
    16. }
    17. private fun initViews() {
    18. usernameText = findViewById(R.id.tv_username)
    19. passwordText = findViewById(R.id.tv_password)
    20. usernameEdit = findViewById(R.id.edit_username)
    21. usernameEdit.afterTextChanged {
    22. username = it
    23. }
    24. passwordEdit = findViewById(R.id.edit_password)
    25. passwordEdit.afterTextChanged {
    26. password = it
    27. }
    28. loginBtn = findViewById(R.id.btn_login)
    29. loginBtn.setOnClickListener {
    30. loginPresenter.login(username, password)
    31. }
    32. }
    33. override fun showSuccess(userInfo: UserInfo) {
    34. runOnUiThread {
    35. usernameEdit.visibility = View.GONE
    36. passwordEdit.visibility = View.GONE
    37. loginBtn.visibility = View.GONE
    38. usernameText.text = getString(R.string.username) + userInfo.userName
    39. passwordText.text = getString(R.string.password) + userInfo.password
    40. showToast("登录成功")
    41. }
    42. }
    43. override fun showFail(message: String) {
    44. runOnUiThread {
    45. showToast(message)
    46. }
    47. }
    48. override fun showLoading(show: Boolean) {
    49. if (show) {
    50. loadingDialog = LoadingDialog(this)
    51. loadingDialog?.show()
    52. } else {
    53. loadingDialog?.cancel()
    54. loadingDialog = null
    55. }
    56. }
    57. }
    1. interface LoginContact {
    2. interface LoginModel {
    3. suspend fun login(username: String, password: String)
    4. }
    5. interface LoginView {
    6. fun showSuccess(userInfo: UserInfo)
    7. fun showFail(message: String)
    8. fun showLoading(show: Boolean)
    9. }
    10. interface LoginPresenter {
    11. fun login(username: String, password: String)
    12. fun onLoginSuccess(userInfo: UserInfo)
    13. fun onLoginFail(message: String)
    14. }
    15. }
    1. class LoginPresenterImp(private val loginView: LoginContact.LoginView) :
    2. LoginContact.LoginPresenter,
    3. CoroutineScope by MainScope() {
    4. private val loginModel: LoginContact.LoginModel
    5. init {
    6. loginModel = LoginModelImpl(this)
    7. }
    8. override fun login(username: String, password: String) {
    9. loginView.showLoading(true)
    10. launch(Dispatchers.IO) {
    11. loginModel.login(username, password)
    12. }
    13. }
    14. override fun onLoginSuccess(userInfo: UserInfo) {
    15. loginView.showLoading(false)
    16. loginView.showSuccess(userInfo)
    17. }
    18. override fun onLoginFail(message: String) {
    19. loginView.showLoading(false)
    20. loginView.showFail(message)
    21. }
    22. }
    1. class LoginModelImpl(private val loginPresenter: LoginContact.LoginPresenter) : LoginContact.LoginModel {
    2. override suspend fun login(username: String, password: String) {
    3. when (val loginResult = HttpUtil.login(username, password)) {
    4. is LoginResult.Success -> {
    5. loginPresenter.onLoginSuccess(loginResult.userInfo)
    6. }
    7. is LoginResult.Fail -> {
    8. loginPresenter.onLoginFail(loginResult.message)
    9. }
    10. }
    11. }
    12. }

    从上面的代码可以看出,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学习https://blog.csdn.net/gxlgxjhll/category_10836219.html

    Google官方推荐的MVVM架构

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

    1. class MVVMDemoActivity : AppCompatActivity() {
    2. private val viewModel: LoginViewModel by viewModels()
    3. private var loadingDialog: LoadingDialog? = null
    4. private lateinit var binding: ActivityMvvmBinding
    5. override fun onCreate(savedInstanceState: Bundle?) {
    6. super.onCreate(savedInstanceState)
    7. binding = ActivityMvvmBinding.inflate(layoutInflater)
    8. setContentView(binding.root)
    9. binding.vm = viewModel
    10. initObserver()
    11. }
    12. @SuppressLint("SetTextI18n")
    13. private fun initObserver() {
    14. binding.editUsername.afterTextChanged {
    15. viewModel.userName = it
    16. }
    17. binding.editPassword.afterTextChanged {
    18. viewModel.password = it
    19. }
    20. viewModel.loginMessage.observe(this) { message ->
    21. showToast(message)
    22. }
    23. viewModel.userInfo.observe(this) { userInfo ->
    24. binding.editUsername.visibility = View.GONE
    25. binding.editPassword.visibility = View.GONE
    26. binding.btnLogin.visibility = View.GONE
    27. binding.tvUsername.text = getString(R.string.username) + userInfo.userName
    28. binding.tvPassword.text = getString(R.string.password) + userInfo.password
    29. }
    30. viewModel.isLoading.observe(this) { show ->
    31. showLoading(show)
    32. }
    33. }
    34. }
    1. <--activity_mvvm.xml-->
    2. "1.0" encoding="utf-8"?>
    3. <layout xmlns:android="http://schemas.android.com/apk/res/android"
    4. xmlns:app="http://schemas.android.com/apk/res-auto">
    5. <data>
    6. <variable
    7. name="vm"
    8. type="com.example.architecture.mvvm.LoginViewModel" />
    9. data>
    10. <androidx.constraintlayout.widget.ConstraintLayout
    11. android:layout_width="match_parent"
    12. android:layout_height="match_parent">
    13. <TextView
    14. android:id="@+id/tv_username"
    15. android:layout_width="wrap_content"
    16. android:layout_height="wrap_content"
    17. android:layout_marginStart="16dp"
    18. android:layout_marginBottom="20dp"
    19. android:text="@string/username"
    20. android:textSize="24sp"
    21. android:textStyle="bold"
    22. app:layout_constraintBottom_toTopOf="@+id/tv_password"
    23. app:layout_constraintStart_toStartOf="parent"
    24. app:layout_constraintTop_toTopOf="parent"
    25. app:layout_constraintVertical_chainStyle="packed" />
    26. <EditText
    27. android:id="@+id/edit_username"
    28. android:layout_width="0dp"
    29. android:layout_height="wrap_content"
    30. android:layout_marginStart="8dp"
    31. android:layout_marginEnd="16dp"
    32. app:layout_constraintBottom_toBottomOf="@+id/tv_username"
    33. app:layout_constraintEnd_toEndOf="parent"
    34. app:layout_constraintStart_toEndOf="@+id/tv_username"
    35. app:layout_constraintTop_toTopOf="@+id/tv_username" />
    36. <TextView
    37. android:id="@+id/tv_password"
    38. android:layout_width="wrap_content"
    39. android:layout_height="wrap_content"
    40. android:layout_marginStart="16dp"
    41. android:layout_marginBottom="20dp"
    42. android:text="@string/password"
    43. android:textSize="24sp"
    44. android:textStyle="bold"
    45. app:layout_constraintBottom_toTopOf="@+id/btn_login"
    46. app:layout_constraintStart_toStartOf="parent"
    47. app:layout_constraintTop_toBottomOf="@+id/tv_username" />
    48. <EditText
    49. android:id="@+id/edit_password"
    50. android:layout_width="0dp"
    51. android:layout_height="wrap_content"
    52. app:layout_constraintBottom_toBottomOf="@+id/tv_password"
    53. app:layout_constraintEnd_toEndOf="@+id/edit_username"
    54. app:layout_constraintStart_toStartOf="@+id/edit_username"
    55. app:layout_constraintTop_toTopOf="@+id/tv_password" />
    56. <Button
    57. android:id="@+id/btn_login"
    58. android:layout_width="match_parent"
    59. android:layout_height="wrap_content"
    60. android:layout_marginHorizontal="16dp"
    61. android:onClick="@{() -> vm.login()}"
    62. android:text="@string/login"
    63. app:layout_constraintBottom_toBottomOf="parent"
    64. app:layout_constraintTop_toBottomOf="@+id/tv_password" />
    65. androidx.constraintlayout.widget.ConstraintLayout>
    66. layout>
    1. class LoginViewModel : ViewModel() {
    2. private val repository = LoginRepository()
    3. var userName: String = ""
    4. var password: String = ""
    5. private val _loginMessage = MutableLiveData()
    6. val loginMessage: LiveData = _loginMessage
    7. private val _userInfo = MutableLiveData()
    8. val userInfo: LiveData = _userInfo
    9. private val _isLoading = MutableLiveData<Boolean>()
    10. val isLoading: LiveData<Boolean> = _isLoading
    11. fun login() {
    12. viewModelScope.launch(Dispatchers.IO) {
    13. _isLoading.postValue(true)
    14. when (val loginResult = repository.login(userName, password)) {
    15. is LoginResult.Success -> {
    16. _isLoading.postValue(false)
    17. _userInfo.postValue(loginResult.userInfo)
    18. _loginMessage.postValue("登录成功")
    19. }
    20. is LoginResult.Fail -> {
    21. _isLoading.postValue(false)
    22. _loginMessage.postValue(loginResult.message)
    23. }
    24. }
    25. }
    26. }
    27. }
    1. class LoginRepository {
    2. suspend fun login(username: String, password: String): LoginResult {
    3. return HttpUtil.login(username, password)
    4. }
    5. }

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

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

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

    MVI

    要了解MVI架构,首先要先了解两个架构原则。

    单一数据源

    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进行统一的管理。

    登录示例代码

    1. class MVIDemoActivity : AppCompatActivity() {
    2. private val viewModel: LoginViewModel by viewModels()
    3. private lateinit var binding: ActivityMviBinding
    4. private var loadingDialog: LoadingDialog? = null
    5. override fun onCreate(savedInstanceState: Bundle?) {
    6. super.onCreate(savedInstanceState)
    7. binding = ActivityMviBinding.inflate(layoutInflater)
    8. binding.vm = viewModel
    9. setContentView(binding.root)
    10. initObserver()
    11. }
    12. @SuppressLint("SetTextI18n")
    13. private fun initObserver() {
    14. binding.editUsername.afterTextChanged {
    15. viewModel.userName = it
    16. }
    17. binding.editPassword.afterTextChanged {
    18. viewModel.password = it
    19. }
    20. lifecycleScope.launch {
    21. this@MVIDemoActivity.repeatOnLifecycle(Lifecycle.State.STARTED) {
    22. viewModel.loginStateFlow.collect { loginState ->
    23. loginState.userInfo?.let { userInfo ->
    24. binding.tvUsername.text = getString(R.string.username) + userInfo.userName
    25. binding.tvPassword.text = getString(R.string.password) + userInfo.password
    26. }
    27. if (loginState.isLogin) {
    28. binding.editUsername.visibility = View.GONE
    29. binding.editPassword.visibility = View.GONE
    30. binding.btnLogin.visibility = View.GONE
    31. } else {
    32. binding.editUsername.visibility = View.VISIBLE
    33. binding.editPassword.visibility = View.VISIBLE
    34. binding.btnLogin.visibility = View.VISIBLE
    35. }
    36. if (loginState.isLoading) {
    37. loadingDialog = LoadingDialog(this@MVIDemoActivity)
    38. loadingDialog?.show()
    39. } else {
    40. loadingDialog?.cancel()
    41. loadingDialog = null
    42. }
    43. }
    44. }
    45. }
    46. lifecycleScope.launch {
    47. this@MVIDemoActivity.repeatOnLifecycle(Lifecycle.State.STARTED) {
    48. viewModel.loginEventFlow.collect { event ->
    49. when (event) {
    50. is LoginViewModel.LoginEvent.ToastEvent -> {
    51. showToast(event.message, event.isShort)
    52. }
    53. }
    54. }
    55. }
    56. }
    57. }
    58. }
    1. class LoginViewModel : ViewModel() {
    2. private val repository = LoginRepository()
    3. var userName: String = ""
    4. var password: String = ""
    5. private val _loginStateFlow = MutableStateFlow(LoginState())
    6. private val _loginEventFlow = MutableSharedFlow()
    7. val loginStateFlow: StateFlow = _loginStateFlow
    8. val loginEventFlow: SharedFlow = _loginEventFlow
    9. fun login() {
    10. viewModelScope.launch(Dispatchers.IO) {
    11. _loginStateFlow.update { loginState ->
    12. loginState.copy(isLoading = true)
    13. }
    14. when (val loginResult = repository.login(userName, password)) {
    15. is LoginResult.Success -> {
    16. _loginStateFlow.update { loginState ->
    17. loginState.copy(
    18. userInfo = loginResult.userInfo,
    19. isLoading = false,
    20. isLogin = true
    21. )
    22. }
    23. _loginEventFlow.emit(LoginEvent.ToastEvent("登录成功"))
    24. }
    25. is LoginResult.Fail -> {
    26. _loginStateFlow.update { loginState ->
    27. loginState.copy(isLoading = false, isLogin = false)
    28. }
    29. _loginEventFlow.emit(LoginEvent.ToastEvent(loginResult.message))
    30. }
    31. }
    32. }
    33. }
    34. data class LoginState(
    35. val userInfo: UserInfo? = null,
    36. val isLoading: Boolean = false,
    37. val isLogin: Boolean = false
    38. )
    39. sealed class LoginEvent {
    40. class ToastEvent(val message: String, val isShort: Boolean = true) : LoginEvent()
    41. }
    42. }

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

     AndroidMVC、MVP、MVVM、MVI架构示例https://download.csdn.net/download/gxlgxjhll/86937904

  • 相关阅读:
    java汽车租赁超时罚款系统springboot+vue-前后端分离-e36ht
    C语言速成笔记 —— 考点详解 & 知识点图解
    TM-30 计算软件 (Excel图表显示版本)
    C++实现的动态规划求解分解为若干素数之和的方案总数
    Java中的volatile为什么不能保证原子性
    深入理解Numpy中sum求和的axis参数
    【1day】用友时空KSOA平台 任意文件上传漏洞学习
    【知识图谱论文】AnyBURL:用于知识图完成的随时自下而上的规则学习
    Zookeeper:Mac通过Docker安装Zookeeper集群
    动手学深度学习7.2 使用块的网络(VGG)-笔记&练习(PyTorch)
  • 原文地址:https://blog.csdn.net/gxlgxjhll/article/details/127773241