前言
在现代移动应用中,大数据列表的展示是一种常见的需求。然而,处理大量数据的网络加载过程可能会带来一些挑战,比如长时间的等待、空数据或错误信息的展示。为了提供更好的用户体验,我们需要在数据加载过程中显示合适的加载状态,使用户能够清楚地知道当前数据的加载状态,并在必要时提供相应的反馈和交互。
Paging 3 是 Google 推出的用于处理大数据列表的分页库,它为我们提供了一种优雅的方式来处理数据加载和分页。而在 Paging 3 中,处理网络数据加载状态是非常重要的一部分。用户可能会在滚动列表时不断加载新的数据,或者在加载失败时需要进行错误重试。在这些情况下,合理的加载状态展示对于提供良好的用户体验至关重要。
在本文中,我们将深入探讨在使用 Paging 3 进行网络数据加载时,如何处理加载状态,以及如何使用 Paging 3 提供的工具和方法来实现更好的用户体验。我们将学习如何使用 LoadState
和 CombinedLoadStates
类来获取加载状态信息,并在 RecyclerView
中添加加载状态项。同时,还会介绍如何自定义加载状态视图,以满足应用的样式和需求,并实现点击加载失败视图时进行重试的功能。
Paging 3 加载状态
在使用 Paging 3 进行网络数据加载时,了解和理解加载状态是非常重要的。Paging 3 提供了一些类和接口来表示数据加载状态,帮助我们在界面上展示合适的加载提示和交互。主要涉及的概念有两个:
-
加载状态类型
LoadState
-
加载类型
LoadType
-
加载状态的类型LoadState:
Paging3中用LoadState
来表示加载状态, LoadState
是一个枚举类,表示加载状态的不同阶段。它包含以下几个状态:
加载状态类型 | 含义 |
---|---|
LoadState.Loading |
表示数据正在加载中,通常在加载初始化数据以及用户滚动列表时加载更多数据时会出现这个状态 |
LoadState.NotLoading |
表示数据加载成功,该状态包含一个endOfPaginationReached 字段,为false 表示已经没有更多数据了,为true 表示还有更多数据,用户还可以继续下拉或上拉加载更多 |
LoadState.Error |
表示加载数据时发生了错误,可能是网络请求失败或其他异常情况 |
-
加载类型
LoadType
加载类型 | 含义 |
---|---|
refresh |
表示整个数据列表的刷新状态,包括初始化列表数据,整个列表数据的刷新 |
prepend |
表示加载上一页数据,对应用户下拉更多,常见的状态和含义与 refresh 类似,只是在这里表示的是加载上一页数据的状态。 |
append |
表示加载下一页数据,对应用户上拉加载更多,常见的状态和含义与 refresh 类似,只是在这里表示的是加载下一页数据的状态。 |
-
CombinedLoadStates
类
CombinedLoadStates
是一个包含了所有加载类型的类,即它包含三个属性:分别对应三种加载类型refresh
、prepend
、append
,它将数据加载的各个类型进行了组合。通过 CombinedLoadStates
,我们可以同时获得每个加载类型对应的详细信息,包括加载状态以及相关的异常信息。
在 RecyclerView 中,我们通常需要根据 CombinedLoadStates
的内容来决定是否展示加载状态项或错误视图。例如,当列表正在加载中时,我们可以显示加载中的动画或提示信息;当数据加载成功时,我们可以隐藏加载状态项;当加载失败时,我们可以展示加载失败的提示并提供重试功能,比如:
private val loadStateListener = object : Function1<CombinedLoadStates, Unit> {override fun invoke(combinedLoadStates: CombinedLoadStates) {when (val state = combinedLoadStates.refresh) {is LoadState.Loading -> {println("refresh Loading")// Show loading animation or loading indicator}is LoadState.Error -> {// Show error message or handle error stateval errorMessage = state.error.message ?: "Unknown error"// Display the error message to the user}is LoadState.NotLoading -> {println("refresh NotLoading")// Hide loading animation or loading indicator}}when (val state = combinedLoadStates.append) {is LoadState.Loading -> {println("append Loading")// Show loading animation or loading indicator}is LoadState.Error -> {// Show error message or handle error stateval errorMessage = state.error.message ?: "Unknown error"// Display the error message to the user}is LoadState.NotLoading -> {println("append NotLoading")// Hide loading animation or loading indicator}}}}private val loadStateListener = object : Function1<CombinedLoadStates, Unit> { override fun invoke(combinedLoadStates: CombinedLoadStates) { when (val state = combinedLoadStates.refresh) { is LoadState.Loading -> { println("refresh Loading") // Show loading animation or loading indicator } is LoadState.Error -> { // Show error message or handle error state val errorMessage = state.error.message ?: "Unknown error" // Display the error message to the user } is LoadState.NotLoading -> { println("refresh NotLoading") // Hide loading animation or loading indicator } } when (val state = combinedLoadStates.append) { is LoadState.Loading -> { println("append Loading") // Show loading animation or loading indicator } is LoadState.Error -> { // Show error message or handle error state val errorMessage = state.error.message ?: "Unknown error" // Display the error message to the user } is LoadState.NotLoading -> { println("append NotLoading") // Hide loading animation or loading indicator } } } }private val loadStateListener = object : Function1<CombinedLoadStates, Unit> { override fun invoke(combinedLoadStates: CombinedLoadStates) { when (val state = combinedLoadStates.refresh) { is LoadState.Loading -> { println("refresh Loading") // Show loading animation or loading indicator } is LoadState.Error -> { // Show error message or handle error state val errorMessage = state.error.message ?: "Unknown error" // Display the error message to the user } is LoadState.NotLoading -> { println("refresh NotLoading") // Hide loading animation or loading indicator } } when (val state = combinedLoadStates.append) { is LoadState.Loading -> { println("append Loading") // Show loading animation or loading indicator } is LoadState.Error -> { // Show error message or handle error state val errorMessage = state.error.message ?: "Unknown error" // Display the error message to the user } is LoadState.NotLoading -> { println("append NotLoading") // Hide loading animation or loading indicator } } } }
在PagingSoure中发送数据加载状态
首先我们根据上文,可以知道,数据加载状态分为:LoadState.Loading
/LoadState.NotLoading
/LoadState.Error
而对于PagingSource
来讲,它的返回值为LoadResult
,LoadResult
有三种可选值:
LoadResult 类型 |
含义 |
---|---|
LoadResult.Page |
表示成功加载了一页数据。当 PagingSource 成功加载了一页数据时,会使用 LoadResult.Page 来封装这些数据,并提供与加载结果相关的额外信息,如是否有前一页数据 (prevKey ) 和是否有后一页数据 (nextKey ),用于支持分页加载 |
LoadResult.Error |
表示加载数据过程中出现了错误。当 PagingSource 加载数据过程中发生了异常或错误,会使用 LoadResult.Error 来封装错误信息,例如网络请求失败或数据解析错误。这样,外部代码可以根据 LoadResult.Error 中的错误信息进行错误处理和展示逻辑。 |
LoadResult.Invalid |
表示当前的PagingSource 加载的数据无效或不可用。返回Invalid 将触发Paging 框架调用invalidate 重新构建PagingSource 实例,从而抛弃当前无效的PagingSource 实例 |
一般情况下,我们不需要考虑LoadResult.Invalid
的状态,本文也暂且不做此种状态的讨论。
通常,当数据请求正常时,PagingSource
返回LoadResult.Page
,对应数据加载状态LoadState.NotLoading
,当请求出错时,返回LoadResult.Error
,而当框架判断加载状态为LoadState.Loading
才会调用PagingSource.load
方法,因此三种数据加载状态和PagingSource
对应了起来:
class MyPagingSource(private val apiService: ApiService) : PagingSource<Int, ListItem>() {override fun getRefreshKey(state: PagingState<Int, ListItem>): Int? {return null}override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ListItem> {try {// 获取请求的页数val pageNumber = params.key ?: 1var listItems = mutableListOf<ListItem>()// 加载数据apiService.getItems(pageNumber, params.loadSize).onSuccess {listItems.addAll(it)}.onFailure {// 加载出错: 返回加载错误的 LoadResult.Error 对象return LoadResult.Error(it)}val prevKey = if (pageNumber > 1) pageNumber - 1 else nullval nextKey = if (listItems.isNotEmpty()) pageNumber + 1 else null// 本次加载结束: 构建 LoadResult.Page 对象并返回return LoadResult.Page(data = listItems,prevKey = prevKey,nextKey = nextKey)} catch (e: Exception) {// 加载出错: 返回加载错误的 LoadResult.Error 对象return LoadResult.Error(e)}}}class MyPagingSource(private val apiService: ApiService) : PagingSource<Int, ListItem>() { override fun getRefreshKey(state: PagingState<Int, ListItem>): Int? { return null } override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ListItem> { try { // 获取请求的页数 val pageNumber = params.key ?: 1 var listItems = mutableListOf<ListItem>() // 加载数据 apiService.getItems(pageNumber, params.loadSize) .onSuccess { listItems.addAll(it) }.onFailure { // 加载出错: 返回加载错误的 LoadResult.Error 对象 return LoadResult.Error(it) } val prevKey = if (pageNumber > 1) pageNumber - 1 else null val nextKey = if (listItems.isNotEmpty()) pageNumber + 1 else null // 本次加载结束: 构建 LoadResult.Page 对象并返回 return LoadResult.Page( data = listItems, prevKey = prevKey, nextKey = nextKey ) } catch (e: Exception) { // 加载出错: 返回加载错误的 LoadResult.Error 对象 return LoadResult.Error(e) } } }class MyPagingSource(private val apiService: ApiService) : PagingSource<Int, ListItem>() { override fun getRefreshKey(state: PagingState<Int, ListItem>): Int? { return null } override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ListItem> { try { // 获取请求的页数 val pageNumber = params.key ?: 1 var listItems = mutableListOf<ListItem>() // 加载数据 apiService.getItems(pageNumber, params.loadSize) .onSuccess { listItems.addAll(it) }.onFailure { // 加载出错: 返回加载错误的 LoadResult.Error 对象 return LoadResult.Error(it) } val prevKey = if (pageNumber > 1) pageNumber - 1 else null val nextKey = if (listItems.isNotEmpty()) pageNumber + 1 else null // 本次加载结束: 构建 LoadResult.Page 对象并返回 return LoadResult.Page( data = listItems, prevKey = prevKey, nextKey = nextKey ) } catch (e: Exception) { // 加载出错: 返回加载错误的 LoadResult.Error 对象 return LoadResult.Error(e) } } }
网络加载状态处理示例
在以下的示例中,我们将解决两个问题:
-
初始化数据加载成功或失败的处理
-
加载下一页时成功或失败的处理
初始化数据加载状态处理
-
定义不同加载状态下的UI
加载状态可以分为:
-
正在加载数据
-
加载失败
-
加载成功
-
加载的内容为空
- 正在加载数据的UI: loading圈 + 正在加载中的文案
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"><ProgressBarandroid:id="@+id/progressBar"style="@style/ProgressBarStyle"android:layout_width="100dp"android:layout_height="100dp"android:layout_gravity="center"app:layout_constraintBottom_toTopOf="@+id/textView"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintHorizontal_bias="0.5"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintVertical_chainStyle="packed" /><TextViewandroid:id="@+id/textView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="8dp"android:text="正在加载中"android:textColor="@color/color_797980"android:textSize="32sp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintHorizontal_bias="0.5"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/progressBar" /></androidx.constraintlayout.widget.ConstraintLayout><?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <ProgressBar android:id="@+id/progressBar" style="@style/ProgressBarStyle" android:layout_width="100dp" android:layout_height="100dp" android:layout_gravity="center" app:layout_constraintBottom_toTopOf="@+id/textView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" /> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="正在加载中" android:textColor="@color/color_797980" android:textSize="32sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/progressBar" /> </androidx.constraintlayout.widget.ConstraintLayout><?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <ProgressBar android:id="@+id/progressBar" style="@style/ProgressBarStyle" android:layout_width="100dp" android:layout_height="100dp" android:layout_gravity="center" app:layout_constraintBottom_toTopOf="@+id/textView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" /> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="正在加载中" android:textColor="@color/color_797980" android:textSize="32sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/progressBar" /> </androidx.constraintlayout.widget.ConstraintLayout>
- 加载失败的UI: 加载异常的提示 + 重新加载的按钮
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/F2EAE7E7"xmlns:app="http://schemas.android.com/apk/res-auto"><ImageViewandroid:id="@+id/ivEmpty"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/ic_net_error"app:layout_constraintBottom_toTopOf="@id/emptyText"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintVertical_chainStyle="packed" /><TextViewandroid:id="@+id/emptyText"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/load_error_retry"android:textSize="24sp"app:layout_constraintBottom_toBottomOf="@id/btnRetry"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/ivEmpty" /><Buttonandroid:id="@+id/btnRetry"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@color/A09D9D"android:layout_marginTop="18dp"android:paddingTop="10dp"android:paddingBottom="10dp"android:paddingStart="40dp"android:paddingEnd="40dp"android:text="@string/load_retry"android:textSize="24sp"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toBottomOf="@id/emptyText"app:layout_constraintBottom_toBottomOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout><?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/F2EAE7E7" xmlns:app="http://schemas.android.com/apk/res-auto"> <ImageView android:id="@+id/ivEmpty" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_net_error" app:layout_constraintBottom_toTopOf="@id/emptyText" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" /> <TextView android:id="@+id/emptyText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/load_error_retry" android:textSize="24sp" app:layout_constraintBottom_toBottomOf="@id/btnRetry" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/ivEmpty" /> <Button android:id="@+id/btnRetry" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@color/A09D9D" android:layout_marginTop="18dp" android:paddingTop="10dp" android:paddingBottom="10dp" android:paddingStart="40dp" android:paddingEnd="40dp" android:text="@string/load_retry" android:textSize="24sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/emptyText" app:layout_constraintBottom_toBottomOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout><?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/F2EAE7E7" xmlns:app="http://schemas.android.com/apk/res-auto"> <ImageView android:id="@+id/ivEmpty" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_net_error" app:layout_constraintBottom_toTopOf="@id/emptyText" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" /> <TextView android:id="@+id/emptyText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/load_error_retry" android:textSize="24sp" app:layout_constraintBottom_toBottomOf="@id/btnRetry" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/ivEmpty" /> <Button android:id="@+id/btnRetry" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@color/A09D9D" android:layout_marginTop="18dp" android:paddingTop="10dp" android:paddingBottom="10dp" android:paddingStart="40dp" android:paddingEnd="40dp" android:text="@string/load_retry" android:textSize="24sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/emptyText" app:layout_constraintBottom_toBottomOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
- 加载内容为空的UI: 暂无内容 + 图标
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"><ImageViewandroid:id="@+id/ivEmpty"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/ic_empty"app:layout_constraintBottom_toTopOf="@id/emptyText"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintVertical_chainStyle="packed" /><TextViewandroid:id="@+id/emptyText"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="20dp"android:text="暂无内容"android:textSize="28sp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/ivEmpty" /></androidx.constraintlayout.widget.ConstraintLayout><?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/ivEmpty" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_empty" app:layout_constraintBottom_toTopOf="@id/emptyText" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" /> <TextView android:id="@+id/emptyText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:text="暂无内容" android:textSize="28sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/ivEmpty" /> </androidx.constraintlayout.widget.ConstraintLayout><?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/ivEmpty" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_empty" app:layout_constraintBottom_toTopOf="@id/emptyText" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" /> <TextView android:id="@+id/emptyText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:text="暂无内容" android:textSize="28sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/ivEmpty" /> </androidx.constraintlayout.widget.ConstraintLayout>
在定义完各个不同加载状态的UI后,我们就需要将这几个UI 放到一个全局的加载状态XML中:
<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><ViewStubandroid:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/loadingView"android:layout="@layout/layout_progress_dialog" /><ViewStubandroid:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/emptyView"android:layout="@layout/layout_empty" /><ViewStubandroid:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/errorView"android:layout="@layout/layout_error" /></FrameLayout><?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ViewStub android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/loadingView" android:layout="@layout/layout_progress_dialog" /> <ViewStub android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/emptyView" android:layout="@layout/layout_empty" /> <ViewStub android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/errorView" android:layout="@layout/layout_error" /> </FrameLayout><?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ViewStub android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/loadingView" android:layout="@layout/layout_progress_dialog" /> <ViewStub android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/emptyView" android:layout="@layout/layout_empty" /> <ViewStub android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/errorView" android:layout="@layout/layout_error" /> </FrameLayout>
这里我们用ViewStub
来做延迟加载,防止不必要的资源浪费,比如加载数据成功并且数据量大于0,因此加载错误的XML和加载内容为空内容XML就没有必要加载出来。
-
自定义View控制不同状态下UI的切换
- 首先我们定义多个页面状态:
sealed class PageState {object Loading : PageState()object Empty : PageState()class Error(val e: Throwable) : PageState()object NotLoading : PageState()}sealed class PageState { object Loading : PageState() object Empty : PageState() class Error(val e: Throwable) : PageState() object NotLoading : PageState() }sealed class PageState { object Loading : PageState() object Empty : PageState() class Error(val e: Throwable) : PageState() object NotLoading : PageState() }
根据上一节,我们定义四种不同的状态
- 根据页面状态控制不同状态UI的显隐:
自定义类PageStateHost
,继承自FrameLayout
class PageStateHost(private val context: Context,attributeSet: AttributeSet? = null,private val refreshAction: () -> Unit = {}): FrameLayout(context, attributeSet) {private val binding by lazy {LayoutPageStateBinding.inflate(LayoutInflater.from(context), this, false)}private var errorView: View? = nullfun init(viewGroup: ViewGroup) {(parent as? ViewGroup)?.removeView(binding.root)viewGroup.addView(binding.root,ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT)}fun setState(state: PageState) {when (state) {is PageState.Loading -> {binding.loadingView.visibility = View.VISIBLEbinding.errorView.visibility = View.GONEbinding.emptyView.visibility = View.GONE}is PageState.Error -> {binding.loadingView.visibility = View.GONEerrorView = errorView ?: binding.errorView.inflate()errorView?.visibility = VISIBLEbinding.emptyView.visibility = View.GONEval retryBtn = errorView?.findViewById<Button>(R.id.btnRetry)retryBtn?.setOnClickListener {refreshAction()}}is PageState.Empty -> {binding.loadingView.visibility = View.GONEbinding.errorView.visibility = View.GONEbinding.emptyView.visibility = View.VISIBLE}else -> {binding.loadingView.visibility = View.GONEbinding.errorView.visibility = View.GONEbinding.emptyView.visibility = View.GONE}}}}class PageStateHost( private val context: Context, attributeSet: AttributeSet? = null, private val refreshAction: () -> Unit = {} ): FrameLayout(context, attributeSet) { private val binding by lazy { LayoutPageStateBinding.inflate(LayoutInflater.from(context), this, false) } private var errorView: View? = null fun init(viewGroup: ViewGroup) { (parent as? ViewGroup)?.removeView(binding.root) viewGroup.addView( binding.root, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) } fun setState(state: PageState) { when (state) { is PageState.Loading -> { binding.loadingView.visibility = View.VISIBLE binding.errorView.visibility = View.GONE binding.emptyView.visibility = View.GONE } is PageState.Error -> { binding.loadingView.visibility = View.GONE errorView = errorView ?: binding.errorView.inflate() errorView?.visibility = VISIBLE binding.emptyView.visibility = View.GONE val retryBtn = errorView?.findViewById<Button>(R.id.btnRetry) retryBtn?.setOnClickListener { refreshAction() } } is PageState.Empty -> { binding.loadingView.visibility = View.GONE binding.errorView.visibility = View.GONE binding.emptyView.visibility = View.VISIBLE } else -> { binding.loadingView.visibility = View.GONE binding.errorView.visibility = View.GONE binding.emptyView.visibility = View.GONE } } } }class PageStateHost( private val context: Context, attributeSet: AttributeSet? = null, private val refreshAction: () -> Unit = {} ): FrameLayout(context, attributeSet) { private val binding by lazy { LayoutPageStateBinding.inflate(LayoutInflater.from(context), this, false) } private var errorView: View? = null fun init(viewGroup: ViewGroup) { (parent as? ViewGroup)?.removeView(binding.root) viewGroup.addView( binding.root, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) } fun setState(state: PageState) { when (state) { is PageState.Loading -> { binding.loadingView.visibility = View.VISIBLE binding.errorView.visibility = View.GONE binding.emptyView.visibility = View.GONE } is PageState.Error -> { binding.loadingView.visibility = View.GONE errorView = errorView ?: binding.errorView.inflate() errorView?.visibility = VISIBLE binding.emptyView.visibility = View.GONE val retryBtn = errorView?.findViewById<Button>(R.id.btnRetry) retryBtn?.setOnClickListener { refreshAction() } } is PageState.Empty -> { binding.loadingView.visibility = View.GONE binding.errorView.visibility = View.GONE binding.emptyView.visibility = View.VISIBLE } else -> { binding.loadingView.visibility = View.GONE binding.errorView.visibility = View.GONE binding.emptyView.visibility = View.GONE } } } }
在此处,我们用ViewStub
来控制不同状态UI的显隐,避免不必要的资源浪费。
为了该FrameLayout
使用的方便,我们定义了init
方法,将需要展示状态的ViewGroup
传入。另外为了能够让用户在加载异常时重新发起数据加载请求,构造函数中提供lambda
表达式,让用户自定义重新加载的操作。
- 页面中使用自定义View实现页面加载状态展示
在BaseActivity
中定义初始化PageStateHost
的逻辑
abstract class BaseActivity: AppCompatActivity() {private var pageStateHost: PageStateHost? = nullfun initPageState(viewGroup: ViewGroup) {if (this is PagingPageState) {pageStateHost = PageStateHost(view.context, refreshAction = refreshDataAction).apply {init(viewGroup)lifecycleScope.launch {pageState.collectLatest {setState(it)}}}}}interface PagingPageState {val pageState: MutableStateFlow<PageState>val refreshDataAction: () -> Unit}}abstract class BaseActivity: AppCompatActivity() { private var pageStateHost: PageStateHost? = null fun initPageState(viewGroup: ViewGroup) { if (this is PagingPageState) { pageStateHost = PageStateHost(view.context, refreshAction = refreshDataAction).apply { init(viewGroup) lifecycleScope.launch { pageState.collectLatest { setState(it) } } } } } interface PagingPageState { val pageState: MutableStateFlow<PageState> val refreshDataAction: () -> Unit } }abstract class BaseActivity: AppCompatActivity() { private var pageStateHost: PageStateHost? = null fun initPageState(viewGroup: ViewGroup) { if (this is PagingPageState) { pageStateHost = PageStateHost(view.context, refreshAction = refreshDataAction).apply { init(viewGroup) lifecycleScope.launch { pageState.collectLatest { setState(it) } } } } } interface PagingPageState { val pageState: MutableStateFlow<PageState> val refreshDataAction: () -> Unit } }
在这里,我们首先定义一个接口PagingPageState
,该接口中使用Flow
来记录当前页面的状态,并定义无参函数作为重新加载的逻辑。
接着,我们定义initPageState
方法用户初始化PageStateHost
,在该方法中,我们首先调用PageStateHost.init
方法,然后调用Flow
的collectLatest
方法用于收集最新页面状态,从而调用setState
展示对应状态的UI
-
在MainActivity中实现PagingPageState,完成页面状态类初始化,并发送页面加载状态
class MainActivity : BaseActivity(), BaseActivity.PagingPageState {private val loadStateListener = object : Function1<CombinedLoadStates, Unit> {override fun invoke(combinedLoadStates: CombinedLoadStates) {when (val state = combinedLoadStates.refresh) {is LoadState.Loading -> {lifecycleScope.launch {pageState.emit(PageState.Loading)}}is LoadState.Error -> {// Show error message or handle error stateval errorMessage = state.error.message ?: "Unknown error"// Display the error message to the userlifecycleScope.launch {pageState.emit(PageState.Error(Throwable(errorMessage)))}}is LoadState.NotLoading -> {// Hide loading animation or loading indicatorlifecycleScope.launch {if (adapter.itemCount == 0) {pageState.emit(PageState.Empty)} else {pageState.emit(PageState.NotLoading)}}}}}}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(binding.root)initPageState(binding.root)initView()addDataObserve()}override val pageState: MutableStateFlow<PageState> = MutableStateFlow(PageState.Loading)override val refreshDataAction: () -> Unit = {adapter.refresh()}}class MainActivity : BaseActivity(), BaseActivity.PagingPageState { private val loadStateListener = object : Function1<CombinedLoadStates, Unit> { override fun invoke(combinedLoadStates: CombinedLoadStates) { when (val state = combinedLoadStates.refresh) { is LoadState.Loading -> { lifecycleScope.launch { pageState.emit(PageState.Loading) } } is LoadState.Error -> { // Show error message or handle error state val errorMessage = state.error.message ?: "Unknown error" // Display the error message to the user lifecycleScope.launch { pageState.emit(PageState.Error(Throwable(errorMessage))) } } is LoadState.NotLoading -> { // Hide loading animation or loading indicator lifecycleScope.launch { if (adapter.itemCount == 0) { pageState.emit(PageState.Empty) } else { pageState.emit(PageState.NotLoading) } } } } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) initPageState(binding.root) initView() addDataObserve() } override val pageState: MutableStateFlow<PageState> = MutableStateFlow(PageState.Loading) override val refreshDataAction: () -> Unit = { adapter.refresh() } }class MainActivity : BaseActivity(), BaseActivity.PagingPageState { private val loadStateListener = object : Function1<CombinedLoadStates, Unit> { override fun invoke(combinedLoadStates: CombinedLoadStates) { when (val state = combinedLoadStates.refresh) { is LoadState.Loading -> { lifecycleScope.launch { pageState.emit(PageState.Loading) } } is LoadState.Error -> { // Show error message or handle error state val errorMessage = state.error.message ?: "Unknown error" // Display the error message to the user lifecycleScope.launch { pageState.emit(PageState.Error(Throwable(errorMessage))) } } is LoadState.NotLoading -> { // Hide loading animation or loading indicator lifecycleScope.launch { if (adapter.itemCount == 0) { pageState.emit(PageState.Empty) } else { pageState.emit(PageState.NotLoading) } } } } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) initPageState(binding.root) initView() addDataObserve() } override val pageState: MutableStateFlow<PageState> = MutableStateFlow(PageState.Loading) override val refreshDataAction: () -> Unit = { adapter.refresh() } }
在MainActivity
中,我们实现PagingPageState
接口,并实现相关变量的获取,其中,重新加载操作为调用adapter.refresh()
,刷新整个列表。
在PagingAdapter加载状态的监听中发送对应的页面状态,从而展示相应的状态UI。
在完成上述工作后,我们来看一下加载成功和加载失败的效果:
加载更多的状态处理
在完成页面状态展示的逻辑之后,我们需要来处理加载更多的状态转换。加载更多状态UI的处理其实就是列表Footer
UI的处理,跟页面状态处理的流程类似,包含以下几个步骤:
-
定义状态UI;
-
定义
LoadStateAdapter
,根据Footer
不同的加载状态展示不同UI; -
将
LoadStateAdapter
关联到RecyclerView上 -
重新出发加载更多的处理
-
定义Footer的状态UI
Footer
的状态定义不需要太复杂,只要一个 loading
圈+ 文字描述
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="40dp"><ProgressBarandroid:id="@+id/progressBar"style="@style/ProgressBarStyle"android:layout_width="24dp"android:layout_height="24dp"android:layout_gravity="center"app:layout_constraintEnd_toStartOf="@+id/textView"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintVertical_bias="0.5"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintHorizontal_chainStyle="packed" /><TextViewandroid:id="@+id/textView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/loading"android:textColor="@color/color_797980"android:textSize="18sp"android:layout_marginStart="16dp"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintVertical_bias="0.5"app:layout_constraintStart_toEndOf="@+id/progressBar" /></androidx.constraintlayout.widget.ConstraintLayout><?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="40dp"> <ProgressBar android:id="@+id/progressBar" style="@style/ProgressBarStyle" android:layout_width="24dp" android:layout_height="24dp" android:layout_gravity="center" app:layout_constraintEnd_toStartOf="@+id/textView" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintVertical_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintHorizontal_chainStyle="packed" /> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/loading" android:textColor="@color/color_797980" android:textSize="18sp" android:layout_marginStart="16dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintVertical_bias="0.5" app:layout_constraintStart_toEndOf="@+id/progressBar" /> </androidx.constraintlayout.widget.ConstraintLayout><?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="40dp"> <ProgressBar android:id="@+id/progressBar" style="@style/ProgressBarStyle" android:layout_width="24dp" android:layout_height="24dp" android:layout_gravity="center" app:layout_constraintEnd_toStartOf="@+id/textView" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintVertical_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintHorizontal_chainStyle="packed" /> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/loading" android:textColor="@color/color_797980" android:textSize="18sp" android:layout_marginStart="16dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintVertical_bias="0.5" app:layout_constraintStart_toEndOf="@+id/progressBar" /> </androidx.constraintlayout.widget.ConstraintLayout>
-
定义LoadStateAdapter
Paging3已经贴心的为我们考虑到添加Footer
或者Header
的场景,我们只需要继承LoadStateAdapter
,并重写onCreateViewHolder
和onBindViewHolder
方法即可:
class DefaultFooterAdapter(private val retryCallback: () -> Unit = {}): LoadStateAdapter<DefaultFooterAdapter.FooterViewHolder>() {override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): FooterViewHolder {return FooterViewHolder(LayoutVerticalListFooterBinding.inflate(LayoutInflater.from(parent.context),parent,false))}override fun onBindViewHolder(holder: FooterViewHolder, loadState: LoadState) {holder.bindData(loadState)}inner class FooterViewHolder(private val binding: LayoutVerticalListFooterBinding) : RecyclerView.ViewHolder(binding.root) {fun bindData(state: LoadState) {// 根据不同的状态,显示不同的内容,比如加载中、加载错误提示、加载完成等when (state) {is LoadState.Loading -> {binding.progressBar.visibility = View.VISIBLEbinding.textView.visibility = View.VISIBLEbinding.textView.text = "正在加载中"}is LoadState.Error -> {binding.progressBar.visibility = View.GONEbinding.textView.visibility = View.VISIBLEbinding.textView.text = "加载异常"}is LoadState.NotLoading -> {binding.progressBar.visibility = View.GONEif(state.endOfPaginationReached) {binding.textView.visibility = View.VISIBLEbinding.textView.text = "到底了"} else {binding.textView.visibility = View.GONEbinding.textView.text = ""}}}}}}class DefaultFooterAdapter(private val retryCallback: () -> Unit = {}) : LoadStateAdapter<DefaultFooterAdapter.FooterViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): FooterViewHolder { return FooterViewHolder( LayoutVerticalListFooterBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder(holder: FooterViewHolder, loadState: LoadState) { holder.bindData(loadState) } inner class FooterViewHolder(private val binding: LayoutVerticalListFooterBinding) : RecyclerView.ViewHolder(binding.root) { fun bindData(state: LoadState) { // 根据不同的状态,显示不同的内容,比如加载中、加载错误提示、加载完成等 when (state) { is LoadState.Loading -> { binding.progressBar.visibility = View.VISIBLE binding.textView.visibility = View.VISIBLE binding.textView.text = "正在加载中" } is LoadState.Error -> { binding.progressBar.visibility = View.GONE binding.textView.visibility = View.VISIBLE binding.textView.text = "加载异常" } is LoadState.NotLoading -> { binding.progressBar.visibility = View.GONE if(state.endOfPaginationReached) { binding.textView.visibility = View.VISIBLE binding.textView.text = "到底了" } else { binding.textView.visibility = View.GONE binding.textView.text = "" } } } } } }class DefaultFooterAdapter(private val retryCallback: () -> Unit = {}) : LoadStateAdapter<DefaultFooterAdapter.FooterViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): FooterViewHolder { return FooterViewHolder( LayoutVerticalListFooterBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder(holder: FooterViewHolder, loadState: LoadState) { holder.bindData(loadState) } inner class FooterViewHolder(private val binding: LayoutVerticalListFooterBinding) : RecyclerView.ViewHolder(binding.root) { fun bindData(state: LoadState) { // 根据不同的状态,显示不同的内容,比如加载中、加载错误提示、加载完成等 when (state) { is LoadState.Loading -> { binding.progressBar.visibility = View.VISIBLE binding.textView.visibility = View.VISIBLE binding.textView.text = "正在加载中" } is LoadState.Error -> { binding.progressBar.visibility = View.GONE binding.textView.visibility = View.VISIBLE binding.textView.text = "加载异常" } is LoadState.NotLoading -> { binding.progressBar.visibility = View.GONE if(state.endOfPaginationReached) { binding.textView.visibility = View.VISIBLE binding.textView.text = "到底了" } else { binding.textView.visibility = View.GONE binding.textView.text = "" } } } } } }
上述代码也是常规的重写RecyclerView.Adapter流程,我们在FooterViewHolde.binData方法中,将加载状态和页面UI进行绑定。
-
将DefaultFooterAdapter和RecyclerView关联
private fun initView() {binding.apply {list.layoutManager = LinearLayoutManager(this@MainActivity)// 直接使用PagingDataAdapter.withLoadStateFooter即可list.adapter = adapter.withLoadStateFooter(DefaultFooterAdapter())adapter.addLoadStateListener(loadStateListener)}}private fun initView() { binding.apply { list.layoutManager = LinearLayoutManager(this@MainActivity) // 直接使用PagingDataAdapter.withLoadStateFooter即可 list.adapter = adapter.withLoadStateFooter(DefaultFooterAdapter()) adapter.addLoadStateListener(loadStateListener) } }private fun initView() { binding.apply { list.layoutManager = LinearLayoutManager(this@MainActivity) // 直接使用PagingDataAdapter.withLoadStateFooter即可 list.adapter = adapter.withLoadStateFooter(DefaultFooterAdapter()) adapter.addLoadStateListener(loadStateListener) } }
在PagingDataAdapter
中,有一个withLoadStateFooter
方法:
/*** Create a [ConcatAdapter] with the provided [LoadStateAdapter]s displaying the* [LoadType.APPEND] [LoadState] as a list item at the start of the presented list.** @see LoadStateAdapter* @see withLoadStateHeaderAndFooter* @see withLoadStateHeader*/fun withLoadStateFooter(footer: LoadStateAdapter<*>): ConcatAdapter {addLoadStateListener { loadStates ->footer.loadState = loadStates.append}return ConcatAdapter(this, footer)}/** * Create a [ConcatAdapter] with the provided [LoadStateAdapter]s displaying the * [LoadType.APPEND] [LoadState] as a list item at the start of the presented list. * * @see LoadStateAdapter * @see withLoadStateHeaderAndFooter * @see withLoadStateHeader */ fun withLoadStateFooter( footer: LoadStateAdapter<*> ): ConcatAdapter { addLoadStateListener { loadStates -> footer.loadState = loadStates.append } return ConcatAdapter(this, footer) }/** * Create a [ConcatAdapter] with the provided [LoadStateAdapter]s displaying the * [LoadType.APPEND] [LoadState] as a list item at the start of the presented list. * * @see LoadStateAdapter * @see withLoadStateHeaderAndFooter * @see withLoadStateHeader */ fun withLoadStateFooter( footer: LoadStateAdapter<*> ): ConcatAdapter { addLoadStateListener { loadStates -> footer.loadState = loadStates.append } return ConcatAdapter(this, footer) }
该方法会返回一个ConcatAdapter
实例,ConcatAdapter
将PagingDataAdapter
和LoadStateDapter
进行了组合,在数据加载时,会讲不同的数据分发给不同的Adapter,比如此处,会将网络加载回来的数据给PagingDataAdapter
,将加载状态给到LoadStateAdapter
。
另外,在withLoadStateFooter
方法中,为LoadStateAdapter
添加一个LoadStateListener
,并将append
类型的LoadState
赋值给LoadStateAdapter
,从而达到在状态改变时,LoadStateAdapter
的onBindViewHolder
会被调用。
至此,我们就完成了加载更多的状态处理,但是为了能够在加载异常时重新触发加载,我们还需要额外多做一些工作。
-
重新触发加载更多
通常用户习惯会反复上拉列表来触发加载更多,因此我们只需要监听Touch
事件,计算用户上拉距离,超过阈值时调用PagingDataAdapter.retry()
方法,因此我们需要对第三步的使用稍微改造一下:
@SuppressLint("ClickableViewAccessibility")fun RecyclerView.setAdapterWithDefaultFooter(adapter: MyPagingDataAdapter) {this.adapter = adapter.withLoadStateFooter(DefaultFooterAdapter())var downEventY = 0fsetOnTouchListener { _, event ->when (event.action) {MotionEvent.ACTION_DOWN -> {downEventY = event.rawY}MotionEvent.ACTION_UP -> {if (downEventY - event.rawY > 20) {adapter.retry()}}}false}}@SuppressLint("ClickableViewAccessibility") fun RecyclerView.setAdapterWithDefaultFooter(adapter: MyPagingDataAdapter) { this.adapter = adapter.withLoadStateFooter(DefaultFooterAdapter()) var downEventY = 0f setOnTouchListener { _, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { downEventY = event.rawY } MotionEvent.ACTION_UP -> { if (downEventY - event.rawY > 20) { adapter.retry() } } } false } }@SuppressLint("ClickableViewAccessibility") fun RecyclerView.setAdapterWithDefaultFooter(adapter: MyPagingDataAdapter) { this.adapter = adapter.withLoadStateFooter(DefaultFooterAdapter()) var downEventY = 0f setOnTouchListener { _, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { downEventY = event.rawY } MotionEvent.ACTION_UP -> { if (downEventY - event.rawY > 20) { adapter.retry() } } } false } }
我们给RecyclerView
定义一个扩展函数setAdapterWithDefaultFooter(adapter: MyPagingDataAdapter)
,在该方法中,我们首先调用adapter.withLoadStateFooter(DefaultFooterAdapter())
设置Footer
,接着设置TouchListener
,记录ACTION_DOWN
事件的纵坐标,当用户抬手时,计算抬手点的纵坐标,从而计算用户滑动列表距离,如果超过20,则触发adapter.retry()
重新加载。
至此,我们完成加载更多的Footer状态处理,运行下看个效果吧:
总结
在Paging3网络框架下,处理网络加载状态要比自己写一套稍微方便一些,特别是Footer
的处理,主要涉及以下几点:
-
数据加载状态
LoadState
; -
加载类型
LoadType
; -
包含所有加载类型的类
CombinedLoadStates
-
加载状态监听
addLoadStateListener
; -
LoadStateAdapter
本文只是对通用的状态处理提出了一套实现方法,更多更复杂场景下的状态展示还需要更加针对性的处理,但是大致上都脱离不了这一套,只是可能会做更多的特殊处理。