MVVM架构模式详解:从原理到Android实战
MVVM架构模式详解:从原理到Android实战
MVVM(Model-View-ViewModel)是一种先进的软件架构模式,旨在将用户界面(UI)开发与应用程序的业务逻辑和数据模型分离。这种模式通过数据绑定机制实现视图和数据的自动同步,大大提高了代码的可维护性和可测试性。本文将主要用JetPack的ViewModel,LiveData,DataBinding进行MVVM架构的实现。
MVVM的核心组成
MVVM模式将应用程序分为三个核心部分,每个部分都有明确的职责划分:
Model(模型) 代表应用程序的数据模型和业务逻辑,负责处理数据的获取、验证、存储和操作。它不直接与UI层交互,只暴露接口供ViewModel调用。
View(视图) 是用户所看到和操作的界面,负责展示数据和处理用户交互。在Android开发中,View通常是Activity、Fragment。
ViewModel(视图模型) 是View和Model之间的桥梁,负责将Model中的数据转换为View可以直接使用的形式。它通过数据绑定机制将数据与View进行绑定,使得数据的变化可以自动反映在View上。
MVVM的各层引用关系
理想的MVVM引用关系是单向依赖的层次结构:
View → ViewModel → Model
View持有ViewModel的引用,观察ViewModel中的LiveData等数据源,但只负责展示数据,不直接操作Model。
ViewModel持有Model/Repository的引用,负责调用数据层并将结果转换为UI状态。
Model完全独立,不持有ViewModel或View的引用,只提供数据访问接口。
这种单向依赖关系确保了关注点的清晰分离,大大降低了各层之间的耦合度。
MVVM的关键特性:双向数据绑定
在MVVM开始之前先大致了解一下单向绑定与双向绑定。
- 单向绑定是指,UI 只是显示数据,不能反向修改 ViewModel 里的内容。举个情境例子:
假设你的 ViewModel:
class UserViewModel : ViewModel() {val username = MutableLiveData("Batman")
}
布局文件:
<EditTextandroid:layout_width="match_parent"android:layout_height="wrap_content"android:text="@{vm.username}" />
此时数据流方向是ViewModel.username -----> EditText.text
也就是:当我们执行:viewModel.username.value = "Bruce Wayne"
,EditText
会立刻显示 "Bruce Wayne"
但当用户在 EditText
输入 "Joker"
,viewModel.username
不会变,仍然是 "Bruce Wayne"
- 双向数据绑定
双向绑定定是MVVM架构的核心特性。它允许ViewModel中的数据变化自动反映在View上,同时用户在View上的操作也会自动更新ViewModel中的数据。
在Android中,这种双向绑定可以通过DataBinding库实现:
<EditTextandroid:text="@={viewmodel.username}" />
当用户编辑EditText时,ViewModel中的username值会自动更新;当ViewModel中的username值变化时,EditText的显示内容也会自动更新。
MVVM在Android中的实现
Google的Android Jetpack组件库为MVVM提供了官方支持,包括以下几个关键组件:
ViewModel 类旨在以生命周期感知的方式存储和管理UI相关的数据,在配置变更(如屏幕旋转)时保持数据一致性。
LiveData 是一种可观察的数据持有者类,具有生命周期感知能力,确保只在活跃的观察者中通知更新,避免内存泄漏。
Data Binding 库允许在布局文件中直接将UI组件绑定到数据源,减少样板代码。
下面是一个完整的Android MVVM实现示例:
- 首先是Model层,这里简单模拟一下登录过程,实际会涉及到网络请求或者数据库操作
class LoginModel() {fun Check(account : String, password : String) : Boolean{if(account == "12345" && password == "67890") {return true}else {return false}}
}
- 接着看ViewModel层,ViewModel层就需要用到LiveData进行观察了。其中result用来检验账号密码是否正确,需要在Activity中被观察,正确进行正确逻辑的调用,错误就调用错误逻辑,所以要用
LiveData
容器。**而account和password使用LiveData并不是给 Activity 用来观察的, 而是给 DataBinding 框架 用来实现 双向绑定 的。如果不使用LiveData仅用普通变量类型声明,在用户编辑EditText之后EditText上的数据无法同步到该变量上。**同时,这里之所以要多写一个result
的LiveData是为了避免给外部传一个可变的LiveData从而修改这个LiveData的值,破坏内部的封装性
class LoginViewModel : ViewModel(){val account = MutableLiveData<String>()val password = MutableLiveData<String>()private val _result = MutableLiveData<Boolean>()val result: LiveData<Boolean> get() = _resultprivate val loginModel = LoginModel()fun Check () {val acc = account?.value ?:""val psd = password?.value?:""_result.value = loginModel.Check(acc, psd)}
}
- 最后看VIew层的Activity和对应的xml布局文件。由于MVVM框架出色的解耦机制并且xml分担了一些代码逻辑,Activity中代码量就很少了。这里只对用户点击按钮后返回的是否正确的结果进行观察就可。
class MainActivity : AppCompatActivity() {lateinit var binding: ActivityMainBindinglateinit var viewModel : LoginViewModeloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()binding = DataBindingUtil.setContentView(this, R.layout.activity_main)binding.lifecycleOwner = thisviewModel = ViewModelProvider(this).get(LoginViewModel::class.java)binding.vm = viewModelviewModel.result.observe(this) { successed -> if(successed) onLoginSucessed() else onLoginFailure() }}fun onLoginSucessed(){Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show()}fun onLoginFailure() {Toast.makeText(this, "登录失败", Toast.LENGTH_SHORT).show()}
}
xml文件中对按钮的点击事件进行绑定,当用户点击按钮后就执行ViewModel层的Check()
方法,同时对输入框中的account
和password
进行绑定,注意,这里绑定数据用的是@={}
,多了一个=
,表明是双向绑定,原因在上面说了。
<?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"xmlns:tools="http://schemas.android.com/tools"><data><variablename="vm"type="com.example.mvvmlogin.LoginViewModel" /><variablename="account"type="String" /><variablename="password"type="String" /></data><androidx.constraintlayout.widget.ConstraintLayoutandroid:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><com.google.android.material.textfield.TextInputLayoutandroid:id="@+id/account_layout"app:layout_constraintTop_toTopOf="parent"android:layout_marginTop="50dp"android:layout_width="match_parent"android:layout_height="80dp"><com.google.android.material.textfield.TextInputEditTextandroid:id="@+id/account_text"android:hint="输入账号"android:text="@={vm.account}"android:layout_width="match_parent"android:layout_height="match_parent" /></com.google.android.material.textfield.TextInputLayout><com.google.android.material.textfield.TextInputLayoutandroid:id="@+id/password_layout"app:layout_constraintTop_toBottomOf="@id/account_layout"android:layout_marginTop="10dp"android:layout_width="match_parent"android:layout_height="80dp"><com.google.android.material.textfield.TextInputEditTextandroid:id="@+id/password_text"android:hint="输入密码"android:text="@={vm.password}"android:layout_width="match_parent"android:layout_height="match_parent" /></com.google.android.material.textfield.TextInputLayout><Buttonandroid:id="@+id/button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="登录"app:layout_constraintTop_toBottomOf="@id/password_layout"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"android:layout_marginTop="8dp"android:backgroundTint="@color/send_color"android:onClick="@{() -> vm.Check()}"/></androidx.constraintlayout.widget.ConstraintLayout>
</layout>
这样我们就完成了这个MVVM架构的小demo
总结
总体来看,MVVM 相较于 MVP 带来了显著的简化与优化。
在实际开发中,我们在 Activity 中不再需要频繁声明各种 UI 控件(如账号、密码输入框)或手动编写 setText()
等数据同步逻辑,MVVM 通过 数据绑定与观察机制,极大减少了繁琐的点击事件处理与视图更新代码。同时,MVVM 彻底避免了 MVP 架构中常见的 “接口爆炸” 问题,不再需要为每一层额外定义成堆的接口文件。
最重要的是,它在保持代码简洁的同时,还解决了 P 层与 V 层相互持有引用 所带来的强耦合问题,实现了更自然、更彻底的解耦,也就更加贯彻了面向对象六大设计原则。