基于RxJava2实现NetworkBoundResource,MVVM下网络状态感知与自动刷新
用RxJava2实现NetworkBoundResource + MVVM下的异常处理方案
看起来你已经搭好了不错的MVVM基础架构,但在NetworkBoundResource的封装和网络状态感知上卡壳了——这很正常,Google的官方示例确实有点抽象。我来一步步帮你拆解,先从核心思路说起,再给你可直接复用的代码,最后结合你的现有代码改造。
一、NetworkBoundResource的核心逻辑(RxJava2版本)
本质上,它是一个本地数据优先,网络数据更新本地,本地驱动UI的数据流管道,同时帮你统一处理:
- 加载状态(正在请求网络)
- 成功状态(本地数据更新,UI自动刷新)
- 异常状态(网络错误、服务器错误等)
先写一个通用的NetworkBoundResource抽象类,适配RxJava2的Flowable/Single/Completable:
public abstract class NetworkBoundResource<ResultType, RequestType> { private final BaseSchedulerProvider schedulerProvider; protected NetworkBoundResource(BaseSchedulerProvider schedulerProvider) { this.schedulerProvider = schedulerProvider; } // 构建最终对外暴露的数据流:先发射本地数据,再尝试用网络数据更新本地 public Flowable<Resource<ResultType>> asFlowable() { return loadFromLocal() // 先发射本地数据,标记为"本地缓存"状态 .map(Resource::success) // 然后发起网络请求,更新本地后再发射最新本地数据 .concatWith(fetchFromRemote() .subscribeOn(schedulerProvider.io()) .flatMapCompletable(this::saveCallResult) .andThen(loadFromLocal().map(Resource::success)) // 网络请求失败时,发射错误状态,同时保留之前的本地数据 .onErrorResumeNext(throwable -> { handleFetchError(throwable); return loadFromLocal().map(Resource::success) .startWithItem(Resource.error(getErrorMessage(throwable), null)); })) // 一开始先发射加载状态 .startWithItem(Resource.loading(null)) .observeOn(schedulerProvider.ui()); } // 子类实现:从本地数据源加载数据(比如Room的Flowable) protected abstract Flowable<ResultType> loadFromLocal(); // 子类实现:从远程数据源请求数据 protected abstract Single<RequestType> fetchFromRemote(); // 子类实现:把远程请求到的数据保存到本地 protected abstract Completable saveCallResult(RequestType item); // 子类可选实现:处理网络请求错误(比如上报日志) protected void handleFetchError(Throwable throwable) {} // 统一处理错误信息,返回给UI的字符串资源ID private int getErrorMessage(Throwable throwable) { if (throwable instanceof HttpException) { return R.string.Common_error_server; } else if (throwable instanceof IOException) { return R.string.Common_no_connection; } else { return R.string.Common_error_loading; } } }
这里用到了一个通用的Resource类,用来包装数据和状态,ViewModel可以通过它感知当前状态:
public class Resource<T> { @NonNull public final Status status; @Nullable public final T data; @Nullable public final Integer messageId; private Resource(@NonNull Status status, @Nullable T data, @Nullable Integer messageId) { this.status = status; this.data = data; this.messageId = messageId; } public static <T> Resource<T> success(@Nullable T data) { return new Resource<>(Status.SUCCESS, data, null); } public static <T> Resource<T> error(@NonNull Integer messageId, @Nullable T data) { return new Resource<>(Status.ERROR, data, messageId); } public static <T> Resource<T> loading(@Nullable T data) { return new Resource<>(Status.LOADING, data, null); } public enum Status { SUCCESS, ERROR, LOADING } }
二、改造你的UserRepository,用NetworkBoundResource封装
比如,如果你需要一个"获取当前登录用户信息"的方法,既从本地加载,又自动从网络刷新,就可以这样写:
@Singleton public class UserRepository { // ... 你的现有代码 ... private final ISApiService apiService; // 需要注入这个API服务 @Inject UserRepository(@NonNull RxSharedPreferences rxSharedPreferences, @NonNull @Remote UserDataSource userRemoteDataSource, @NonNull @Local UserDataSource userLocalDataSource, ISApiService apiService) { this.userRemoteDataSource = userRemoteDataSource; this.userLocalDataSource = userLocalDataSource; preferences = rxSharedPreferences; this.apiService = apiService; } // 新增:获取当前登录用户的数据流,自动同步网络 public Flowable<Resource<User>> getCurrentUser() { return new NetworkBoundResource<User, User>(schedulerProvider) { @Override protected Flowable<User> loadFromLocal() { // 复用你现有的getUserUsingSystemPreferences方法,返回Flowable<User> return getUserUsingSystemPreferences(); } @Override protected Single<User> fetchFromRemote() { // 假设你的API有获取当前用户信息的接口,根据实际情况调整 long userId = preferences.getLong(Consts.KEY_USER_ID__long, -1L); return apiService.getUserById(userId); } @Override protected Completable saveCallResult(User user) { // 把网络获取的用户信息保存到本地Room return userLocalDataSource.saveUser(user).ignoreElement(); } }.asFlowable(); } // ... 你的现有代码 ... }
三、改造ViewModel,感知状态并处理异常
以LoginViewModel为例,你可以订阅getCurrentUser()的数据流,根据Resource的状态更新UI或显示错误:
public class LoginViewModel extends ViewModel { // ... 你的现有代码 ... private final CompositeDisposable disposables = new CompositeDisposable(); private final MutableLiveData<Resource<User>> currentUserLiveData = new MutableLiveData<>(); @Inject LoginViewModel(@NonNull final BaseSchedulerProvider schedulerProvider, @NonNull final UserRepository userRepository) { // ... 你的现有代码 ... // 订阅当前用户数据流 disposables.add( userRepository.getCurrentUser() .subscribe(resource -> { currentUserLiveData.setValue(resource); // 根据状态处理UI switch (resource.status) { case LOADING: // 显示加载框(比如通过LiveData通知UI) break; case SUCCESS: // 更新用户信息UI if (resource.data != null) { loginUiModel.setUser(resource.data); } break; case ERROR: // 显示错误提示 if (resource.messageId != null) { showSnackbar(resource.messageId); } break; } }, throwable -> { // 处理本地加载失败的情况(比如未登录) showSnackbar(R.string.Common_not_logged_in); }) ); } // 对外暴露LiveData给UI观察 public LiveData<Resource<User>> getCurrentUserLiveData() { return currentUserLiveData; } @Override protected void onCleared() { super.onCleared(); disposables.clear(); } // ... 你的现有代码 ... }
四、针对你现有登录流程的异常处理优化
你的登录逻辑现在是在login()方法里用doOnError处理异常,其实可以统一用Resource的思路包装,让ViewModel更清晰:
// UserRepository里的login方法改造 public Single<Resource<User>> login(final String email, final String password) { return userRemoteDataSource.login(email, password) .flatMap(userLocalDataSource::saveUser) .flatMap(user -> userLocalDataSource.login(user.getEmail(), user.getPassword())) .doOnSuccess(this::saveUserId) .map(Resource::success) .onErrorReturn(throwable -> { int messageId; if (throwable instanceof HttpException) { messageId = R.string.Login_invalid_input; } else if (throwable instanceof IOException) { messageId = R.string.Common_no_connection; } else { messageId = R.string.Common_error_loading; } return Resource.error(messageId, null); }) .subscribeOn(schedulerProvider.io()) .observeOn(schedulerProvider.ui()); } // LoginViewModel里的login调用改造 public void login(final String login, final String password) { disposables.add( userRepository.login(login, password) .subscribe(resource -> { switch (resource.status) { case SUCCESS: // 登录成功,跳转页面 break; case ERROR: if (resource.messageId != null) { showSnackbar(resource.messageId); } break; } }) ); }
关键要点总结
- 单一数据源原则:UI永远只从本地(Room/SharedPreferences)获取数据,网络请求只用来更新本地,这样本地数据变化时,Room的Flowable会自动通知UI刷新。
- 状态统一包装:用
Resource类把数据和状态绑定,ViewModel不用关心数据来源,只需要根据状态处理UI。 - 异常集中处理:在NetworkBoundResource或Repository层统一处理异常类型,把错误信息转换成UI能直接用的字符串资源ID,ViewModel只负责显示。
内容的提问来源于stack exchange,提问作者reinaldo
相关产品推荐
相关产品推荐

