Android Paging组件错误处理

1,653 阅读6分钟

一 前言

在上一篇Android Paging组件在MVVM架构中的使用指南(一)中,我初步介绍了Android Jetpack Paging组件在MVVM架构中的基础使用方法,其中数据源使用了Github api也就是网络数据源,因此我们自然会想到:网络连接错误怎么办?事实上,如果按照上一篇中实现的话,一旦在数据分页时遇到网络连接错误,即使网络重新稳定,也不会重新触发分页了,这显然是不能接受的,因此我将在本篇着手处理网络连接错误

官方文档中关于处理网络连接错误中提到:

使用网络对使用分页库显示的数据进行抓取或分页时,切记不要始终将网络视为“可用”或“不可用”,因为许多连接会断断续续或不稳定:

  • 特定服务器可能无法响应网络请求。
  • 设备可能连接到速度较慢或信号较弱的网络。

您的应用应检查每个请求是否失败,并在网络不可用的情况下尽可能正常恢复。例如,如果数据刷新步骤不起作用,您可以提供“重试”按钮供用户选择。如果在数据分页步骤中发生错误,则最好自动重新尝试分页请求。

我认为这段话比较重要的有三点:

  1. 不能通过判断当前网络是否连通判断网络请求是否成功;
  2. 下拉刷新不成功时,页面要显示重试按钮;
  3. 数据下滑分页加载不成功时,需要在下一次滚动时尝试分页请求。

这是官方文档的推荐做法,也是我在这篇文章中最终实现的方式。

二 思路

明确一下页面数据加载逻辑。

  1. 下拉刷新:将调用loadInitial方法和loadAfter方法,在loadInitial成功时即显示下拉刷新成功,失败时即显示下拉刷新失败,并出现重试按钮,点击重试会重新请求数据;

    下拉刷新成功 下拉刷新失败
  2. 上拉加载:存在两种情况,上拉加载会出现:

    • 用户向下滑动过快,数据还未填充,上拉加载会出现,但此时实际上已调用了loadAfter方法,只是数据还未加载成功,加载成功后即显示上拉加载成功,注意此时为了避免重复加载数据,需要去除已添加的数据加载事件,失败后即显示上拉加载失败,并添加数据加载事件,以备下次加载数据;
    上拉加载成功 上拉加载失败
    • 用户向下滑动时,loadAfter方法出现错误,此时需要为上拉加载添加加载事件,用户在下一次滚动时,将出现上拉加载,并调用添加的事件加载数据,后续操作同上。

      这个的展示效果不明显,读者可自行测试。

  3. 全部数据加载完成后,上拉加载需要显示无更多数据

    20201029_095924

这三点就是我们需要考虑的页面数据加载逻辑。

根据页面逻辑可以设计出回调接口为:

public interface NetworkState {
    // 加载成功
    void onSuccess();

    // 加载中
    void onLoading();

    // 初始加载|下拉刷新
    void onLoadInitialError(Runnable runnable, String errorMessage);

    // 分页加载|上拉加载
    void onLoadAfterError(Runnable runnable, String errorMessage);

    // 数据加载全部完成
    void onFinish();
}

下面讲解该接口的具体使用方法。

三 安装

implementation "com.google.android.material:material:1.2.1" // snack bar
implementation 'com.scwang.smart:refresh-layout-kernel:2.0.1'      //核心必须依赖
implementation 'com.scwang.smart:refresh-header-classics:2.0.1'    //经典刷新头

相比于第一篇,添加了谷歌material库和下拉刷新SmartRefreshLayout库。

四 实现

4.1 完善GithubDataSource

主要是对loadInitialloadAfter方法添加错误处理:

@Override
public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<Integer, GithubRes.Item> callback) {
    networkState.onLoading(); // 数据加载中
    Call<GithubRes> call = githubService.query(query, sort, FIRST_PAGE, PAGE_SIZE);
    try {
        Response<GithubRes> resResponse = call.execute();
        GithubRes res = resResponse.body();

        if (res != null) {
            // 数据加载成功
            networkState.onSuccess();
            callback.onResult(res.items, null, FIRST_PAGE + 1);
        } else
            networkState.onLoadInitialError(() -> loadInitial(params, callback), "访问错误!"); // 数据为空
    } catch (IOException e) {
        e.printStackTrace();
        networkState.onLoadInitialError(() -> loadInitial(params, callback), e.getMessage()); // 数据加载失败
    }
}

@Override
public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, GithubRes.Item> callback) {
    Call<GithubRes> call = githubService.query(query, sort, params.key, PAGE_SIZE);
    try {
        GithubRes res = call.execute().body();
        if (res != null) {
            networkState.onSuccess();
            if (!res.complete)
                callback.onResult(res.items, params.key + 1);
            else {
                callback.onResult(res.items, null);
                networkState.onFinish();
            }
        } else
            networkState.onLoadAfterError(() -> loadAfter(params, callback), "访问错误!");
    } catch (IOException e) {
        e.printStackTrace();
        // 回调
        networkState.onLoadAfterError(() -> loadAfter(params, callback), e.getMessage());
    }
}

仔细的读者会发现我仅在loadInitial中添加了networkState.onLoading()方法,这是因为上拉加载会有单独的loading动画。

4.3 在Activity中实现接口

main_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<com.scwang.smart.refresh.layout.SmartRefreshLayout xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/refreshLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity"
        android:orientation="vertical">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
            <EditText
                android:id="@+id/search_repo"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:hint="请输入搜索内容"
                android:imeOptions="actionSearch"
                android:inputType="textNoSuggestions"
                android:selectAllOnFocus="true"
                android:text="Android"/>
            <Button
                android:id="@+id/search"
                android:text="搜索"
                android:backgroundTint="@color/colorAccent"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
        </LinearLayout>
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
        <ProgressBar
            android:id="@+id/progress_circular"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="gone"/>
    </LinearLayout>
</com.scwang.smart.refresh.layout.SmartRefreshLayout>

添加了SmartRefreshLayoutProgressBar

其中SmartRefreshLayout用于下拉刷新和上拉加载,ProgressBar会在初始加载页面中出现。

onSuccess方法实现:

public void onSuccess() {
    runOnUiThread(() -> {
        progressBar.setVisibility(View.GONE);
        if (refresh.getState() == RefreshState.Refreshing)
            refresh.finishRefresh(true);
        if (refresh.getState() == RefreshState.Loading){
            refresh.finishLoadMore(true);
            refresh.setOnLoadMoreListener(null);   
        }
    });
}

此处必须在Ui线程对页面元素的可见性进行修改,因此使用了runOnUiThread方法进行了包装,这里的finishRefreshfinishLoadMore表示下拉刷新和上拉加载已经成功,结束展示。

onLoading方法实现:

public void onLoading() {
    runOnUiThread(() -> {
        progressBar.setVisibility(View.VISIBLE);
    });
}

上文中也已经提到,该方法将仅在初次加载时被触发。

onLoadInitialError方法实现:

public void onLoadInitialError(Runnable runnable, String errorMessage) {
    runOnUiThread(() -> {
        progressBar.setVisibility(View.GONE);
        if (refresh.getState() == RefreshState.Refreshing)
            refresh.finishRefresh(false);
    });
    Snackbar.make(findViewById(android.R.id.content), errorMessage, Snackbar.LENGTH_INDEFINITE)
            .setAction("RETRY", view -> executorService.submit(runnable)).show();
}

初次加载成功,progressBar将调用onSuccess方法隐藏,否则调用onLoadInitialError方法隐藏,由于已经设置在不满一页时不可以上拉加载,因此在这里无需处理上拉加载的状态。

onLoadAfterError方法实现:

public void onLoadAfterError(Runnable runnable, String errorMessage) {
    runOnUiThread(() -> {
        if (refresh.getState() == RefreshState.Loading)
            refresh.finishLoadMore(false);
    });
    refresh.setOnLoadMoreListener(refreshLayout -> executorService.submit(runnable));
}

对应于页面数据加载逻辑:

  1. 当用户向下滑动过快,数据还未填充,上拉加载将出现,但此时DataSource实际上已调用了loadAfter方法,成功后将调用onSuccess方法隐藏,并通过refresh.setOnLoadMoreListener(null);代码删除绑定事件,避免重复加载数据,失败后将调用onLoadAfterError方法隐藏,并通过refresh.setOnLoadMoreListener(refreshLayout -> executorService.submit(runnable));代码绑定事件,用于下次请求数据;
  2. 用户向下滑动时,loadAfter方法出现错误,将调用onLoadAfterError方法,为上拉加载绑定事件,在用户下次上拉时会调用绑定的事件,成功后将调用onSuccess方法隐藏,并删除绑定事件,失败后将调用onLoadAfterError方法隐藏,并绑定事件,用户下次上拉时将重复该流程。

onFinish方法实现:

public void onFinish() {
    runOnUiThread(refresh::finishLoadMoreWithNoMoreData);
}

SmartRefreshLayout确实封装的相当好了,只需要这一个方法即可完成任务。

刷新方法实现:

VieModel:
private MutableLiveData<GithubDataSource> mGithubDataSource;
public void setItemPagedList(String query, String sort) {
    GithubDataSourceFactory factory = new GithubDataSourceFactory(query, sort, networkState);
    ---
    mGithubDataSource = factory.getmGithubDataSource();
    ---
}
public void invalidate() {
    if (mGithubDataSource != null && mGithubDataSource.getValue() != null)
        mGithubDataSource.getValue().invalidate();
}

Activity:

refresh.setOnRefreshListener(refreshLayout -> {
    mainViewModel.invalidate();
});

用户下拉刷新时,若成功,将调用onSuccess方法隐藏,否则调用onLoadInitialError方法隐藏,并出现Snackbar,显示重试按钮,点击重试按钮即可重新加载数据,当然也可以下拉刷新重新加载数据。

五 结语

实际上,在开始写Paging博客之前,我在项目开发时就已经被这个问题阻碍了很久,一直没有想到一种比较好的解决方式,网上关于Paging的博客也大多未介绍处理网络错误的部分,或者处理的不那么优雅,这次我花费了一天的时间,认真思考,也耐心的阅读了很多使用Kotlin语言处理网络错误的文章,他们给了我很大的启发,感谢他们!最终,自认为找到了一种相对优雅的实现方式,当然,还是存在很多可以改进的地方,如果各位有更好的方式,或者在使用中发现了问题,还麻烦批评指正。

六 源码

源码已经上传至github,欢迎star。

七 参考

  1. Using Android Paging library with Retrofit
  2. Paging Library with Android MVVM
  3. Android官方文档