Android MVVM 架构详解:基于 Java 与 RxJava 的响应式实践

举报
shenlan9755 发表于 2026/08/31 15:10:16 2026/08/31
【摘要】 Android MVVM 架构详解:基于 Java 与 RxJava 的响应式实践 一、为什么需要 MVVM在 Android 开发中,随着业务逻辑日益复杂,传统的 MVC 和 MVP 模式逐渐暴露出各自的痛点:MVC:Controller 与 View 强耦合,Activity 往往承担了"上帝对象"的角色,代码动辄上千行,难以测试、难以维护。MVP:虽然将 View 与 Present...

Android MVVM 架构详解:基于 Java 与 RxJava 的响应式实践

一、为什么需要 MVVM

在 Android 开发中,随着业务逻辑日益复杂,传统的 MVC 和 MVP 模式逐渐暴露出各自的痛点:

  • MVC:Controller 与 View 强耦合,Activity 往往承担了"上帝对象"的角色,代码动辄上千行,难以测试、难以维护。
  • MVP:虽然将 View 与 Presenter 解耦,但 Presenter 持有 View 的引用,需要在生命周期中小心翼翼地管理回调,容易引发空指针和内存泄漏;且 Presenter 与 View 之间是命令式的双向交互,数据流向不够清晰。

MVVM(Model-View-ViewModel)的核心思想是:数据驱动 UI。ViewModel 不持有 View 的任何引用,只暴露可观察的数据流;View 订阅这些数据流并自动更新。这种"单向数据流 + 响应式订阅"的模式带来了三个关键收益:

  1. 生命周期安全:ViewModel 与 View 解耦,配置变更(如屏幕旋转)时数据不丢失。
  2. 可测试性:ViewModel 是纯逻辑对象,不依赖 Android Framework 组件,可独立做单元测试。
  3. 职责清晰:Model 负责数据,ViewModel 负责状态与逻辑,View 负责渲染,三者各司其职。

二、MVVM 三层职责划分

2.1 Model 层

Model 层是数据的来源,包含:

  • 本地数据源:Room 数据库、SharedPreferences、文件缓存。
  • 远程数据源:Retrofit 网络接口。
  • Repository:作为 Model 层的统一出口,决定数据来自本地还是远程,并做缓存策略。

Repository 是 MVVM 中非常关键的一环,它对 ViewModel 屏蔽了数据来源细节,使 ViewModel 只关心业务逻辑。

2.2 ViewModel 层

ViewModel 层的职责:

  • 持有 UI 状态(State)。
  • 调用 Repository 获取数据,并将结果转换为 UI 可消费的状态。
  • 通过 RxJava 的 Observable / Flowable / LiveData 将数据暴露给 View。
  • 绝不持有 View 的引用,也绝不导入 android.viewandroid.widget 等包

2.3 View 层

View 层(Activity / Fragment)的职责:

  • 观察 ViewModel 暴露的数据流,并在数据变化时更新 UI。
  • 将用户事件(点击、输入等)传递给 ViewModel。
  • 处理 Android 生命周期相关的资源管理。

View 层应该尽量" dumb"——只做渲染和事件转发,不包含业务判断逻辑。


三、技术选型:Java + RxJava + LiveData

本文采用以下技术栈:

组件 作用
ViewModel / AndroidViewModel 官方 AAC 组件,生命周期感知的 ViewModel 基类
LiveData 生命周期感知的可观察数据持有者
RxJava 2 响应式编程框架,处理异步数据流与复杂事件编排
Room 官方 SQLite ORM,作为本地数据源
Retrofit 网络请求框架,作为远程数据源
Dagger 2 依赖注入,管理 Repository 与 ViewModel 的实例

为什么用 RxJava 而不是纯 LiveData?LiveData 擅长"单值的生命周期感知通知",但面对连续数据流、背压、线程切换、多源合并等场景能力有限。RxJava 提供了丰富的操作符,可以优雅地处理这些复杂场景,再通过 LiveDataReactiveStreams 或自定义桥接转换为 LiveData 供 View 观察。


四、分层代码实战

下面以"用户登录后加载个人资料与文章列表"这一典型场景为例,完整演示各层实现。

4.1 Model 层

4.1.1 远程数据源

public interface ApiService {
    @POST("user/login")
    Observable<LoginResponse> login(@Body LoginRequest request);

    @GET("user/profile")
    Observable<UserProfile> getProfile(@Header("Authorization") String token);

    @GET("user/articles")
    Observable<List<Article>> getArticles(@Header("Authorization") String token);
}

4.1.2 本地数据源(Room)

@Entity(tableName = "user_profile")
public class UserProfileEntity {
    @PrimaryKey
    public long id;
    public String name;
    public String avatar;
    public String token;
    public long updateTime;
}

@Dao
public interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    Completable saveProfile(UserProfileEntity profile);

    @Query("SELECT * FROM user_profile WHERE id = :id")
    Flowable<UserProfileEntity> observeProfile(long id);
}

注意 observeProfile 返回 Flowable 而非 Observable——Room 查询是持续的数据流(数据库变更会自动推送),使用 Flowable 可以处理背压。

4.1.3 Repository

Repository 将远程与本地数据源组合,实现"网络优先、本地兜底"的缓存策略:

public class UserRepository {
    private final ApiService apiService;
    private final UserDao userDao;

    @Inject
    public UserRepository(ApiService apiService, UserDao userDao) {
        this.apiService = apiService;
        this.userDao = userDao;
    }

    /**
     * 登录:网络请求成功后,将 token 与 profile 持久化到本地
     */
    public Observable<LoginResult> login(String username, String password) {
        return apiService.login(new LoginRequest(username, password))
                .flatMapResponse(response -> 
                    apiService.getProfile(response.token)
                        .flatMap(profile -> 
                            userDao.saveProfile(toEntity(profile, response.token))
                                .andThen(Observable.just(new LoginResult(profile, response.token)))
                        )
                )
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

    /**
     * 加载文章列表:先推本地缓存,再请求网络更新
     */
    public Observable<List<Article>> loadArticles(String token) {
        return apiService.getArticles(token)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .onErrorResumeNext(Observable.empty()); // 网络失败时静默,使用本地兜底
    }
}

这里体现了 RxJava 的核心价值:flatMap 串联异步请求,andThen 在 Completable 完成后继续流,onErrorResumeNext 做错误降级——用命令式代码写会非常冗长且难以维护。

4.2 ViewModel 层

ViewModel 持有 UI 状态,通过 LiveData 暴露给 View:

public class LoginViewModel extends ViewModel {
    private final UserRepository repository;

    // UI 状态:使用 MutableLiveData 在内部修改,暴露为不可变的 LiveData
    private final MutableLiveData<UiState> uiState = new MutableLiveData<>();
    public LiveData<UiState> getUiState() { return uiState; }

    private CompositeDisposable disposables = new CompositeDisposable();

    @Inject
    public LoginViewModel(UserRepository repository) {
        this.repository = repository;
        uiState.setValue(UiState.idle());
    }

    public void login(String username, String password) {
        // 输入校验
        if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) {
            uiState.setValue(UiState.error("用户名或密码不能为空"));
            return;
        }

        // 进入加载态
        uiState.setValue(UiState.loading());

        Disposable d = repository.login(username, password)
                .subscribe(
                    result -> uiState.setValue(UiState.success(result)),
                    error   -> uiState.setValue(UiState.error(error.getMessage()))
                );
        disposables.add(d);
    }

    /**
     * 加载文章:展示"网络 + 本地"合并策略
     */
    public void loadArticles(String token) {
        Disposable d = repository.loadArticles(token)
                .subscribe(
                    articles -> uiState.setValue(UiState.articlesLoaded(articles)),
                    error    -> uiState.setValue(UiState.error("加载失败,请稍后重试"))
                );
        disposables.add(d);
    }

    @Override
    protected void onCleared() {
        disposables.clear(); // 防止 RxJava 订阅泄漏
        super.onCleared();
    }
}

4.2.1 UiState:密封的状态建模

用统一的 UiState 类表达所有可能的 UI 状态,避免在 View 中散落多个布尔标志位:

public class UiState {
    public enum Status { IDLE, LOADING, SUCCESS, ERROR, ARTICLES_LOADED }

    public final Status status;
    public final LoginResult result;
    public final List<Article> articles;
    public final String errorMessage;

    private UiState(Status status, LoginResult result, 
                    List<Article> articles, String errorMessage) {
        this.status = status;
        this.result = result;
        this.articles = articles;
        this.errorMessage = errorMessage;
    }

    public static UiState idle()           { return new UiState(Status.IDLE, null, null, null); }
    public static UiState loading()        { return new UiState(Status.LOADING, null, null, null); }
    public static UiState success(LoginResult r) { return new UiState(Status.SUCCESS, r, null, null); }
    public static UiState error(String msg)      { return new UiState(Status.ERROR, null, null, msg); }
    public static UiState articlesLoaded(List<Article> a) { 
        return new UiState(Status.ARTICLES_LOADED, null, a, null); 
    }
}

这种"单一状态对象"模式是 MVVM 的最佳实践之一。它保证了 UI 在任意时刻只对应一个确定的状态,避免了"loading=true 但 error 也非空"这类不一致的状态组合。

4.3 View 层

Activity 只做三件事:初始化 ViewModel、订阅状态、转发事件。

public class LoginActivity extends AppCompatActivity {
    private LoginViewModel viewModel;
    private ActivityLoginBinding binding; // ViewBinding

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityLoginBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        viewModel = new ViewModelProvider(this, viewModelFactory).get(LoginViewModel.class);

        // 订阅 UI 状态
        viewModel.getUiState().observe(this, this::renderState);

        // 转发用户事件
        binding.btnLogin.setOnClickListener(v -> 
            viewModel.login(binding.etUsername.getText().toString(),
                            binding.etPassword.getText().toString())
        );
    }

    private void renderState(UiState state) {
        switch (state.status) {
            case IDLE:
                binding.progressBar.setVisibility(View.GONE);
                break;
            case LOADING:
                binding.progressBar.setVisibility(View.VISIBLE);
                binding.btnLogin.setEnabled(false);
                break;
            case SUCCESS:
                binding.progressBar.setVisibility(View.GONE);
                navigateToHome(state.result);
                break;
            case ERROR:
                binding.progressBar.setVisibility(View.GONE);
                binding.btnLogin.setEnabled(true);
                Toast.makeText(this, state.errorMessage, Toast.LENGTH_SHORT).show();
                break;
            case ARTICLES_LOADED:
                showArticles(state.articles);
                break;
        }
    }
}

observe(this, ...) 中的 this 是 LifecycleOwner,LiveData 会自动在 onStop 时暂停订阅、在 onStart 时恢复,无需手动管理订阅的生命周期——这是 LiveData 相对于纯 RxJava 的核心优势。


五、RxJava 与 LiveData 的桥接

在纯 RxJava 方案中,View 需要手动管理 CompositeDisposable。更优雅的做法是将 RxJava 流转换为 LiveData,统一用 observe 订阅:

方案一:LiveDataReactiveStreams

public LiveData<UiState> getUiStateStream() {
    return LiveDataReactiveStreams.fromPublisher(
        stateObservable.toFlowable(BackpressureStrategy.BUFFER)
    );
}

方案二:自定义桥接操作符

public static <T> LiveData<T> fromObservable(Observable<T> observable) {
    MutableLiveData<T> liveData = new MutableLiveData<>();
    observable.subscribe(
        liveData::setValue,
        Throwable::printStackTrace
    );
    return liveData;
}

推荐方案一。LiveDataReactiveStreams 是官方提供的转换器,正确处理了背压与生命周期取消订阅,避免了方案二中潜在的订阅泄漏。


六、依赖注入与 ViewModel Factory

ViewModel 不能通过无参构造函数直接实例化(需要注入 Repository),因此需要自定义 ViewModelProvider.Factory

public class ViewModelFactory implements ViewModelProvider.Factory {
    private final UserRepository userRepository;

    @Inject
    public ViewModelFactory(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        if (modelClass.isAssignableFrom(LoginViewModel.class)) {
            return (T) new LoginViewModel(userRepository);
        }
        throw new IllegalArgumentException("Unknown ViewModel class: " + modelClass);
    }
}

配合 Dagger 2,在 Activity 中注入 ViewModelFactory 后即可通过 ViewModelProvider 获取 ViewModel 实例,保证依赖链完整且可测试。


七、单元测试

MVVM 最大的好处之一是 ViewModel 可独立测试。由于 ViewModel 不依赖任何 Android UI 组件,测试时只需 mock Repository:

public class LoginViewModelTest {
    private UserRepository repository;
    private LoginViewModel viewModel;

    @Before
    public void setup() {
        repository = mock(UserRepository.class);
        viewModel = new LoginViewModel(repository);
    }

    @Test
    public void login_emptyUsername_emitsErrorState() {
        viewModel.login("", "password");
        UiState state = viewModel.getUiState().getValue();
        assertEquals(UiState.Status.ERROR, state.status);
        assertEquals("用户名或密码不能为空", state.errorMessage);
    }

    @Test
    public void login_validCredentials_emitsSuccessState() {
        LoginResult fakeResult = new LoginResult(...);
        when(repository.login("user", "pass"))
            .thenReturn(Observable.just(fakeResult));

        viewModel.login("user", "pass");

        UiState state = viewModel.getUiState().getValue();
        assertEquals(UiState.Status.SUCCESS, state.status);
        assertEquals(fakeResult, state.result);
    }
}

测试中用 TestScheduler 替代 Schedulers.io() 可以精确控制异步执行时机,避免真实线程带来的时序不确定性。RxJava 提供了 RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline()) 来在测试中同步执行。


八、常见陷阱与最佳实践

8.1 陷阱:在 ViewModel 中持有 View 引用

这是 MVVM 最致命的反模式。一旦 ViewModel 持有 Activity 引用,配置变更时旧 Activity 被销毁但 ViewModel 仍存活,会导致内存泄漏与空指针。

正确做法:ViewModel 只暴露 LiveData / Observable,View 主动订阅。

8.2 陷阱:忘记清理 RxJava 订阅

RxJava 的订阅默认不会自动取消。如果 ViewModel 中发起了一个长耗时请求,而 ViewModel 被销毁时未清理,会导致回调悬空。

正确做法:在 ViewModel 中维护 CompositeDisposable,并在 onCleared() 中调用 clear()

8.3 陷阱:在 Observable 链中调用 LiveData.setValue

LiveData.setValue 必须在主线程调用。如果在 RxJava 的 map 操作符内(运行在 IO 线程)直接调用,会抛 IllegalStateException

正确做法:通过 observeOn(AndroidSchedulers.mainThread()) 切回主线程后再更新 LiveData,或使用 postValue

8.4 最佳实践:单一状态对象

如 4.2.1 所述,用一个 UiState 对象表达全部 UI 状态,而非多个独立的 LiveData 字段。这保证了状态的一致性,也让 View 的渲染逻辑变成一个纯粹的 switch 分支。

8.5 最佳实践:Repository 作为唯一数据出口

ViewModel 永远不直接访问 Room 或 Retrofit,只通过 Repository。这样数据策略(缓存、降级、合并)集中在 Repository 层,ViewModel 保持纯粹的业务逻辑。


九、数据流向总结

用户操作
   │
   ▼
View (Activity/Fragment) ──事件──▶ ViewModel
                                      │
                                      │ 调用
                                      ▼
                                  Repository
                                   │      │
                          本地数据源      远程数据源
                          (Room)         (Retrofit)
                                   │      │
                                   └──┬───┘
                                      │ Observable / Flowable
                                      ▼
                                  ViewModel
                                      │ 转换为 UiState
                                      │ 通过 LiveData 暴露
                                      ▼
View (Activity/Fragment) ◀──订阅── LiveData<UiState>
   │
   ▼
渲染 UI

关键特征:单向数据流。数据从 Model 流向 ViewModel 再流向 View,用户事件从 View 流向 ViewModel,ViewModel 通过 Repository 访问 Model。没有任何一层反向依赖其上层,保证了可测试性与可维护性。


十、总结

MVVM 并非银弹,但在中大型 Android 项目中,它通过"数据驱动 + 响应式订阅 + 生命周期感知"三件套,显著降低了 View 与逻辑的耦合度。结合 Java + RxJava 的技术栈:

  • RxJava 负责复杂异步流的编排与错误处理,提供丰富的操作符应对合并、降级、防抖等场景。
  • LiveData 负责生命周期安全的 UI 状态通知,自动处理订阅的暂停与恢复。
  • ViewModel 负责状态持有与业务逻辑,配置变更时不丢失数据。
  • Repository 负责数据策略,对上层屏蔽数据来源细节。

四者各司其职,共同构成了一个清晰、可测试、可扩展的架构体系。在实际落地中,务必遵守"ViewModel 不持有 View 引用"与"单一状态对象"两条铁律,这是 MVVM 发挥价值的前提。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。