You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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;
                        }
                    })
    );
}

关键要点总结

  1. 单一数据源原则:UI永远只从本地(Room/SharedPreferences)获取数据,网络请求只用来更新本地,这样本地数据变化时,Room的Flowable会自动通知UI刷新。
  2. 状态统一包装:用Resource类把数据和状态绑定,ViewModel不用关心数据来源,只需要根据状态处理UI。
  3. 异常集中处理:在NetworkBoundResource或Repository层统一处理异常类型,把错误信息转换成UI能直接用的字符串资源ID,ViewModel只负责显示。

内容的提问来源于stack exchange,提问作者reinaldo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:38:14