Android MVVM 架构详解:基于 Java 与 RxJava 的响应式实践
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 订阅这些数据流并自动更新。这种"单向数据流 + 响应式订阅"的模式带来了三个关键收益:
- 生命周期安全:ViewModel 与 View 解耦,配置变更(如屏幕旋转)时数据不丢失。
- 可测试性:ViewModel 是纯逻辑对象,不依赖 Android Framework 组件,可独立做单元测试。
- 职责清晰: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.view、android.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 发挥价值的前提。
- 点赞
- 收藏
- 关注作者
评论(0)