分页库
学习如何在 Android 应用中实现无限滚动。
分页简介
Paging 3 库是实现 RecyclerView 中无限滚动的有力工具,它通过将大型数据集划分为较小的数据块,并逐步将这些数据加载到视图中,从而实现平滑高效的数据加载。当用户在列表中滚动时,这种机制能够确保流畅且高效的性能。
此外,Paging 3 还简化了从本地数据库和网络源加载数据的过程。
分页的优势
- Paging 库因其内置的缓存系统而具有资源效率优势。该系统有助于提升数据加载与检索速度,从而为用户提供更流畅的整体体验。因此,该库能够在处理大规模数据集时依然高效利用系统资源,保持性能最优。
-
Paging 库内置了错误处理支持,可自动应对数据加载或检索过程中可能出现的问题。该功能包括刷新数据或重试失败请求的能力,确保瞬时错误不会中断用户体验。这保证了
RecyclerView中显示的数据始终准确且最新。 -
与 Paging 3 库一同提供的
RecyclerView适配器在分页方面具有高度灵活性,能够在用户接近当前页面末尾时自动加载新数据并发起请求。这确保了RecyclerView中的数据始终保持最新,使用户在浏览列表时获得无缝体验。
实现分页
我们将逐步演示如何在 Android 应用中实现分页功能。
为此,我们需要以下组件,具体说明如下,首先通过示意图进行展示:
为了演示分页的实际效果,我们将构建一个电影应用,使用 RecyclerView 展示电影列表。当用户滚动列表时,应用会根据上述分页技术自动加载更多数据。要实际体验该功能,只需运行应用并滚动电影列表。当到达当前页面末尾时,应用将自动加载更多数据并添加到列表中,使用户可以无缝继续滚动。
No configuration found for ID: 1b0d9ddd35f6b97d8287d892febe8ee1f6739904470926f6288906fd0db13043
我们将分析电影应用中使用的以下组件:
PagingSourcePagingDataPagerPagingConfig
现在开始!
但在进入类之前,必须先在 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 能够实时展示正在获取的最新数据。