一 前言
在上一篇Android Paging组件在MVVM架构中的使用指南(一)中,我初步介绍了Android Jetpack Paging
组件在MVVM
架构中的基础使用方法,其中数据源使用了Github api
也就是网络数据源,因此我们自然会想到:网络连接错误怎么办?事实上,如果按照上一篇中实现的话,一旦在数据分页时遇到网络连接错误,即使网络重新稳定,也不会重新触发分页了,这显然是不能接受的,因此我将在本篇着手处理网络连接错误。
官方文档中关于处理网络连接错误中提到:
使用网络对使用分页库显示的数据进行抓取或分页时,切记不要始终将网络视为“可用”或“不可用”,因为许多连接会断断续续或不稳定:
- 特定服务器可能无法响应网络请求。
- 设备可能连接到速度较慢或信号较弱的网络。
您的应用应检查每个请求是否失败,并在网络不可用的情况下尽可能正常恢复。例如,如果数据刷新步骤不起作用,您可以提供“重试”按钮供用户选择。如果在数据分页步骤中发生错误,则最好自动重新尝试分页请求。
我认为这段话比较重要的有三点:
- 不能通过判断当前网络是否连通判断网络请求是否成功;
- 下拉刷新不成功时,页面要显示重试按钮;
- 数据下滑分页加载不成功时,需要在下一次滚动时尝试分页请求。
这是官方文档的推荐做法,也是我在这篇文章中最终实现的方式。
二 思路
明确一下页面数据加载逻辑。
- 下拉刷新:将调用
loadInitial
方法和loadAfter
方法,在loadInitial
成功时即显示下拉刷新成功,失败时即显示下拉刷新失败,并出现重试按钮,点击重试会重新请求数据;
上拉加载:存在两种情况,上拉加载会出现:
用户向下滑动过快,数据还未填充,上拉加载会出现,但此时实际上已调用了
loadAfter
方法,只是数据还未加载成功,加载成功后即显示上拉加载成功,注意此时为了避免重复加载数据,需要去除已添加的数据加载事件,失败后即显示上拉加载失败,并添加数据加载事件,以备下次加载数据;
用户向下滑动时,
loadAfter
方法出现错误,此时需要为上拉加载添加加载事件,用户在下一次滚动时,将出现上拉加载,并调用添加的事件加载数据,后续操作同上。
这个的展示效果不明显,读者可自行测试。
全部数据加载完成后,上拉加载需要显示无更多数据。
这三点就是我们需要考虑的页面数据加载逻辑。
根据页面逻辑可以设计出回调接口为:
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[1]库。
四 实现
4.1 完善GithubDataSource
主要是对loadInitial
和loadAfter
方法添加错误处理:
@Override
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback callback) {
networkState.onLoading(); // 数据加载中
Call call = githubService.query(query, sort, FIRST_PAGE, PAGE_SIZE);try {
Response 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()); // 数据加载失败
}
}@Overridepublic void loadAfter(@NonNull LoadParams params, @NonNull LoadCallback callback) {
Call 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">
<LinearLayoutandroid:id="@+id/container"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"android:orientation="vertical">
<LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal">
<EditTextandroid: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"/>
<Buttonandroid:id="@+id/search"android:text="搜索"android:backgroundTint="@color/colorAccent"android:layout_width="wrap_content"android:layout_height="wrap_content"/>
LinearLayout>
<androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recycler_view"android:layout_width="match_parent"android:layout_height="wrap_content"/>
<ProgressBarandroid:id="@+id/progress_circular"android:layout_width="match_parent"android:layout_height="wrap_content"android:visibility="gone"/>
LinearLayout>
com.scwang.smart.refresh.layout.SmartRefreshLayout>
添加了SmartRefreshLayout
和ProgressBar
。
其中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
方法进行了包装,这里的finishRefresh
和finishLoadMore
表示下拉刷新和上拉加载已经成功,结束展示。
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));
}
对应于页面数据加载逻辑:
- 当用户向下滑动过快,数据还未填充,上拉加载将出现,但此时
DataSource
实际上已调用了loadAfter
方法,成功后将调用onSuccess
方法隐藏,并通过refresh.setOnLoadMoreListener(null);
代码删除绑定事件,避免重复加载数据,失败后将调用onLoadAfterError
方法隐藏,并通过refresh.setOnLoadMoreListener(refreshLayout -> executorService.submit(runnable));
代码绑定事件,用于下次请求数据; - 用户向下滑动时,
loadAfter
方法出现错误,将调用onLoadAfterError
方法,为上拉加载绑定事件,在用户下次上拉时会调用绑定的事件,成功后将调用onSuccess
方法隐藏,并删除绑定事件,失败后将调用onLoadAfterError
方法隐藏,并绑定事件,用户下次上拉时将重复该流程。
onFinish
方法实现:
public void onFinish() {
runOnUiThread(refresh::finishLoadMoreWithNoMoreData);
}
SmartRefreshLayout
确实封装的相当好了,只需要这一个方法即可完成任务。
刷新方法实现:
VieModel:
private MutableLiveData 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[2],欢迎star。
七 参考
- Using Android Paging library with Retrofit[3]
- Paging Library with Android MVVM[4]
- Android官方文档[5]
参考资料
[1]SmartRefreshLayout: https://github.com/scwang90/SmartRefreshLayout
[2]github: https://github.com/Civitasv/Android-Paging
[3]Using Android Paging library with Retrofit: https://medium.com/@SaurabhSandav/using-android-paging-library-with-retrofit-fa032cac15f8
[4]Paging Library with Android MVVM: https://www.linkedin.com/pulse/paging-library-android-mvvm-paul-hundal
[5]Android官方文档: https://developer.android.com/topic/libraries/architecture/paging