咱们先聊个扎心的话题:你有没有在深夜两点,盯着屏幕上一堆“屎山”代码怀疑人生?那些View层里塞满了网络请求,Model层还在调用UI组件,改一个Bug引出十个新Bug,每次重构都像在拆炸弹——剪错一根线,整个App直接炸给你看。
别慌,这不是你能力不行,是架构背锅。
今天咱们不整那些虚头巴脑的理论,直接上手,从MVC的坑里爬出来,一步步走到MVVM,顺便把那些让开发者秃头的耦合问题,一个个拆解干净。我会用Android和iOS双端实战例子,让你看得懂、学得会、回去能用。
一、MVC:曾经的王者,如今的“坑王”
1.1 什么是MVC?
MVC,Model-View-Controller,这哥们儿是Web开发时代的元老。它的核心思想很朴素:
- Model:数据层,负责业务逻辑和数据存储
- View:UI层,负责显示界面
- Controller:控制层,负责协调Model和View
听起来很完美对吧?但在移动端,这玩意儿慢慢暴露出了致命问题。
1.2 MVC在移动端的多重人格分裂
在Web端,Controller处理完请求后直接渲染页面,边界清晰。但在移动端,情况变得复杂了:
Android典型MVC代码:
public class MainActivity extends AppCompatActivity {
// View层组件
private TextView tvTitle;
private Button btnLoad;
// Controller和Model职责混在一起
private DataModel model;
private ApiService apiService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 这里既是初始化View,又是Controller逻辑
initView();
model = new DataModel();
apiService = new ApiService();
btnLoad.setOnClickListener(v -> {
// Controller在处理用户交互
loadDataFromNetwork();
});
}
private void loadDataFromNetwork() {
// 请求数据,然后直接更新UI——这是Controller还是Model?
apiService.fetchData(new Callback() {
@Override
public void onSuccess(String data) {
// View层直接操作
tvTitle.setText(data);
}
@Override
public void onError(Exception e) {
// 错误处理也混在一起
showToast("加载失败");
}
});
}
private void initView() {
tvTitle = findViewById(R.id.tv_title);
btnLoad = findViewById(R.id.btn_load);
}
}
看看这段代码,是不是很眼熟?
MainActivity既是Activity(View),又在处理业务逻辑(Controller),还在持有Model引用- 网络请求直接在Activity里写,耦合度爆炸
- 想测试这个逻辑?连个Mock都难,因为
tvTitle是真实UI组件
iOS典型MVC代码:
class ViewController: UIViewController {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var loadButton: UIButton!
private var dataModel: DataModel?
private var apiClient: ApiClient?
override func viewDidLoad() {
super.viewDidLoad()
// View初始化
dataModel = DataModel()
apiClient = ApiClient()
// 按钮点击处理
loadButton.addTarget(self, action: #selector(loadData), for: .touchUpInside)
}
@objc private func loadData() {
apiClient?.fetchData { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let data):
// View直接更新
self.titleLabel.text = data
case .failure(let error):
// 错误处理也在这里
self.showError(error)
}
}
}
private func showError(_ error: Error) {
let alert = UIAlertController(title: "错误", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default))
self.present(alert, animated: true)
}
}
iOS这边也差不多,ViewController承担了太多职责。
1.3 MVC的三大死穴
死穴一:肥大的Controller
在移动端,屏幕小、交互复杂,Controller不得不处理大量UI逻辑、网络请求、数据解析、动画控制……一个Controller文件上千行代码太常见了。
死穴二:难以测试
你想测试一个网络请求的逻辑?不好意思,你得启动整个Activity/ViewController,还得有真实的网络环境。单元测试?根本没法写。
死穴三:耦合度爆表
View直接依赖Model,Model不知道View的存在,但Controller把两者绑在一起,改一处动全身。
二、走进MVVM:解耦的艺术
2.1 MVVM的核心思想
MVVM = Model-View-ViewModel,关键是那个ViewModel。
它像是一个中间层,把View和Model彻底分开:
- Model:还是数据层,保持不变
- View:只管显示,不关心数据从哪来
- ViewModel:持有Model的数据,转换成View能理解的格式,通过数据绑定让View自动更新
核心变化:View和ViewModel之间通过数据绑定通信,而不是互相调用。
2.2 Android MVVM实战
让我们用Android的Jetpack架构组件重新写一遍上面的例子。
第一步:定义Model
// DataModel.java - 纯粹的数据层,没有任何UI依赖
public class DataModel {
private String title;
private List<Item> items;
// 数据变更回调,但这里不用,因为ViewModel会处理
public interface OnDataChangedListener {
void onDataChanged(DataModel model);
}
// 只做数据操作
public void updateTitle(String newTitle) {
this.title = newTitle;
}
public String getTitle() {
return title;
}
}
public class Item {
private String id;
private String name;
private double price;
// getter/setter省略
}
第二步:创建ViewModel
// MainViewModel.java - 核心!负责业务逻辑
public class MainViewModel extends ViewModel {
// 用LiveData包装数据,View可以观察它
private LiveData<String> titleLiveData;
private LiveData<List<Item>> itemsLiveData;
private LiveData<UiState> uiState;
private DataModel dataModel;
private ApiRepository apiRepository;
public MainViewModel() {
dataModel = new DataModel();
apiRepository = new ApiRepository();
// 初始化 LiveData
titleLiveData = Transformations.map(dataModel.getTitle(), title -> title);
// 用StateFlow或者LiveData包装UI状态
uiState = new MutableLiveData<>(UiState.LOADING);
}
// 暴露给View的方法
public LiveData<String> getTitle() {
return titleLiveData;
}
public LiveData<List<Item>> getItems() {
return itemsLiveData;
}
public LiveData<UiState> getUiState() {
return uiState;
}
// 业务逻辑都在ViewModel里
public void loadData() {
// 切换UI状态
uiState.setValue(UiState.LOADING);
// 异步加载数据
apiRepository.fetchData(new Callback<List<Item>>() {
@Override
public void onSuccess(List<Item> items) {
// 更新数据
dataModel.setItems(items);
// 更新UI状态
uiState.setValue(UiState.SUCCESS);
}
@Override
public void onError(Exception e) {
uiState.setValue(new UiState.Error(e.getMessage()));
}
});
}
// UI状态枚举
public static class UiState {
public static final UiState LOADING = new UiState() {};
public static final UiState SUCCESS = new UiState() {};
private final String errorMessage;
private UiState() {
this.errorMessage = null;
}
public UiState(String errorMessage) {
this.errorMessage = errorMessage;
}
public boolean isSuccess() {
return errorMessage == null;
}
public String getErrorMessage() {
return errorMessage;
}
}
}
第三步:View层(Activity/Fragment)只做观察
// MainActivity.java - 现在的它很干净
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding; // ViewBinding
private MainViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 获取ViewModel(共享给同Activity的Fragment)
viewModel = new ViewModelProvider(this).get(MainViewModel.class);
// 观察数据变化,UI自动更新
observeTitle();
observeItems();
observeUiState();
// 按钮点击,只是调用ViewModel的方法
binding.btnLoad.setOnClickListener(v -> viewModel.loadData());
}
private void observeTitle() {
viewModel.getTitle().observe(this, title -> {
// View只管更新UI,不关心数据从哪来
binding.tvTitle.setText(title);
});
}
private void observeItems() {
viewModel.getItems().observe(this, items -> {
binding.recyclerAdapter.submitList(items);
});
}
private void observeUiState() {
viewModel.getUiState().observe(this, state -> {
if (state instanceof MainViewModel.UiState.Loading) {
binding.progressBar.setVisibility(View.VISIBLE);
binding.btnLoad.setEnabled(false);
} else if (state instanceof MainViewModel.UiState.Error) {
binding.progressBar.setVisibility(View.GONE);
binding.btnLoad.setEnabled(true);
showError(((MainViewModel.UiState.Error) state).getErrorMessage());
} else {
binding.progressBar.setVisibility(View.GONE);
binding.btnLoad.setEnabled(true);
}
});
}
private void showError(String message) {
new AlertDialog.Builder(this)
.setTitle("错误")
.setMessage(message)
.setPositiveButton("确定", null)
.show();
}
@Override
protected void onDestroy() {
super.onDestroy();
binding.unbind();
}
}
看到了吗? MainActivity现在只有:
- 初始化ViewModel
- 观察数据变化
- 处理用户交互(调用ViewModel的方法)
没有网络请求,没有数据解析,没有UI和数据的耦合。
2.3 iOS MVVM实战
iOS这边我们用Combine或者SwiftUI来演示,因为它们是MVVM的天然搭档。
Model层:
// DataModel.swift
class DataModel: ObservableObject {
@Published var title: String = ""
@Published var items: [Item] = []
func updateTitle(_ newTitle: String) {
title = newTitle
}
}
struct Item: Identifiable {
let id: String
let name: String
let price: Double
}
ViewModel层(核心):
// MainViewModel.swift
class MainViewModel: ObservableObject {
// UI状态
@Published var uiState: UiState = .loading
@Published var title: String = ""
@Published var items: [Item] = []
private let apiClient: ApiClient
private var cancellables = Set<AnyCancellable>()
enum UiState {
case loading
case success
case error(String)
}
init(apiClient: ApiClient = ApiClient()) {
self.apiClient = apiClient
}
// 暴露给View的方法
func loadData() {
uiState = .loading
// 异步请求,用Combine处理
apiClient.fetchItems()
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
guard let self = self else { return }
switch completion {
case .finished:
self.uiState = .success
case .failure(let error):
self.uiState = .error(error.localizedDescription)
}
}, receiveValue: { [weak self] fetchedItems in
self?.items = fetchedItems
})
.store(in: &cancellables)
}
func getTitle() -> String {
return "Hello from ViewModel"
}
}
View层(使用SwiftUI):
// MainView.swift - SwiftUI让MVVM更自然
struct MainView: View {
@StateObject private var viewModel = MainViewModel()
var body: some View {
VStack {
// 标题
Text(viewModel.title)
.font(.largeTitle)
// 列表
List(viewModel.items) { item in
HStack {
Text(item.name)
Spacer()
Text("$\(item.price, specifier: "%.2f")")
}
}
// 加载状态
if viewModel.uiState == .loading {
ProgressView()
}
// 错误提示
if case .error(let message) = viewModel.uiState {
Text(message)
.foregroundColor(.red)
}
// 按钮
Button("加载数据") {
viewModel.loadData()
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
如果用UIKit + Combine:
// ViewController.swift
class ViewController: UIViewController {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var loadButton: UIButton!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
private var viewModel: MainViewModel!
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// 初始化ViewModel
viewModel = MainViewModel()
// 观察数据变化
setupObservables()
// 按钮点击
loadButton.addTarget(self, action: #selector(loadDataTapped), for: .touchUpInside)
}
private func setupObservables() {
// 标题变化
viewModel.$title
.receive(on: DispatchQueue.main)
.sink { [weak self] title in
self?.titleLabel.text = title
}
.store(in: &cancellables)
// 列表数据变化
viewModel.$items
.receive(on: DispatchQueue.main)
.sink { [weak self] items in
self?.tableView.reloadData()
}
.store(in: &cancellables)
// UI状态变化
viewModel.$uiState
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
self?.updateUIState(state)
}
.store(in: &cancellables)
}
private func updateUIState(_ state: MainViewModel.UiState) {
switch state {
case .loading:
activityIndicator.startAnimating()
loadButton.isEnabled = false
case .success:
activityIndicator.stopAnimating()
loadButton.isEnabled = true
case .error(let message):
activityIndicator.stopAnimating()
loadButton.isEnabled = true
showAlert(message: message)
}
}
@objc private func loadDataTapped() {
viewModel.loadData()
}
private func showAlert(message: String) {
let alert = UIAlertController(title: "错误", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default))
present(alert, animated: true)
}
deinit {
cancellables.removeAll()
}
}
三、为什么MVVM能减少耦合?
3.1 依赖倒置原则的体现
在MVC中,Controller直接依赖View和Model。在MVVM中:
- View依赖ViewModel(抽象),而不是具体的实现
- ViewModel依赖Model(抽象),通过接口注入
- 测试时,可以Mock ViewModel,不启动任何UI
// 好的架构:接口隔离
public interface ApiRepository {
LiveData<Result<List<Item>>> fetchItems();
}
// ViewModel依赖接口,而不是具体实现
public class MainViewModel extends ViewModel {
private final ApiRepository apiRepository;
// 通过构造器注入,方便测试
public MainViewModel(ApiRepository apiRepository) {
this.apiRepository = apiRepository;
}
// 测试时传入Mock实现
public void loadData() {
// ...
}
}
3.2 单向数据流
MVVM强制数据单向流动:
Model → ViewModel → View
↑
用户交互
View不能直接修改Model,必须通过ViewModel。这避免了“数据到处飞”的混乱局面。
3.3 生命周期安全
ViewModel有自己的生命周期,不受Activity/ViewController的生命周期影响。Configuration Changes(屏幕旋转)时,ViewModel数据不会丢失。
// ViewModel在屏幕旋转后仍然存在
MainViewModel viewModel = new ViewModelProvider(this).get(MainViewModel.class);
// 数据还在!
四、进阶:MVI架构——MVVM的下一站
如果你已经熟练MVVM,可能会发现新问题:
- 状态管理复杂时,ViewModel里全是
@Published变量,难以追踪 - 多个ViewModel之间的通信困难
- 异步操作的状态机难以管理
这时候,MVI(Model-View-Intent)登场。
4.1 MVI的核心思想
MVI引入两个关键概念:
- Intent(意图):用户的所有操作都是Intent,通过单向流传递
- **State(
