Logo
Section 1 / 7

分页库

学习如何在 Android 应用中实现无限滚动。

导入

分页库

学习如何在 Android 应用中实现无限滚动。

分页简介

Paging 3 库是实现 RecyclerView 中无限滚动的有力工具,它通过将大型数据集划分为较小的数据块,并逐步将这些数据加载到视图中,从而实现平滑高效的数据加载。当用户在列表中滚动时,这种机制能够确保流畅且高效的性能。

此外,Paging 3 还简化了从本地数据库和网络源加载数据的过程。

分页的优势

  • Paging 库因其内置的缓存系统而具有资源效率优势。该系统有助于提升数据加载与检索速度,从而为用户提供更流畅的整体体验。因此,该库能够在处理大规模数据集时依然高效利用系统资源,保持性能最优。
  • Paging 库内置了错误处理支持,可自动应对数据加载或检索过程中可能出现的问题。该功能包括刷新数据或重试失败请求的能力,确保瞬时错误不会中断用户体验。这保证了 RecyclerView 中显示的数据始终准确且最新。

  • 与 Paging 3 库一同提供的 RecyclerView 适配器在分页方面具有高度灵活性,能够在用户接近当前页面末尾时自动加载新数据并发起请求。这确保了 RecyclerView 中的数据始终保持最新,使用户在浏览列表时获得无缝体验。

实现分页

我们将逐步演示如何在 Android 应用中实现分页功能。

为此,我们需要以下组件,具体说明如下,首先通过示意图进行展示:

...
分页数据流示意图

为了演示分页的实际效果,我们将构建一个电影应用,使用 RecyclerView 展示电影列表。当用户滚动列表时,应用会根据上述分页技术自动加载更多数据。要实际体验该功能,只需运行应用并滚动电影列表。当到达当前页面末尾时,应用将自动加载更多数据并添加到列表中,使用户可以无缝继续滚动。

No configuration found for ID: 1b0d9ddd35f6b97d8287d892febe8ee1f6739904470926f6288906fd0db13043

我们将分析电影应用中使用的以下组件:

  • PagingSource
  • PagingData
  • Pager
  • PagingConfig

现在开始!

但在进入类之前,必须先在 build.gradle 文件中添加以下依赖项,并确保成功同步。

1implementation "androidx.paging:paging-runtime:3.1.1"

为了从 TMDB API 获取热门电影,我们需要定义一个服务类,用于向 API 发起网络请求并解析返回的数据。为保护 API 密钥安全,建议将其存储在 local.properties 文件中,并通过 BuildConfig 类将其集成到代码中。这样可以在服务类中访问密钥的同时,确保其安全性。

以下是定义 MovieApi 类以从 TMDB API 获取热门电影的示例:

1interface MovieApi { 2 3 companion object { 4 const val api_key = BuildConfig.MOVIE_API_KEY 5 } 6 7 @GET("movie/popular?") 8 suspend fun getPopularMovies( 9 @Query("api_key") api_key: String = MovieApi.api_key, 10 @Query("page") page: Int 11 ): Response<MovieResponse> 12}

以下是 Movie 和 MovieResponse 的模型类:

1data class Movie( 2 @SerializedName("adult") val adult: Boolean, 3 @SerializedName("backdrop_path") val backdrop_path: String, 4 @SerializedName("genre_ids") val genre_ids: List<Int>, 5 @SerializedName("id") val id: Int, 6 @SerializedName("original_language") val original_language: String, 7 @SerializedName("original_title") val original_title: String, 8 @SerializedName("overview") val overview: String, 9 @SerializedName("popularity") val popularity: Double, 10 @SerializedName("poster_path") val poster_path: String, 11 @SerializedName("release_date") val release_date: String, 12 @SerializedName("title") val title: String, 13 @SerializedName("video") val video: Boolean, 14 @SerializedName("vote_average") val vote_average: Double, 15 @SerializedName("vote_count") val vote_count: Int 16)

MovieResponse 包含电影列表及总页数:

1data class MovieResponse( 2 @SerializedName("page") val page: Int, 3 @SerializedName("results") val movies: List<Movie>, 4 @SerializedName("total_pages") val pages: Int 5)

PagingSource

PagingSource 是一个抽象类,负责定义如何从网络、本地数据库或文件中获取分页数据。

下面的代码展示了如何实现 PagingSource 类以从网络 API 获取数据。

1class MoviePagingSource(private val service: MovieApi) : PagingSource<Int, Movie>() { 2 override fun getRefreshKey(state: PagingState<Int, Movie>): Int { 3 return 1 4 } 5 6 override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> { 7 return try { 8 val currentPage = params.key ?: 1 9 val response = service.getPopularMovies(MovieApi.api_key, currentPage) 10 val responseData = mutableListOf<Movie>() 11 val data = response.body()?.movies ?: emptyList() 12 responseData.addAll(data) 13 14 LoadResult.Page( 15 data = responseData, 16 prevKey = if (currentPage == 1) null else currentPage - 1, 17 nextKey = currentPage + 1 18 ) 19 } catch (e: Exception) { 20 LoadResult.Error(e) 21 } catch (e: HttpException) { 22 LoadResult.Error(e) 23 } 24 } 25}

在本应用中,我们使用 PagingSource 从 API 远程获取数据,作为我们的数据源。

我们传入之前创建的 MovieApi 类,以便 PagingSource 能够调用它并获取结果。

MoviePagingSource 类实现了 PagingSource,其中包含两个参数:键(key)和值(value)。在本例中,键是表示页码的 Int 值,值是 Movie 数据类,即要加载的数据类型。

接下来,MoviePagingSource 必须重写两个方法:load() 和 getRefreshKey()。

  • load(params: LoadParams<Int>):此方法在用户滚动时被调用,用于加载更多数据,正是这一机制实现了应用中的无限滚动效果。如果 getPopularMovies() 调用成功,电影数据将被封装在 LoadResult.Page 对象中,并附带前一页或下一页的键;若调用失败,则返回 LoadResult.Error 对象,表示异常。我们使用 params.key 获取当前键(初始键),首次加载时为 null。我们将其设为 1 作为初始页码。获取 prevKey 时,将当前页码减 1,因为最小页码为 1。若当前页码为 1,则 prevKey 为 null,而 nextKey 为 currentPage + 1。

  • getRefreshKey(state: PagingState<Int, Movie>):此方法用于后续对 PagingSource.load() 的刷新调用,用于在初始或当前页加载完成后加载其他页面。

Pager

Pager 作为 PagingSource 与 UI 之间的中介,按需逐次获取数据。它通过调用 PagingSource 的 load() 方法并传入 loadParams 来实现,然后接收返回的 LoadResult 对象。Pager 通常在应用的数据层中创建,特别是在 Repository 中。

1class Repository @Inject constructor(private val api: MovieApi) { 2 val listData = Pager(PagingConfig(pageSize = 1)) { 3 MoviePagingSource(api) 4 }.flow 5}

为了构造 Pager,我们还使用了 PagingConfig。PagingConfig 允许通过传递参数来调整 Pager 的行为,其中包括 pageSize,用于定义从 PagingSource 加载的项目数量。

有多种方式可以消费 PagingData,例如使用 Pager.flow 或 Pager.LiveData。本例中我们选择使用 Pager.flow,因为它支持异步流式传输数据至 UI,适用于用户滚动时的动态更新。相比之下,Pager.LiveData 仅能持有单一值,不适合流式数据传输,仅在仅需单个值时可用于分页。

ViewModel

我们将通过 ViewModel 向视图提供数据。如下所示,我们将 Repository 传入以调用 Pager 中的 PagingData。

1@HiltViewModel 2class MovieViewModel @Inject constructor(private val repository: Repository) : ViewModel() { 3 val movies = repository.listData.cachedIn(viewModelScope) 4}

我们通过 Repository 实例调用 listData。在 ViewModel 的作用域中使用 cachedIn 方法有助于管理生命周期并防止内存泄漏,从而提升应用整体效率。

View

我们来看 MovieFragment 类。我们使用 ViewModel 作为数据源与视图之间的抽象层。

1@AndroidEntryPoint 2class MovieFragment : Fragment(R.layout.fragment_movie) { 3 private lateinit var popularMovieAdapter: PopularMovieAdapter 4 private var fragmentMovieBinding: FragmentMovieBinding? = null 5 private val movieViewModel: MovieViewModel by viewModels() 6 7 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 8 super.onViewCreated(view, savedInstanceState) 9 val binding = FragmentMovieBinding.bind(view) 10 fragmentMovieBinding = binding 11 12 initMembers() 13 setUpViews(binding) 14 fetchMovies() 15 } 16 17 private fun initMembers() { 18 popularMovieAdapter = PopularMovieAdapter() 19 } 20 21 private fun fetchMovies() { 22 lifecycleScope.launch { 23 movieViewModel.movies.collectLatest { 24 popularMovieAdapter.submitData(it) 25 } 26 } 27 } 28 29 private fun setUpViews(binding: FragmentMovieBinding) { 30 binding.popularMoviesRecyclerview.layoutManager = GridLayoutManager(activity, 3) 31 binding.popularMoviesRecyclerview.adapter = popularMovieAdapter 32 } 33}

fetchMovies() 方法启动一个协程以处理异步数据流。collectLatest 操作符用于收集流发出的最新值并更新 UI。通过执行 submitData() 方法,适配器将使用流发出的最新数据更新显示内容。这使得 UI 能够实时展示正在获取的最新数据。

ViewModel 模式

新闻动态