从MVP到MVVM:架构演进的痛苦与收获

记录团队从MVP架构升级到MVVM架构的完整过程,包括技术选型、迁移策略和实际效果

-- 次阅读

从MVP到MVVM:架构演进的痛苦与收获

架构升级背景

2018年初,Google在Android Dev Summit上大力推荐Android Architecture Components,标志着Android官方架构方案的正式推出。在此之前,我们的项目一直使用MVP架构,虽然运行稳定,但也逐渐暴露出一些问题:

  1. 代码冗余:Presenter中大量模板代码
  2. 生命周期管理复杂:需要手动处理内存泄漏
  3. 测试困难:UI逻辑和业务逻辑耦合严重
  4. 响应式支持不足:数据变化需要手动刷新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/ViewModel1500行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共存)

后续规划

  1. 完善测试体系:增加更多的单元测试和UI测试
  2. 性能优化:进一步优化LiveData的性能
  3. 组件化探索:基于MVVM架构进行组件化改造
  4. 团队分享:在团队内进行MVVM架构的培训和分享

总结

从MVP到MVVM的架构升级是一次成功的技术演进。虽然迁移过程中遇到了一些挑战,但最终的收益是显著的:

技术收益

  • 代码量减少40%以上
  • 测试覆盖率提升60%
  • 开发效率提升30%

团队收益

  • 架构更加标准化
  • 新人上手时间缩短50%
  • 代码质量显著提升

这次架构升级不仅提升了项目的技术水平,更重要的是让我对Android架构设计有了更深入的理解,为后续的技术成长奠定了坚实的基础。

架构升级感悟

  • 技术选型要结合团队实际情况
  • 渐进式迁移比激进式迁移更安全
  • 架构升级不仅是技术问题,更是团队协作问题
-- 次访问
Powered by Hugo & Stack Theme
使用 Hugo 构建
主题 StackJimmy 设计