前言
在前几篇中,主要讲解了Jetpack其他组件相关,在本篇中,将会对Paging组件进行讲解。
1、认识Jetpack—Paging
1.1 Paging组件的意义
分页加载是在应用程序开发过程中十分常见的需求,Paging就是Google为了方便Android开发者完成分页加载而设计的一个组件,它为几种常见的分页机制提供了统一的解决方案
,让我们可以吧更多的经历专注在业务代码上。
1.2 Paging支持的架构类型
如图所示
Paging支持的架构类型:
-
网络数据
- 对网络数据进行分页加载,是最常见的一种分页需求,也是我们学习的重点。不同的公司针对分页机制所设计的API接口通常也不太一样,但总体而言可以归纳为3种。
- Paging组件提供了3种不同的方案,以应对不同的分页机制。
- 分别是:
PositionDataSource、PageKeyedDataSource、ItemKeyedDataSource
-
数据库
- 掌握了网络数据分页之后,数据库数据分页将会容易很多,无非就是数据源的替换
-
网络+数据库
- 出于用户体验的考虑,我们通常会对网络数据进行缓存,以便用户在下次打开应用程序时,应用程序可以先展示缓存数据。
- 我们通常会利用数据库对网络数据进行缓存,这意味着我们需要同时处理好网络和数据库这两个数据源。
- 多数据源会让业务逻辑变得更为复杂,所以!我们通常采用单一数据源作为解决方案。
- 既从网络获取的数据,直接缓存进数据库,列表只从数据库这个唯一的数据源获取数据,这里我们会学习到
BoundaryCallback
1.4 Paging的工作原理
如图所示
Paging的3个核心类
-
PagedListAdpater
- RecycleView 需要搭配适配器使用,如果希望使用Paging组件,适配器需要继承自PagedListAdpater
-
PagedList
- PagedList负责通知DataSource何时获取数据,以及如何获取数据。例如,何时加载第一页/下一页,第一页加载的数量、提前多少条数据开始执行预加载等,从DataSource获取的数据将存储在PagedList中
-
DataSource
- 在DataSource中执行具体的数据载入工作,数据可以来自网络,也可以来自本地数据库,根据分页机制的不同,Paging为我们提供了3种DataSource
- 数据的载入需要在工作线程中进行
概念终于说完了,阔以开始实战了!
2、准备工作
2.1 基础配置
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
...略
def retrofit_version = '2.9.0'
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
implementation 'com.squareup.picasso:picasso:2.71828'
...略
注意:这里我们使用的paging:paging
版本为:2.1.2
,在下一篇Flow+Paging文章中,将会使用3+
版本。
2.2 RetrofitClient
class RetrofitClient {
companion object {
private const val BASE_URL = "http://192.168.0.104:8080/pagingserver/"
private var mInstance: RetrofitClient? = null
@Synchronized
@JvmStatic
fun getInstance(): RetrofitClient {
if (mInstance == null) {
mInstance = RetrofitClient()
}
return mInstance as RetrofitClient
}
}
private var retrofit: Retrofit? = null
constructor() {
retrofit = Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(getClient())
.build()
}
private fun getClient(): OkHttpClient {
return OkHttpClient.Builder().build()
}
fun getApi(): Api {
return retrofit!!.create(Api::class.java)
}
}
OK,准备工作做好了,上面不是提到过三个核心类,那么就按照上面图解顺序依次讲解:
3、Paging的三个核心类
3.1 PagedListAdpater
//分析点1
class MoviePagedListAdapter : PagedListAdapter<Movie, MovieViewHolder> {
//<html>None of the following functions can be called with the arguments supplied:<br/>protected/*protected and package*/ constructor PagedListAdapter<T : Any!, VH : RecyclerView.ViewHolder!>(config: AsyncDifferConfig<Movie!>) defined in androidx.paging.PagedListAdapter<br/>protected/*protected and package*/ constructor PagedListAdapter<T : Any!, VH : RecyclerView.ViewHolder!>(diffCallback: DiffUtil.ItemCallback<Movie!>) defined in androidx.paging.PagedListAdapter
private var context: Context? = null
//分析点2
constructor(context: Context) : super(DIFF_CALLBACK) {
this.context = context
}
companion object {
//分析点3
//差分,只更新需要更新的元素,而不是整个刷新数据源
private val DIFF_CALLBACK: DiffUtil.ItemCallback<Movie> =
object : DiffUtil.ItemCallback<Movie>() {
//调用以检查两个对象是否代表同一个项目。
//如果两个项目代表相同的对象,则为真,如果它们不同,则为假。
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem === newItem
}
//调用以检查两个项目是否具有相同的数据。
//此信息用于检测项目的内容是否已更改。
//如果项目的内容相同则为真,如果不同则为假。
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val root = LayoutInflater.from(context).inflate(R.layout.item, parent, false)
return MovieViewHolder(root)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
//单个实体类可直接通过getItem 获取
val movie = getItem(position)
if (movie != null) {
//使用
Picasso.get()
.load(movie.cover)
.placeholder(R.drawable.ic_launcher_foreground)
.error(R.drawable.ic_launcher_foreground)
.into(holder.imageView)
if (movie.title!!.length >= 8) {
movie.title = movie.title!!.substring(0, 7)
}
holder.textViewTitle!!.text = movie.title
holder.textViewRate!!.text = movie.rate
}
}
class MovieViewHolder : RecyclerView.ViewHolder {
var imageView: ImageView? = null
var textViewTitle: TextView? = null
var textViewRate: TextView? = null
constructor(itemView: View) : super(itemView) {
imageView = itemView.findViewById(R.id.imageView)
textViewTitle = itemView.findViewById(R.id.textViewTitle)
textViewRate = itemView.findViewById(R.id.textViewRate)
}
}
}
代码解析:
- 分析点1:这里我们可以看到对应RecyclerView绑定的Adapter并非直接继承
RecyclerView.Adapter
,而是PagedListAdapter<xxx, xxxHolder>
- 分析点2:当然使用
PagedListAdapter
,就需要调用父类构造方法并传对应参数DIFF_CALLBACK
,而对应的参数定义在分析点3位置 - 分析点3:我们可以看到,该
DIFF_CALLBACK
属性,继承了DiffUtil.ItemCallback<Movie>
抽象类,并且实现了对应的抽象方法。里面那个方法就是差分数据比较,也就是说,当有数据发生改变时,UI只会刷新数据改变部分,而非全部!
这个Adpater代码就这块,这里写的比较全,下面所有案例都同用这一个。OK,现在看第二个核心类!
这里用到Movie
实体类,
class Movie {
var id = 0
var title: String? = null
var rate: String? = null
var cover: String? = null
}
3.2 PagedList
class MovieViewModel : ViewModel {
var moviePagedList: LiveData<PagedList<Movie>>? = null
constructor() : super() {
val config = PagedList.Config.Builder() //设置控件占位
//使用此配置传递 false 以禁用 PagedLists 中的空占位符。 <p> 如果未设置,则默认为 true。
.setEnablePlaceholders(false)
.setPageSize(MovieDataSource.PER_PAGE) //设置当距离底部还有多少条数据时开始加载下一页
.setPrefetchDistance(2) //设置首次加载的数量
.setInitialLoadSizeHint(MovieDataSource.PER_PAGE * 2)
.setMaxSize(65536 * MovieDataSource.PER_PAGE)
.build()
moviePagedList = LivePagedListBuilder(MovieDataSourceFactory(), config).build()
}
}
通过这段代码我们可知:
LiveData<PagedList<Movie>>
这里使用了LiveData
,由此我们可知,UI刷新的主要责任就在于moviePagedList
变量,而该变量使用了PagedList
moviePagedList =LivePagedListBuilder(MovieDataSourceFactory(), config).build()
这里通过LivePagedListBuilder.build()
给对应的LiveData数据赋值,第二个参数config
就是上面所配置的内容- 这里第一个参数用到了
MovieDataSourceFactory
,因此进入MovieDataSourceFactory
看看!
MovieDataSourceFactory
class MovieDataSourceFactory : DataSource.Factory<Int, Movie>() {
override fun create(): DataSource<Int, Movie> {
return MovieDataSource()
}
}
这里我们可以看到:
MovieDataSourceFactory
继承了DataSource.Factory<Int, Movie>()
- 然后在重写的
create()
,返回了MovieDataSource
实例
因此进入第三个核心类,DataSource 看看!
3.3 DataSource
class MovieDataSource : PositionalDataSource<Movie>() {
companion object {
const val PER_PAGE = 8
}
/**
* 加载初始列表数据
*/
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Movie>) {
}
/**
* 调用以从 DataSource 加载一系列数据(加载下一页数据)
*/
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Movie>) {
}
}
我们看到
- 这里的
MovieDataSource
继承了PositionalDataSource
- 重写了
loadInitial
方法,该方法表示:加载初始列表数据时调用 - 重写了
loadRange
方法,该方法表示:加载下一页数据时调用
这里的DataSource
继承了PositionalDataSource
,那么只能继承PositionalDataSource
么?如果说有其他的方式,那么对应作用是什么呢?
4、DataSource三种继承方式
不同的继承方式,可实现不同Json的分页效果!
我们先以已经继承过的PositionalDataSource
开始讲解
4.1 继承PositionalDataSource
那么 继承PositionalDataSource有什么特点呢?
如图所示
适用于可通过任意位置加载数据,且目标数据源数量固定的情况。例如,请求时携带参数:start=2&count=5
,则表示向服务器请求从第2条数据开始往后延5个数据~
那么!
4.1.1 先来看看服务端的代码
public class PositionalDataSourceServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("PositionalDataSourceServlet doGet");
int start = Integer.parseInt(request.getParameter("start"));
int count = Integer.parseInt(request.getParameter("count"));
System.out.println("start:"+start+",count:"+count);
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("count",count);
jsonObject.addProperty("start",start);
jsonObject.addProperty("total",ServerStartupListener.MOVIES.size());
Gson gson = new Gson();
//从MOVIES集合中取出一段数据出来
List<Movie> searchList = new ArrayList<>();
for (int i = start; i < start + count; i++) {
try{
searchList.add(ServerStartupListener.MOVIES.get(i));
}catch (IndexOutOfBoundsException e){
//索引越界,跳出循环
System.out.println(e.getMessage());
break;
}
}
JsonArray jsonArray = (JsonArray) gson.toJsonTree(searchList, new TypeToken<List<Movie>>(){}.getType());
jsonObject.add("subjects",jsonArray);
response.setHeader("Content-type", "text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print(jsonObject.toString());
out.close();
}
}
很原始的写法,不过我们从中可以看到对应的数据结构为:
{
"count":8,
"start":0,
"total":100,
"subjects":[
{
"id":35076714,
"title":"扎克·施奈德版正义联盟",
"cover":"https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2634360594.webp",
"rate":"8.9"
}
]
}
因此
4.1.2 客户端解析json实体类
class Movies {
//当前返回的数量
var count = 0
//起始位置
var start = 0
//一共有多少条
var total = 0
@SerializedName("subjects")
var movieList: List<Movie>? = null
override fun toString(): String {
return "Movies{" +
"count=" + count +
", start=" + start +
", total=" + total +
", movieList=" + movieList +
'}'
}
}
现在实体类写好了,那么来看看最终MovieDataSource
长啥样
4.1.3 MovieDataSource
class MovieDataSource : PositionalDataSource<Movie>() {
companion object {
const val PER_PAGE = 8
}
/**
* 加载初始列表数据
*/
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Movie>) {
val startPosition = 0
RetrofitClient.getInstance()
.getApi()
//startPosition 第一个;PER_PAGE 一页加载多少个
.getMovies(startPosition, PER_PAGE)
.enqueue(object : Callback<Movies> {
override fun onResponse(call: Call<Movies?>, response: Response<Movies?>) {
if (response.body() != null) {
//把数据传递给PagedList
callback.onResult(
response.body()!!.movieList!!,
response.body()!!.start,
response.body()!!.total
)
Log.d("hqk", "loadInitial:" + response.body()!!.movieList)
}
}
override fun onFailure(call: Call<Movies?>, t: Throwable) {}
})
}
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Movie>) {
RetrofitClient.getInstance()
.getApi()
//params.startPosition 上一页最后一个,也就是下一页第一个;PER_PAGE 一页加载多少个
.getMovies(params.startPosition, PER_PAGE)
.enqueue(object : Callback<Movies?> {
override fun onResponse(call: Call<Movies?>, response: Response<Movies?>) {
if (response.body() != null) {
//把数据传递给PagedList
callback.onResult(response.body()!!.movieList!!)
Log.d("hqk", "loadRange:" + response.body()!!.movieList)
}
}
override fun onFailure(call: Call<Movies?>, t: Throwable) {}
})
}
}
代码解析:
-
loadInitial与loadRange
这两个方法在上面讲过,分别是首次加载,分页加载调用的方法,通过callback.onResult
返回当前请求的网络数据 -
而
callback.onResult
对应的callback
,就是对应方法的LoadInitialCallback与LoadRangeCallback
-
而对应调用了
getApi().getMovies
方法,那么来看看这方法:interface Api { @GET("pds.do") fun getMovies( @Query("start") since: Int, @Query("count") perPage: Int ): Call<Movies> }
-
这里可以看到使用了
start与count
,他们分别代表对应页数开始的下标以及每页的个数 -
而在
loadRange
方法里,对应params.startPosition
表示,上一页最后一个下标以及当前页面开始下标
万事俱备,只差UI使用!
4.1.4 对应UI使用
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
val adapter = MoviePagedListAdapter(this)
recyclerView.adapter = adapter
val movieViewModel = MovieViewModel()
movieViewModel.moviePagedList!!.observe(this, Observer<PagedList<Movie>> {
adapter.submitList(it)
})
}
}
这里我们看到
- 使用了
adapter.submitList(it)
代码,而这个方法是PagedListAdapter
里面所提供的 - 它的作用:就是与对应
PagedListAdapter
里面的差分数据相互配合,然后显示对应Adapter
的数据
来看看使用效果
对应后台打印
PositionalDataSourceServlet doGet
start:0,count:8
PositionalDataSourceServlet doGet
start:8,count:8
综上所述,当实例化MovieViewModel
时,将会通过Paging三大核心类进入到对应DataSource,先是启用对应loadInitial
方法来加载初始页,当加载下一页时,将会调用loadRange
方法,加载下一页数据,然后修改对应LiveData
数据,因此通过observe
通知UI刷新数据!
接下来就是第二种继承方式!
4.2 继承PageKeyedDataSource
那么继承PageKeyedDataSource有什么作用呢??
如图所示
该方式适用于数据源以页的方式进行请求的情况。例如,若请求时携带的参数为page=2&pageSize=5
,则表示数据源以5条数据为一页,当前返回第二页的5条数据。
那么!
4.2.1 先来看看服务端的代码
public class PageKeyedDataSourceServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("PageKeyedDataSourceServlet doGet");
int page = Integer.parseInt(request.getParameter("page"));
int pagesize = Integer.parseInt(request.getParameter("pagesize"));
System.out.println("page:"+page+",pagesize:"+pagesize);
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("has_more",true);
Gson gson = new Gson();
//从MOVIES集合中取出一段数据出来
List<Movie> searchList = new ArrayList<>();
int end = page * pagesize;
int begin = end - pagesize;
System.out.println("begin:"+begin+",end:"+end);
for (int i = begin; i < end; i++) {
try{
searchList.add(ServerStartupListener.MOVIES.get(i));
}catch (IndexOutOfBoundsException e){
//索引越界,跳出循环
System.out.println(e.getMessage());
if (i > ServerStartupListener.MOVIES.size()) {
jsonObject.addProperty("has_more",false);
}
break;
}
}
JsonArray jsonArray = (JsonArray) gson.toJsonTree(searchList, new TypeToken<List<Movie>>(){}.getType());
jsonObject.add("subjects",jsonArray);
response.setHeader("Content-type", "text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print(jsonObject.toString());
out.close();
}
}
通过这段代码可知,对应Json格式为:
{
"has_more":true,
"subjects":[
{
"id":35076714,
"title":"扎克·施奈德版正义联盟",
"cover":"https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2634360594.webp",
"rate":"8.9"
}
]
}
因此!
4.2.2 客服端解析json实体类
class Movies {
@SerializedName("has_more")
var hasMore = false
@SerializedName("subjects")
var movieList: List<Movie>? = null
override fun toString(): String {
return "Movies{" +
"hasMore=" + hasMore +
", movieList=" + movieList +
'}'
}
}
现在实体类写好了,那么来看看最终MovieDataSource
长啥样
4.2.3 MovieDataSource
class MovieDataSource : PageKeyedDataSource<Int, Movie>() {
companion object {
const val PER_PAGE = 8
const val FIRST_PAGE = 1
}
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, Movie>) {
RetrofitClient.getInstance()
.getApi()
.getMovies(
FIRST_PAGE,
PER_PAGE
).enqueue(object : Callback<Movies> {
override fun onResponse(call: Call<Movies>, response: Response<Movies>) {
if (response.body() != null) {
//把数据传递给PagedList
callback.onResult(
response.body()!!.movieList!!,
null,
MovieDataSource.FIRST_PAGE + 1
)
Log.d("hqk", "loadInitial:" + response.body()!!.movieList)
}
}
override fun onFailure(call: Call<Movies?>, t: Throwable) {}
})
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Movie>) {
Log.d("hqk", "loadInitial loadBefore")
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Movie>) {
RetrofitClient.getInstance()
.getApi()
.getMovies(params.key, PER_PAGE)
.enqueue(object : Callback<Movies?> {
override fun onResponse(call: Call<Movies?>, response: Response<Movies?>) {
if (response.body() != null) {
//把数据传递给PagedList
val nextKey = if (response.body()!!.hasMore) params.key + 1 else null
callback.onResult(response.body()!!.movieList!!, nextKey)
Log.d("hqk", "loadInitial loadAfter :" + response.body()!!.movieList)
}
}
override fun onFailure(call: Call<Movies?>, t: Throwable) {}
})
}
}
代码解析:
-
loadInitial与loadAfter
这两个方法分别是首次加载,分页加载调用的方法,通过callback.onResult
返回当前请求的网络数据 -
而
callback.onResult
对应的callback
,就是对应方法的LoadInitialCallback与LoadCallback
-
而对应调用了
getApi().getMovies
方法,那么来看看这方法:interface Api { // @GET("pds.do") // fun getMovies( // @Query("start") since: Int, // @Query("count") perPage: Int // ): Call<Movies> @GET("pkds.do") fun getMovies( @Query("page") page: Int, @Query("pagesize") pagesize: Int ): Call<Movies> }
-
这里可以看到使用了
page与pagesize
,他们分别代表对应页数以及每页的个数 -
在
loadInitial
方法里,当首页加载完成调用callback.onResult
时,传入了MovieDataSource.FIRST_PAGE + 1
这个表示下次直接从第二页开始加载数据 -
而在
loadAfter
方法里,通过response.body()!!.hasMore
表示是否有下一页,由此来决定下一页是否+1
万事俱备,只差UI使用!
因为UI使用代码与运行效果一样,因此这里就不再次贴对应代码以及对应运行效果了!
直接看后台打印日志
PageKeyedDataSourceServlet doGet
page:1,pagesize:8
begin:0,end:8
PageKeyedDataSourceServlet doGet
page:2,pagesize:8
begin:8,end:16
PageKeyedDataSourceServlet doGet
page:3,pagesize:8
begin:16,end:24
OK,感觉越来越轻松了!趁热打铁,来看看最后一种使用!
4.3 继承ItemKeyedDataSource
还是老规矩,先来看对应特点!
如图所示
当前方式适用于:当目标数据的下一页需要依赖于上一页数据中最后一个对象中的某个字段作为key的情况,此类分页形式常见于评论功能的实现,例如,若上一页数据中最后一个对象的key为9001,那么在请求下一页时,需要鞋带参数since=9001&pageSize=5
,则服务器会返回key=9001
之后的5条数据
那么!
4.3.1 先来看看服务端的代码
public class ItemKeyedDataSourceServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("ItemKeyedDataSourceServlet doGet");
int since = Integer.parseInt(request.getParameter("since"));
int pagesize = Integer.parseInt(request.getParameter("pagesize"));
System.out.println("since:"+since+",pagesize:"+pagesize);
//从MOVIES集合中取出一段数据出来
List<Movie> searchList = new ArrayList<>();
//第一次请求,since等于0,重新给since赋值为第一个元素的id
if(since == 0){
Movie movie = ServerStartupListener.MOVIES.get(0);
searchList.add(movie);
since = movie.getId();
}
Gson gson = new Gson();
for (int i = 0; i < ServerStartupListener.MOVIES.size(); i++) {
try{
//通过请求参数since,找到id等于since的元素
//往后找到pagesize个元素
Movie movie = ServerStartupListener.MOVIES.get(i);
if(movie.getId() == since){
for (int j = i+1; j < i + pagesize; j++) {
searchList.add(ServerStartupListener.MOVIES.get(j));
}
}
}catch (IndexOutOfBoundsException e){
//索引越界,跳出循环
System.out.println(e.getMessage());
break;
}
}
JsonArray jsonArray = (JsonArray) gson.toJsonTree(searchList, new TypeToken<List<Movie>>(){}.getType());
response.setHeader("Content-type", "text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print(jsonArray.toString());
out.close();
}
}
通过这段代码可知,对应Json格式为:
[
{
"id":35076714,
"title":"扎克·施奈德版正义联盟",
"cover":"https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2634360594.webp",
"rate":"8.9"
}
]
因此使用Movie
实体类就可以了,就在外层套上一个list
完全满足!
那么就可以直接看对应的DataSource
4.3.2 MovieDataSource
class MovieDataSource : ItemKeyedDataSource<Int, Movie>() {
companion object {
const val PER_PAGE = 8
const val FIRST_PAGE = 1
}
override fun getKey(movie: Movie): Int {
return movie.id
}
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Movie>) {
val since = 0
RetrofitClient.getInstance()
.getApi()
.getMovies(since, PER_PAGE)
.enqueue(object : Callback<List<Movie>> {
override fun onResponse(
call: Call<List<Movie>>,
response: Response<List<Movie>>
) {
if (response.body() != null) {
//把数据传递给PagedList
callback.onResult(response.body()!!)
Log.d("hqk", "loadInitial:" + response.body())
}
}
override fun onFailure(call: Call<List<Movie>>, t: Throwable) {}
})
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Movie>) {
RetrofitClient.getInstance()
.getApi()
.getMovies(params.key, PER_PAGE)
.enqueue(object : Callback<List<Movie>> {
override fun onResponse(
call: Call<List<Movie>>,
response: Response<List<Movie>>
) {
if (response.body() != null) {
//把数据传递给PagedList
callback.onResult(response.body()!!)
Log.d("hqk", "loadInitial:" + response.body())
}
}
override fun onFailure(call: Call<List<Movie>>, t: Throwable) {}
})
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Movie>) {
}
}
代码解析:
loadInitial与loadAfter
这两个方法,到这就不用再次讲解了吧- 不过注意的是,这里额外多了一个
getKey
方法! - 返回的是就是当条对应的id
因为UI使用代码与运行效果一样,因此这里就不再次贴对应代码以及对应运行效果了!
直接看后台打印日志
ItemKeyedDataSourceServlet doGet
since:0,pagesize:8
ItemKeyedDataSourceServlet doGet
since:34960094,pagesize:8
ItemKeyedDataSourceServlet doGet
since:27662747,pagesize:8
ItemKeyedDataSourceServlet doGet
since:34962956,pagesize:8
ItemKeyedDataSourceServlet doGet
since:30257787,pagesize:8
ItemKeyedDataSourceServlet doGet
since:25862300,pagesize:8
ItemKeyedDataSourceServlet doGet
since:26996524,pagesize:8
ItemKeyedDataSourceServlet doGet
since:30403683,pagesize:8
OK,越来越简单了!到这!已经全部讲完这三种方式的使用方法以及对应的特点!
以为这就完了?那当然肯定没完囖!
到现在为止!只实现了有网络的情况。而在没有网络情况,已经加载后的数据,重进app就直接over了!
那如果想要实现无网络加载缓存该怎么做呢???
这就要请出最后一位嘉宾到场!
5、BoundaryCallback
特点上面说了!那么它是怎么实现的呢?
如图所示
- App通知
BoundaryCallback
,然后BoundaryCallback
再去请求服务器,请求下来的数据经过“中间商-数据库”过一下手最后发送给UI刷新数据! - 因为有网的时候对应数据经过数据库时,就已经有缓存了,因此在没有网络的情况下就能直接加载缓存
这里服务端的代码使用4.2格式
这里说到使用数据库,那么!
5.1 对应数据表实体类
@Entity(tableName = "movie")
class Movie {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "no", typeAffinity = ColumnInfo.INTEGER)
var NO = 0
@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
var id = 0
@ColumnInfo(name = "title", typeAffinity = ColumnInfo.TEXT)
var title: String? = null
@ColumnInfo(name = "rate", typeAffinity = ColumnInfo.TEXT)
var rate: String? = null
@ColumnInfo(name = "cover", typeAffinity = ColumnInfo.TEXT)
var cover: String? = null
}
因为用到了数据库,那么就不再只是之前普通的网络实体类了,同时还是访问数据库的实体类!
因为当前使用的是4.2格式的访问方式,因此
5.2 客服端解析json实体类
class Movies {
@SerializedName("has_more")
var hasMore = false
@SerializedName("subjects")
var movieList: List<Movie>? = null
}
对应API
interface Api {
// @GET("pds.do")
// fun getMovies(
// @Query("start") since: Int,
// @Query("count") perPage: Int
// ): Call<Movies>
// @GET("pkds.do")
// fun getMovies(
// @Query("page") page: Int,
// @Query("pagesize") pagesize: Int
// ): Call<Movies>
// @GET("ikds.do")
// fun getMovies(
// @Query("since") since: Int,
// @Query("pagesize") pagesize: Int
// ): Call<List<Movie>>
@GET("pkds.do")
fun getMovies(
@Query("page") page: Int,
@Query("pagesize") pagesize: Int
): Call<Movies>
}
既然用到了数据库!那么
5.3 创建对应数据库
@Database(entities = [Movie::class], version = 1, exportSchema = true)
abstract class MyDatabase : RoomDatabase() {
companion object {
private const val DATABASE_NAME = "my_db.db"
private var mInstance: MyDatabase? = null
@Synchronized
@JvmStatic
fun getInstance(context: Context): MyDatabase {
if (mInstance == null) {
mInstance = Room.databaseBuilder(
context.applicationContext,
MyDatabase::class.java,
DATABASE_NAME
).build()
}
return mInstance as MyDatabase
}
}
abstract fun getMovieDao(): MovieDao
}
这个在本专栏讲解过,这里不再详解了!
5.4 对应Dao
@Dao
interface MovieDao {
@Insert
fun insertMovies(movies: List<Movie>)
@Query("DELETE FROM movie")
fun clear()
@Query("SELECT * FROM movie")
fun getMovieList(): DataSource.Factory<Int, Movie>
}
注意
- 这里查询所有的时候,返回格式为
DataSource.Factory
- 因此可以得出:在Room数据库里,已经自动帮我们实现了三大核心类的
DataSource
- 所以在之前的基础上,可以删除掉已经实现后的
DataSource
现在就差最后的重点到场了
5.5 BoundaryCallback
class MovieBoundaryCallback : BoundaryCallback<Movie> {
companion object {
const val PER_PAGE = 8
var FIRST_PAGE = 1
}
private var application: Application? = null
constructor(application: Application) {
this.application = application
}
override fun onZeroItemsLoaded() {
super.onZeroItemsLoaded()
//加载第一页数据!
getTopData()
}
private fun getTopData() {
RetrofitClient.getInstance()
.getApi()
.getMovies(FIRST_PAGE, PER_PAGE)
.enqueue(object : Callback<Movies> {
override fun onResponse(call: Call<Movies>, response: Response<Movies>) {
if (response.body() != null) {
//把数据传递给PagedList
insertMovies(response.body()!!.movieList!!)
Log.d("hqk", "loadInitial:" + response.body())
}
}
override fun onFailure(call: Call<Movies>, t: Throwable) {
}
})
}
//把网络数据,保存到数据库
private fun insertMovies(movies: List<Movie>) {
thread {
MyDatabase.getInstance(application!!)
.getMovieDao()
.insertMovies(movies)
}
}
override fun onItemAtEndLoaded(movie: Movie) {
super.onItemAtEndLoaded(movie)
//加载第二页数据
getTopAfterData()
}
private fun getTopAfterData() {
FIRST_PAGE += 1
RetrofitClient.getInstance()
.getApi()
.getMovies(FIRST_PAGE, PER_PAGE)
.enqueue(object : Callback<Movies> {
override fun onResponse(call: Call<Movies>, response: Response<Movies>) {
if (response.body() != null) {
insertMovies(response.body()!!.movieList!!)
Log.d("hqk", "loadInitial:" + response.body())
}
}
override fun onFailure(call: Call<Movies>, t: Throwable) {
}
})
}
}
代码解析:
- 这里我们看到:继承了
BoundaryCallback
; - 重写了
onZeroItemsLoaded与onItemAtEndLoaded
方法; - 这两个方法分别表示加载第一页以及加载次页调用的方法
- 每次加载完成时,两个方法都会调用
insertMovies
方法,将其数据添加至数据库里
这里看到,所有的网络数据全进入了数据库,既然有进,那就肯定有出!
因此,来看看
5.6 对应ViewModel
class MovieViewModel : AndroidViewModel {
companion object {
const val PER_PAGE = 8
}
var moviePagedList: LiveData<PagedList<Movie>>? = null
constructor(application: Application) : super(application) {
val movieDao: MovieDao = MyDatabase.getInstance(application).getMovieDao()
moviePagedList =
LivePagedListBuilder(movieDao.getMovieList(), PER_PAGE).setBoundaryCallback(
MovieBoundaryCallback(application)
).build()
}
/**
* 刷新
*/
fun refresh() {
viewModelScope.launch(Dispatchers.IO) {
MyDatabase.getInstance(getApplication())
.getMovieDao()
.clear()
}
}
}
代码解析
这里我们可以看到:
- 因为这里用到上下文,因此这里使用的是
AndroidViewModel
- 多了个
refresh
方法,用来清空数据库,达到刷新数据的作用 - 而
moviePagedList
获取方式,依旧使用的是LivePagedListBuilder
; - 不同的是,构造参数不再是自己实现的
DataSource.Factory
,而是使用Room数据库里的DataSource.Factory
- 并且还额外设置了
setBoundaryCallback
对象
最后!
5.7 来看看对应UI使用
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
val adapter = MoviePagedListAdapter(this)
recyclerView.adapter = adapter
val movieViewModel = MovieViewModel(application)
movieViewModel.moviePagedList!!.observe(this, Observer<PagedList<Movie>> {
adapter.submitList(it)
})
findViewById<SwipeRefreshLayout>(R.id.swipeRefresh).also {
it.setOnRefreshListener {
movieViewModel.refresh()
it.isRefreshing = false
}
}
}
}
这里没啥可说的,
5.8 来看看使用效果
5.8.1 有网情况
刷新没问题!
5.8.2 无网缓存情况
这里看到在没有网络的情况,重进App依然能够加载已经加载过的缓存数据!
如图所示
在对应的数据库文件里,也能看到对应的数据大小在变动!
结束语
好了,到这里就真的结束了,相信看到这里的小伙伴对Jetpack—Paging有了深刻的理解!在下一篇中,将会结合Flow+Paging3进行讲解!