写在前面:现在随便出去面试Android APP相关的工作,面试官基本上都会提问APP架构相关的问题,用Java、kotlin写APP的话,其实就三种架构MVC、MVP、MVVM,MVC和MVP高度相似,区别不大,MVVM则不同,引入了新的JetPack工具:ViewModel、LiveData、DataBinding,导入了“View和数据双向绑定的概念”。搞Android APP的必须把这三种架构搞清楚、搞透彻。
正式开始码字前,我们要先清楚两个词:高内聚、低耦合。相信计算机和相关专业的同学肯定都在老师的嘴里听说过这两个词,我们写Android APP为什么要用架构,其实就是为了实现这两个词代表的含义。
高内聚:Java、kotlin都是面向对象编程的语言,高内聚就是要求每个类的功能精简,类里面的每个方法精简。最好就是每个类、每个方法相对独立,对外的依赖少,功能明确。
低耦合:追求高内聚的结果必然就是低耦合,低耦合说的就是代码的不同功能模块之间,没有绝对的依赖关系,不是谁离开谁就运行不下去那种。不能出现搭积木拆了一块剩下的全塌了这种情况。
高内聚、低耦合的目的最终就是为了项目代码方便维护,后续方便功能扩展。代码架构就是为了更方便的实现高内聚、低耦合目标的代码组成方式,使用了之后你的代码就像用收纳盒规整过一样。当然高内聚、低耦合只是一个指导思想,在实际开发中我们不可能处处都能完美做到,但必须作为一个追求的目标。
下面依次介绍MVC、MVP、MVVM,搞清楚它们之间的联系和区别,以及为什么会演变出新的架构。
一、MVC
MVC主要分为三个部分,Model数据层、View视图层、Controller控制层,Model和View不直接联系,它们之间以Controller作为纽带。下面举一个简单的例子进行说明。
1.1 Model:负责数据处理
用Android Studio创建一个普通的项目。创建一个UserModel类用来模拟处理数据,当然写得很简单。
// Model - 保存用户数据
public class UserModel {
private String name;
// 设置用户名字
public void setName(String name) {
this.name = name;
}
// 获取用户名字
public String getName() {
return this.name;
}
}
1.2 View:负责显示UI、更新UI和接收用户输入事件
View视图层简单来说就是Activity、Fragment、Dialog这些负责承载视图的组件,方便理解就直接把它看成Activity吧。MainActivity中我们这样写:代码都比较简单可以直接看懂。
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private EditText nameInput; // 输入框
private Button submitButton; // 提交按钮
private TextView welcomeMessage; // 显示欢迎消息
private UserController controller; // Controller
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化界面组件
nameInput = findViewById(R.id.nameInput);
submitButton = findViewById(R.id.submitButton);
welcomeMessage = findViewById(R.id.welcomeMessage);
// 初始化 Controller
controller = new UserController(this);
// 按钮点击事件
submitButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 获取用户输入并通过 Controller 处理
controller.onSubmitButtonClicked();
}
});
}
// 显示欢迎消息
public void showWelcomeMessage(String message) {
welcomeMessage.setText(message);
}
// 获取用户输入的名字
public String getUserInput() {
return nameInput.getText().toString();
}
}
1.3 Controller:负责将用户输入的数据传递给 Model,并通过 View 更新界面
创建一个Controller类:
// Controller - 处理业务逻辑
public class UserController {
private MainActivity view; // View
private UserModel model; // Model
public UserController(MainActivity view) {
this.view = view;
this.model = new UserModel();
}
// 处理提交按钮点击事件
public void onSubmitButtonClicked() {
// 从 View 获取用户输入
String name = view.getUserInput();
// 将输入保存到 Model 中
model.setName(name);
// 创建欢迎消息并通过 View 显示
String welcomeMessage = "Hello, " + model.getName() + "!";
view.showWelcomeMessage(welcomeMessage);
}
}
1.4 总结
上述的代码我们实现了一个最简单的MVC架构,MVC的核心思想就是让View视图层和Model数据处理层完全解耦分离,中间通过Controller控制层链接,视图层只负责更新数据,不像以前把很多逻辑都塞到Activity中,导致Activity文件臃肿。可以看到Controller同时持有了Model、View的实例对象。MVC架构可以用下面这张图做一个形象的表示:
点击下载 Android MVC架构示例Demo https://github.com/xuhao120833/MVC
二、MVP
MVP包含三个部分:Model、View、Presenter。和MVC相比用Presenter替代了Controller,在MVC中Controller同时持有了Model、View的实例对象,起到中间人的作用,但是这种形式的缺点在于,换一个View就得新建一个Controller,Controller无法复用,大大增加了代码量,于是MVP更进一步,Presenter持有的不再是Model、View的实例对象,而是一个接口引用,这样一来Presenter就可以得到复用,进一步解耦了Model和Presenter的关系、View和Presenter的关系。
我们举一个模拟登录界面的例子来说明MVP架构。
2.1 创建接口
Android Studio新建一个名称为MVP的项目,之后新建如下三个Java接口:ILoginModel、LoginCallback、LoginView。
ILoginModel接口用于不同的Model实现。
package com.htc.mvp;
public interface ILoginModel {
// 定义登录方法
void login(String username, String password, LoginCallback callback);
}
LoginCallback接口,在调用ILoginModel.login方法时实现的回调。
package com.htc.mvp;
// 回调接口,用于通知登录结果
public interface LoginCallback {
void onSuccess(String user);
void onFailure(String error);
}
LoginView接口用于不同的View去实现。
package com.htc.mvp;
public interface LoginView {
// 显示加载动画
void showLoading();
// 隐藏加载动画
void hideLoading();
// 登录成功时显示消息
void showLoginSuccess(String message);
// 登录失败时显示错误
void showLoginError(String error);
}
2.2 LoginModel实现ILoginModel接口
package com.htc.mvp;
import android.os.Handler;
import android.os.Looper;
public class LoginModel implements ILoginModel {
@Override
public void login(String username, String password, LoginCallback callback) {
// 模拟网络延迟2秒
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if ("admin".equals(username) && "123456".equals(password)) {
callback.onSuccess("欢迎您, " + username + "!");
} else {
callback.onFailure("用户名或密码错误");
}
}
}, 2000);
}
}
2.3 MainActivity实现LoginView接口
package com.htc.mvp;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity implements LoginView {
private LoginPresenter presenter;
private EditText etUsername;
private EditText etPassword;
private Button btnLogin;
private ProgressBar progressBar;
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etUsername = findViewById(R.id.etUsername);
etPassword = findViewById(R.id.etPassword);
btnLogin = findViewById(R.id.btnLogin);
progressBar = findViewById(R.id.progressBar);
// 使用构造函数注入 LoginModel 实例
presenter = new LoginPresenter(this, new LoginModel());
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String username = etUsername.getText().toString().trim();
String password = etPassword.getText().toString().trim();
presenter.performLogin(username, password);
}
});
}
@Override
public void showLoading() {
progressBar.setVisibility(View.VISIBLE);
}
@Override
public void hideLoading() {
progressBar.setVisibility(View.GONE);
}
@Override
public void showLoginSuccess(String message) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
@Override
public void showLoginError(String error) {
Toast.makeText(this, error, Toast.LENGTH_LONG).show();
}
}
2.4 实现LoginPresenter
package com.htc.mvp;
public class LoginPresenter {
private LoginView view;
private ILoginModel model;
// 通过构造方法传入 ILoginModel 的实例,可以在外部进行依赖注入
public LoginPresenter(LoginView view, ILoginModel model) {
this.view = view;
this.model = model;
}
// 执行登录操作
public void performLogin(String username, String password) {
if (username == null || username.isEmpty() || password == null || password.isEmpty()) {
view.showLoginError("用户名和密码不能为空");
return;
}
view.showLoading();
model.login(username, password, new LoginCallback() {
@Override
public void onSuccess(String user) {
view.hideLoading();
view.showLoginSuccess(user);
}
@Override
public void onFailure(String error) {
view.hideLoading();
view.showLoginError(error);
}
});
}
}
2.5 总结
可以看到MVP模式中,Model、View中用到的方法都被抽象到接口中了,而Presenter只持有了Model、View的接口引用,保证了Presenter可以得到复用。MVP架构可以用下面这张图做一个形象的表示:创建Presenter的时候传的是View、Model的实例,但是Presenter保存的却是View、Model的接口引用,这就保证了Presenter可以调用不同的View和Model,保证了Presenter的复用,这是MVP相较于MVC进步最大的地方。还有一个不常提的优势是,由于Presenter持有的是接口引用,就很方便进行单元测试,不用创建真的View、Model,可以使用Mockito或类似的库模拟传入Presenter进行测试。
注:图中的虚线表示通过接口进行方法调用。
点击下载Android MVP架构示例Demo:https://github.com/xuhao120833/MVP
三、MVVM
MVVM架构有三大要素:Model、View、ViewModel。相较于MVC、MVP,MVVM迎来了很大的变化,ViewModel不直接持有View的引用或者实例对象,而是通过DataBinding或者LiveData.observe来更新UI。MVVM的学习难度更高,引入了三个新的技术:ViewModel、DataBinding、LiveData。下面依次介绍三个新的小伙伴。
3.1 ViewModel:
ViewModel是JetPack androidx.lifecycle仓库中的组件,存在的意义主要是帮助开发者有效保存UI界面数据,看它所属于的仓库路径包含lifecycle,就知道它和“生命周期”强相关。上述解释很抽象,必须举个例子理解一下。
Android开发者都知道,当系统语言、屏幕方向(横屏)、主题等发生变化时,会触发当前Activity重建重新执行onCreate,如果你把和UI相关的数据放在Activity中,数据就会被重新赋值导致数据丢失。倘若你的UI界面复杂,数据很多,那么数据丢失带来的结果将是毁灭性的。Android传统的开发也给我们提供了保存数据的方法,但是很简陋、不实用,只适合保存简单键值对,如下所示:以下示例为伪代码。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
// 恢复数据
front = savedInstanceState.getInt("front", -1);
rear = savedInstanceState.getInt("rear", -1);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
//onSaveInstanceState() → onPause() → onStop() → onDestroy()
super.onSaveInstanceState(outState);
//保存数据到 Bundle,避免系统设置如语言改变时,首页midlleApp显示的位置复位。
if (circularQueue != null) {
outState.putInt("front", circularQueue.front); // 保存头指针
outState.putInt("rear", circularQueue.rear); // 保存尾指针
Log.d(TAG,"onSaveInstanceState 保存头尾指针 front "+circularQueue.front+" rear"+circularQueue.rear);
}
}
注:Bundle savedInstanceState就是Android原生提供的保存数据的方法。用起来也很简单,就是当系统属性发生变化时,Activity被重建前会先回调到onSaveInstanceState方法,如果有需要保存的数据就用Bundle outState进行保存,Activity重新执行到onCreate时又通过Bundle savedInstanceState把数据取出来使用,达到防止数据丢失的作用。
但是通过Bundle来保存数据也有致命的缺陷:1、它只能保存下面的类型:简单的基本类型键值对;List<T>、ArrayList<T>实例对象,T必须实现Parcelable接口,也就是必须是可序列化、反序列化的类。2、Bundle最大只能保存1MB的数据,很小,超过限制会崩溃。就是因为有这些缺点,我们必须引入ViewModel,它可以保存大量数据、几乎所有类型数据都能保存、生命周期和Activity/Fragment无关。 Bundle 和ViewModel区别如下图:
总结:为什么要用ViewModel? ——》数据放在ViewModel中,ViewModel有独立的生命周期,Activity/Fragment意外被销毁时,和它没有关系,保存在其中的数据不会被重新赋值,它可以保存大量数据,囊括几乎所有类型,完美解决了Bundle savedInstanceState的致命缺陷。
3.2 LiveData
LiveData也是JetPack androidx.lifecycle仓库下提供的一个组件,和生命周期这个概念也是强相关,当然这次不是它自己的什么周期,而是观察者的生命周期。这话听起来很拗口,我们接下来慢慢说。
LiveData是用来保存数据的,它最核心的设计思想是“观察者模式”,也就是它可以被Activty/Fragment观察,当它保存的数据发生变化时自动通知所有的观察者更新UI。说到这里有同学就问了:“那这和普通的观察者模式也没区别呀?只不过代替码农封装了通知观察者的过程,把这个过程隐藏了而已。”LiveData不仅如此,它最牛的地方来了:可以多个Activity/Fragment同时观察一个LiveData数据,LiveData数据发生变化时,会根据传进来的LifecycleOwner,提前获悉所有观察者的生命周期状态,只有处在 “活跃期”的,它才会通知,已经销毁的还会自动解绑。 完全不用程序员自己操心,避免了Activity/Fragment已经被销毁,但是依然是观察者,数据改变依然需要通知导致的内存泄漏。下面我总结一下LiveData的特点:
<1> 观察者生命周期感知:上面说了,它只会通知“活跃期”的观察者更新UI,那么怎么定义活跃期?很简单,就是用户能用眼睛看到的就是处在活跃期的观察者。
<2>自动处理线程:处理耗时任务传统的写法是开一个线程后台处理,处理完之后如果数据需要用来更新UI,那么还得手动切换到主线程,比如runOnUiThread。LiveData就不需要这么麻烦,它提供了两个更新数据的方法,一个setValue用于在主线程直接更新数据,立马通知活跃观察者;一个postValue用于在其它线程更新数据,postValue的工作逻辑是,把数据更新的任务自动放到主线程的工作队列中,等到主线程执行到了这个任务再去更新数据——》然后通知活跃的观察者更新UI。
<3>避免内存泄:LiveData 是弱引用的,这意味着当 Activity 或 Fragment 被销毁时,它的观察者会自动被解除,不会导致内存泄漏。你不需要手动移除观察者。
<4>常见类型:常见的有LiveData和MutableLiveData两种,LiveData中的数据只读,MutableLiveData的数据可读可写。
<5>支持保存几乎所有数据类型
说了这么多,接下来用kotlin看看一个最简单的Livedata如何使用:
注:以下代码用伪代码展示
//ViewModel类中使用MutableLiveData保存数据
class UserViewModel : ViewModel() {
val user = MutableLiveData<User>()
val userName = MutableLiveData<String>()
fun update_user() {
user.value = User(id = 1, name = userName.value.toString(), age = 30)
//kotlin中的.value = 就是Java中setValue在主线程更新LiveData数据的意思,这里是简写
}
. . . . . .
}
//MainActivity 中观察userName的变化
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
}
private val userViewModel: UserViewModel by viewModels() // ✅ 移到类内部
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 观察 LiveData 数据变化
userViewModel.userName.observe(this) { userName ->
// 这里可以处理一些更新逻辑
Log.d(TAG,"userViewModel.user.observe binding.livedata.setText"+userName)
userViewModel.update_user()
}
}
. . . . . .
}
总结:综合看下来LiveData更像是一个托管工具人,它够强大也很好用,专门用来更新UI,自动管理线程,避免内存泄漏,是一个半自动化工具。
3.3 DataBinding
如果说LiveData是一个半自动化工具的话,那么DataBinding就是全自动化工具。强如LiveData最后也需要通过binding.livedata.setText(userName+" Livedata使用")这种形式来更新UI,DataBinding直接把userViewModel.userName.observe(this)这种观察到变化再更新的模式直接抛弃了,它直接到layout布局文件给UI组件绑定数据。
其它特点和LiveData高度相似,也是只会更新活跃期的Activity/Fragment。我们需要注意的是,DataBinding的绑定方式分为两种:
<1> 双向绑定:数据变化会导致UI变化,UI变化也会导致数据变化,可以用EditText编辑文字来模拟。
<2> 单向绑定:只有数据变化才会导致UI变化,反过来却不行。下面举个kotlin例子介绍如何使用:
想要使用DataBinding,第一步需要在build.gradl.kts中打开databinding开关:
android {
. . . . . .
buildFeatures {
dataBinding = true
}
}
第二步,去layout布局文件中,结合ViewModel(用一般的数据类也行,这里只是用ViewModel结合举例)绑定UI和数据。@={ 双向绑定,@{ 单向绑定
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.htc.mvvm_kotlin.UserViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 双向绑定:EditText 与 ViewModel 中的 userName 绑定 -->
<EditText
android:id="@+id/editTextName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={viewModel.userName}" />
<!-- 单向绑定:TextView 显示 userName -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@{viewModel.userName}" />
<!-- 显示其他信息 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@{`User Name: ` + viewModel.user.name}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@{`User ID: ` + viewModel.user.id}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@{`Age: ` + viewModel.user.age}" />
<TextView
android:id="@+id/livedata"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="LiveData" />
</LinearLayout>
</layout>
第三步,Activty/Fragment中加载使用。
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
}
private val userViewModel: UserViewModel by viewModels() // ✅ 移到类内部
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
// 绑定 ViewModel
binding.viewModel = userViewModel
binding.lifecycleOwner = this
// 观察 LiveData 数据变化
userViewModel.userName.observe(this) { userName ->
// 这里可以处理一些更新逻辑
Log.d(TAG,"userViewModel.user.observe binding.livedata.setText"+userName)
binding.livedata.setText(userName+" Livedata使用")
userViewModel.update_user()
}
}
}
总结:DataBinding是一个绑定UI、自动更新UI的工具,很省事,中间过程几乎都省略了,但是我个人是不推荐使用DataBinding双向绑定的,因为如果一旦UI更新出错,那么将是致命的,报错信息很少,几乎无法排错。使用起来也容易出错,导致编译不过。MVVM推荐的使用方式是:ViewModel保存数据 + LiveData绑定UI更新 + DataBinding单向绑定UI更新。LiveData.observe留给程序员更多自主可控的空间。
3.4 总结
MVVM引入了LiveData、DataBinding、ViewModel这些强大的工具,旨在进一步解耦代码,ViewModel不直接持有View层的实例对象或者引用,而是通过DataBinding绑定、LiveData.oberverve这些方式来更新UI。MVVM相较于MVC、MVP来说,带来了数据持久化、更加解耦、防止内存泄漏等诸多进步,可以用下面的图片来简单表示这种结构:ViewModel和Model之间即有双箭头实线又有双箭头虚线,意思是ViewModel即可以持有Model的引用(虚线),也可以持有Model的实例对象(实线)。
&emsp可以看到同一个ViewModel的LiveData可以被多个Activity/Fragment observe或者DataBinding。
特别注意:DataBinding绑定的不一定是ViewModel,普通的数据类也行;ViewModel中也不是只能用LiveData;LiveData离开了ViewModel也可以正常使用。它们三个是互相独立的,可单独使用,别混为一潭。它们各有特点,在MVC、MVP架构中也可以单独导入使用。只是它们结合起来共同构成了MVVM的完整形态。
3.5 Demo APP下载
MVVM架构推荐使用Kotlin编写,更简洁,和JetPack组件结合得更好。这里Koltin、Java的实现都一起给出。
3.5.1 Kotlin Demo APP下载
点击下载Kotlin Demo APP ,GitHub链接:https://github.com/xuhao120833/MVVM_Kotlin
3.5.2 Java Demo APP下载
点击下载Java Demo APP ,GitHub链接:https://github.com/xuhao120833/MVVM_Java
四、总结
总的看起来,MVVM比MVC、MVP先进得多,但是也较为复杂,容易出错。如果你写的APP对性能、内存这些要求没那么高,完全可以不用MVVM。最后还是一句话:没有最好的架构,只有最适合的架构。熟练掌握一种,就能工作了。但是对所有架构的持续学习是必须要做的。
注:还没来得及校对,如果有错别字和表达不清的地方还请见谅,后续会陆续改正的——2025.3.6
注:校对已完成——2025.3.7