从MVP到MVVM:架构演进的痛苦与收获
架构升级背景
2018年初,Google在Android Dev Summit上大力推荐Android Architecture Components,标志着Android官方架构方案的正式推出。在此之前,我们的项目一直使用MVP架构,虽然运行稳定,但也逐渐暴露出一些问题:
- 代码冗余:Presenter中大量模板代码
- 生命周期管理复杂:需要手动处理内存泄漏
- 测试困难:UI逻辑和业务逻辑耦合严重
- 响应式支持不足:数据变化需要手动刷新UI
经过技术调研和团队讨论,我们决定将项目从MVP架构升级到MVVM架构。
MVP架构的问题分析
1. 当前MVP架构的问题
// 传统的MVP架构
public class UserPresenter implements UserContract.Presenter {
private UserContract.View view;
private UserRepository repository;
private CompositeDisposable disposables;
@Override
public void loadUser(int userId) {
view.showLoading();
disposables.add(
repository.getUser(userId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(user -> {
view.hideLoading();
view.showUser(user);
}, error -> {
view.hideLoading();
view.showError(error.getMessage());
})
);
}
@Override
public void detachView() {
if (disposables != null && !disposables.isDisposed()) {
disposables.dispose();
}
view = null;
}
}
问题分析:
- 模板代码过多,每个Presenter都需要类似的订阅管理
- 生命周期管理容易出错,忘记dispose会导致内存泄漏
- View接口定义繁琐,每个方法都需要对应的View方法
- 数据变化需要手动触发UI更新
2. 架构升级的必要性
随着项目规模的扩大,MVP架构的问题越来越明显:
- 新功能开发效率低
- 代码维护成本高
- 测试覆盖率低
- 团队协作效率受影响
Android Architecture Components介绍
1. 核心组件
ViewModel:
public class UserViewModel extends ViewModel {
private MutableLiveData<User> userLiveData = new MutableLiveData<>();
private UserRepository repository;
public LiveData<User> getUser(int userId) {
loadUser(userId);
return userLiveData;
}
private void loadUser(int userId) {
repository.getUser(userId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(user -> userLiveData.setValue(user));
}
}
LiveData:
public class UserDataRepository {
private MediatorLiveData<List<User>> users;
public LiveData<List<User>> getUsers() {
if (users == null) {
users = new MediatorLiveData<>();
loadUsers();
}
return users;
}
private void loadUsers() {
LiveData<List<User>> dbSource = localDataSource.getUsers();
LiveData<List<User>> networkSource = remoteDataSource.getUsers();
users.addSource(dbSource, users -> {
users.removeSource(dbSource);
if (users.isEmpty()) {
users.addSource(networkSource, networkUsers -> {
users.removeSource(networkSource);
users.setValue(networkUsers);
localDataSource.saveUsers(networkUsers);
});
} else {
users.removeSource(dbSource);
users.setValue(users);
}
});
}
}
DataBinding:
<!-- 布局文件启用DataBinding -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.example.UserViewModel" />
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.user.name}"
android:visibility="@{viewModel.isLoading ? View.GONE : View.VISIBLE}" />
</LinearLayout>
</layout>
MVVM架构设计
1. 新架构分层
┌─────────────────────────────────────┐
│ View Layer │
│ (Activity/Fragment + DataBinding) │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ ViewModel Layer │
│ (ViewModel + LiveData) │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Repository Layer │
│ (Repository + Data Sources) │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Data Layer │
│ (Local DB + Remote API + Cache) │
└─────────────────────────────────────┘
2. Repository模式实现
public class UserRepository {
private UserLocalDataSource localDataSource;
private UserRemoteDataSource remoteDataSource;
private MediatorLiveData<List<User>> cachedUsers;
private boolean isDataRetrieved = false;
public LiveData<List<User>> getUsers() {
if (cachedUsers == null) {
cachedUsers = new MediatorLiveData<>();
cachedUsers.setValue(Collections.emptyList());
}
if (!isDataRetrieved) {
fetchUsersFromRemote();
}
return cachedUsers;
}
private void fetchUsersFromRemote() {
final LiveData<List<User>> remoteLiveData = remoteDataSource.getUsers();
cachedUsers.addSource(remoteLiveData, users -> {
cachedUsers.removeSource(remoteLiveData);
if (users != null && !users.isEmpty()) {
isDataRetrieved = true;
localDataSource.saveUsers(users);
cachedUsers.addSource(localDataSource.getUsers(),
cachedUsers::setValue);
} else {
cachedUsers.addSource(localDataSource.getUsers(),
cachedUsers::setValue);
}
});
}
}
3. ViewModel实现
public class UserListViewModel extends ViewModel {
private UserRepository userRepository;
private LiveData<List<User>> users;
private MutableLiveData<Boolean> isLoading = new MutableLiveData<>();
public UserListViewModel(UserRepository userRepository) {
this.userRepository = userRepository;
loadUsers();
}
public LiveData<List<User>> getUsers() {
if (users == null) {
users = userRepository.getUsers();
}
return users;
}
public LiveData<Boolean> getIsLoading() {
return isLoading;
}
private void loadUsers() {
isLoading.setValue(true);
// Repository会自动处理数据获取逻辑
getUsers();
isLoading.setValue(false);
}
// 业务逻辑方法
public void refreshUsers() {
isLoading.setValue(true);
// 触发重新加载
userRepository.refreshUsers();
isLoading.setValue(false);
}
public void deleteUser(User user) {
userRepository.deleteUser(user);
}
}
迁移过程
1. 分阶段迁移策略
第一阶段:基础设施搭建
- 添加Android Architecture Components依赖
- 创建基础的Repository和ViewModel类
- 配置DataBinding
第二阶段:核心模块迁移
- 用户模块(最重要的模块)
- 选择1-2个核心页面进行试点
第三阶段:全面迁移
- 其他模块逐步迁移
- 完善测试用例
第四阶段:优化和清理
- 移除旧的MVP代码
- 优化性能和用户体验
2. 具体迁移步骤
步骤1:添加依赖
// build.gradle
implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "android.arch.lifecycle:viewmodel:1.1.1"
implementation "android.arch.lifecycle:livedata:1.1.1"
implementation "android.arch.persistence.room:runtime:1.1.1"
kapt "android.arch.lifecycle:compiler:1.1.1"
kapt "android.arch.persistence.room:compiler:1.1.1"
// DataBinding
android {
dataBinding {
enabled = true
}
}
步骤2:创建基础架构
// 基础ViewModel
public abstract class BaseViewModel extends ViewModel {
protected MediatorLiveData<Resource> loadingState = new MediatorLiveData<>();
public LiveData<Resource> getLoadingState() {
return loadingState;
}
protected void setLoadingState(Resource resource) {
loadingState.setValue(resource);
}
}
// 基础Repository
public abstract class BaseRepository {
protected <T> LiveData<Resource<T>> createResourceLiveData(
LiveData<T> databaseSource,
LiveData<Resource<T>> networkSource) {
return Transformations.switchMap(networkSource, resource -> {
if (resource.status == Status.SUCCESS) {
return databaseSource;
} else {
return networkSource;
}
});
}
}
步骤3:迁移具体页面
// 原来的MVP实现
public class UserActivity extends AppCompatActivity implements UserContract.View {
private UserPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
presenter = new UserPresenter(this, new UserRepository());
presenter.loadUser(getUserId());
}
@Override
public void showUser(User user) {
// 更新UI
}
}
// 新的MVVM实现
public class UserActivity extends AppCompatActivity {
private ActivityUserBinding binding;
private UserViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_user);
// 获取ViewModel
ViewModelFactory factory = new ViewModelFactory(getApplication());
viewModel = ViewModelProviders.of(this, factory).get(UserViewModel.class);
// 绑定LiveData
binding.setViewModel(viewModel);
binding.setLifecycleOwner(this);
// 观察数据变化
observeUser();
}
private void observeUser() {
viewModel.getUser(getUserId()).observe(this, user -> {
if (user != null) {
// DataBinding会自动更新UI
}
});
}
}
3. DataBinding布局迁移
原来的布局:
<!-- activity_user.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone" />
<ImageView
android:id="@+id/ivAvatar"
android:layout_width="100dp"
android:layout_height="100dp" />
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
迁移后的布局:
<!-- activity_user.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/android-app"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.UserViewModel" />
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{viewModel.loading ? View.VISIBLE : View.GONE}" />
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:imageUrl="@{viewModel.user.avatarUrl}"
tools:src="@tools:sample/avatars" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.user.name}"
tools:text="张三" />
</LinearLayout>
</layout>
遇到的问题和解决方案
1. 生命周期问题
问题:LiveData的生命周期管理不够灵活
解决方案:
// 使用MediatorLiveData进行复杂的生命周期管理
public class UserViewModel extends ViewModel {
private MediatorLiveData<Resource<User>> userMediator = new MediatorLiveData<>();
public LiveData<Resource<User>> getUser(int userId) {
LiveData<Resource<User>> networkSource = repository.getUserFromNetwork(userId);
LiveData<Resource<User>> dbSource = repository.getUserFromDb(userId);
userMediator.addSource(networkSource, resource -> {
if (resource.status == Status.SUCCESS) {
userMediator.removeSource(networkSource);
repository.saveUserToDb(resource.data);
}
userMediator.setValue(resource);
});
return userMediator;
}
}
2. 数据一致性问题
问题:多个ViewModel之间数据同步困难
解决方案:
// 使用单例Repository保证数据一致性
public class UserRepository {
private static volatile UserRepository INSTANCE;
public static UserRepository getInstance() {
if (INSTANCE == null) {
synchronized (UserRepository.class) {
if (INSTANCE == null) {
INSTANCE = new UserRepository();
}
}
}
return INSTANCE;
}
}
3. 复杂UI状态管理
问题:复杂的UI状态难以用LiveData表示
解决方案:
// 定义Resource类统一管理状态
public class Resource<T> {
public enum Status { SUCCESS, ERROR, LOADING }
public final Status status;
public final T data;
public final String message;
private Resource(Status status, T data, String message) {
this.status = status;
this.data = data;
this.message = message;
}
public static <T> Resource<T> success(T data) {
return new Resource<>(Status.SUCCESS, data, null);
}
public static <T> Resource<T> error(String msg, T data) {
return new Resource<>(Status.ERROR, data, msg);
}
public static <T> Resource<T> loading() {
return new Resource<>(Status.LOADING, null, null);
}
}
迁移效果对比
1. 代码量对比
| 指标 | MVP架构 | MVVM架构 | 改进 |
|---|---|---|---|
| 模板代码 | 40% | 15% | ↓62.5% |
| Presenter/ViewModel | 1500行 | 800行 | ↓46.7% |
| View接口方法 | 20个 | 0个 | ↓100% |
| 生命周期管理代码 | 200行 | 0行 | ↓100% |
2. 开发效率提升
// MVP方式 - 需要大量模板代码
public class UserPresenter implements UserContract.Presenter {
@Override
public void loadUser(int userId) {
view.showLoading();
disposables.add(
repository.getUser(userId)
.subscribe(user -> {
view.hideLoading();
view.showUser(user);
}, error -> {
view.hideLoading();
view.showError(error.getMessage());
})
);
}
}
// MVVM方式 - 代码简洁
public class UserViewModel extends ViewModel {
public LiveData<User> getUser(int userId) {
return repository.getUser(userId);
}
}
3. 测试改进
// MVP测试 - 需要Mock View
@Test
public void testLoadUser() {
UserContract.View mockView = mock(UserContract.View.class);
UserRepository repository = mock(UserRepository.class);
UserPresenter presenter = new UserPresenter(mockView, repository);
User user = new User("张三");
when(repository.getUser(anyInt())).thenReturn(Observable.just(user));
presenter.loadUser(1);
verify(mockView).showLoading();
verify(mockView).showUser(user);
verify(mockView).hideLoading();
}
// MVVM测试 - 纯Java测试,无需Android环境
@Test
public void testGetUser() {
UserRepository repository = mock(UserRepository.class);
UserViewModel viewModel = new UserViewModel(repository);
User user = new User("张三");
when(repository.getUser(anyInt())).thenReturn(Observable.just(user));
TestObserver<User> testObserver = new TestObserver<>();
viewModel.getUser(1).observe(testObserver);
testObserver.assertValue(user);
}
经验总结
1. 架构升级的收益
开发效率提升:
- 减少了大量模板代码
- 数据绑定自动处理UI更新
- 生命周期管理更加安全
代码质量提升:
- 关注点分离更加彻底
- 测试覆盖率显著提升
- 代码可维护性增强
团队协作改善:
- 架构更加标准化
- 新人上手更快
- 代码Review效率提升
2. 迁移过程中的教训
渐进式迁移的重要性:
- 不要一次性全部迁移
- 先选择核心模块试点
- 建立完善的回滚机制
团队培训的必要性:
- MVVM概念需要时间理解
- DataBinding语法需要学习
- 新的开发模式需要适应
3. 最佳实践
// 1. Repository模式的最佳实践
public class UserRepository {
// 单例模式保证数据一致性
// 统一的数据源管理
// 错误处理策略
}
// 2. ViewModel的最佳实践
public class UserViewModel extends ViewModel {
// 只关注UI相关的数据
// 不持有Activity/Fragment引用
// 使用LiveData管理数据流
}
// 3. DataBinding的最佳实践
// 使用BindingAdapter统一处理复杂的绑定逻辑
@BindingAdapter({"imageUrl", "placeholder"})
public static void loadImage(ImageView view, String url, Drawable placeholder) {
Glide.with(view)
.load(url)
.placeholder(placeholder)
.into(view);
}
技术栈总结(2018年1月)
Android SDK 27 (8.1)
Android Architecture Components 1.1.1
LiveData 1.1.1
ViewModel 1.1.1
Room 1.1.1
DataBinding
RxJava 2.1.0 (与LiveData共存)
后续规划
- 完善测试体系:增加更多的单元测试和UI测试
- 性能优化:进一步优化LiveData的性能
- 组件化探索:基于MVVM架构进行组件化改造
- 团队分享:在团队内进行MVVM架构的培训和分享
总结
从MVP到MVVM的架构升级是一次成功的技术演进。虽然迁移过程中遇到了一些挑战,但最终的收益是显著的:
技术收益:
- 代码量减少40%以上
- 测试覆盖率提升60%
- 开发效率提升30%
团队收益:
- 架构更加标准化
- 新人上手时间缩短50%
- 代码质量显著提升
这次架构升级不仅提升了项目的技术水平,更重要的是让我对Android架构设计有了更深入的理解,为后续的技术成长奠定了坚实的基础。
架构升级感悟:
- 技术选型要结合团队实际情况
- 渐进式迁移比激进式迁移更安全
- 架构升级不仅是技术问题,更是团队协作问题