paging组件的用法和意义
阅读原文时间:2023年08月29日阅读:1

一.Paging组件的意义

  分页加载是在应用程序开发过程中十分常见的需求,我们经常需要以列表的方式加载大量的数据,这些数据通常来自网络或本地数据库。然而,如果一次性将所有数据全部加载出来,必然会消耗大量的时间和数据流量,而且用户可能只是需要一部分数据就行。因此,Google便推出了paging组件,来实现分页加载;分页加载就是对数据进行按需加载,在不影响用户体验的同时,还能节省数据流量,提升应用的性能。

二.Paging支持的架构类型

  Paging支持3种架构类型,分别是:网络,数据库,网络+数据库

  网络:也就是通过网络请求的方式去获得服务器返回的数据,然后分页加载出来

  数据库:掌握了从网络上获取数据并加载出来,从数据库加载就变得很简单,只需替换数据源即可

  网络+数据库:出于用户的体验,通常我们会对网络数据进行缓存,以便用户下次打开应用程序时,应用程序可以先展示缓存数据,我们通常会利用数据库对网络数据进行缓存,但这也意味着我们需要同时处理好网络和数据库这两个数据源。但是,多个数据源会让业务逻辑变得更为复杂,所以我们通常采用单一数据源作为解决方案,即从网络获取的数据,直接缓存进数据库,列表仅从数据库这个唯一的数据源获取数据。

三.三种分页机制的适用场景

  PositionalDataSource:

  适用于从任意位置加载任意数量的数据,且目标数据源中数据固定的情况。

  PageKeyedDataSource:

  适用于数据源以页的方式进行请求的情况,比如请求第二页的5条数据。

  ItemKeyedDataSource:

  适用于当目标数据的下一页需要依赖于上一页数据中最后一个对象中的某个字段作为key的情况,例如我请求key=9001后的5条数据作为下一页的数据。

  下面会以PositionalDataSource为例进行讲解,其他方式极其相似。

四.分页机制的实现

  我们以从豆瓣网上获取热度最高的250部电影为例进行讲解,这里我们使用Retrofit+OkHttp进行网络数据的获取,如果对这两个网络请求工具不熟悉的话,可以看这篇博客:https://www.cnblogs.com/luqman/p/okhttp_retrofit.html

  这里我们请求的api接口是https://movie.douban.com/j/chart/top_list?type=11&interval_id=100%3A90&action=&start=0&limit=250,接口返回的前五条数据如下所示:

[{"rating":["9.7","50"],"rank":1,"cover_url":"https://img2.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p480747492.webp","is_playable":true,"id":"1292052","types":["犯罪","剧情"],"regions":["美国"],"title":"肖申克的救赎","url":"https:\/\/movie.douban.com\/subject\/1292052\/","release_date":"1994-09-10","actor_count":25,"vote_count":2912430,"score":"9.7","actors":["蒂姆·罗宾斯","摩根·弗里曼","鲍勃·冈顿","威廉姆·赛德勒","克兰西·布朗","吉尔·贝罗斯","马克·罗斯顿","詹姆斯·惠特摩","杰弗里·德曼","拉里·布兰登伯格","尼尔·吉恩托利","布赖恩·利比","大卫·普罗瓦尔","约瑟夫·劳格诺","祖德·塞克利拉","保罗·麦克兰尼","芮妮·布莱恩","阿方索·弗里曼","V·J·福斯特","弗兰克·梅德拉诺","马克·迈尔斯","尼尔·萨默斯","耐德·巴拉米","布赖恩·戴拉特","唐·麦克马纳斯"],"is_watched":false},{"rating":["9.6","50"],"rank":2,"cover_url":"https://img1.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2561716440.webp","is_playable":true,"id":"1291546","types":["剧情","爱情","同性"],"regions":["中国大陆","中国香港"],"title":"霸王别姬","url":"https:\/\/movie.douban.com\/subject\/1291546\/","release_date":"1993-07-26","actor_count":28,"vote_count":2150114,"score":"9.6","actors":["张国荣","张丰毅","巩俐","葛优","英达","蒋雯丽","吴大维","吕齐","雷汉","尹治","马明威","费振翔","智一桐","李春","赵海龙","李丹","童弟","沈慧芬","黄斐","徐杰","黄磊","冯远征","杨立新","方征","周璞","隋永清","宋小川","杜广沛"],"is_watched":false},{"rating":["9.6","50"],"rank":3,"cover_url":"https://img2.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2578474613.webp","is_playable":true,"id":"1292063","types":["剧情","喜剧","爱情","战争"],"regions":["意大利"],"title":"美丽人生","url":"https:\/\/movie.douban.com\/subject\/1292063\/","release_date":"2020-01-03","actor_count":29,"vote_count":1332240,"score":"9.6","actors":["罗伯托·贝尼尼","尼可莱塔·布拉斯基","乔治·坎塔里尼","朱斯蒂诺·杜拉诺","赛尔乔·比尼·布斯特里克","玛丽萨·帕雷德斯","霍斯特·布赫霍尔茨","利迪娅·阿方西","朱利亚娜·洛约迪切","亚美利哥·丰塔尼","彼得·德·席尔瓦","弗朗西斯·古佐","拉法埃拉·莱博罗尼","克劳迪奥·阿方西","吉尔·巴罗尼","马西莫·比安奇","恩尼奥·孔萨尔维","吉安卡尔洛·科森蒂诺","阿伦·克雷格","汉尼斯·赫尔曼","弗兰科·梅斯科利尼","安东尼奥·普雷斯特","吉娜·诺维勒","理查德·塞梅尔","安德烈提多娜","迪尔克·范登贝格","奥梅罗·安东努蒂","沈晓谦","张欣"],"is_watched":false},{"rating":["9.6","50"],"rank":4,"cover_url":"https://img2.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p492406163.webp","is_playable":true,"id":"1295124","types":["剧情","历史","战争"],"regions":["美国"],"title":"辛德勒的名单","url":"https:\/\/movie.douban.com\/subject\/1295124\/","release_date":"1993-11-30","actor_count":49,"vote_count":1110744,"score":"9.6","actors":["连姆·尼森","本·金斯利","拉尔夫·费因斯","卡罗琳·古多尔","乔纳森·萨加尔","艾伯丝·戴维兹","马尔戈萨·格贝尔","马克·伊瓦涅","碧翠斯·马科拉","安德烈·瑟韦林","弗里德里希·冯·图恩","克齐斯茨托夫·拉夫特","诺伯特·魏塞尔","维斯瓦夫·科马萨","布拉德·雅各布维茨","Maciej Orlos","皮奥特·赛尔沃斯","Tadeusz Huk","马丁·塞梅洛格","托马斯·德德克","奥拉夫·卢巴申科","马瑞安·格林卡","约亨·尼克尔","阿格涅兹卡·克鲁科沃娜","阿格尼兹卡·旺格","托马斯·莫里斯","佐久间玲","吴俊全","约阿希姆·保罗·阿斯波克","彭河","戈兹·奥托","玛雅·奥丝塔泽斯卡","Maciej Kozlowski","艾尔文·莱德","Eugeniusz Priwieziencew","Marta Bizon","埃兹拉·达甘","吉恩·莱赫纳","Razia Israeli","拉米·希尔伯格","布兰科·拉斯蒂格","路德格·皮斯特","埃琳娜·勒文松","胡契克·卡勒塔","塔德乌什·布拉德茨基","亨里克·比斯塔","帕维·德朗柯","耶日·诺瓦克","安娜·穆查"],"is_watched":false},{"rating":["9.6","50"],"rank":5,"cover_url":"https://img1.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p1505392928.webp","is_playable":true,"id":"1296141","types":["剧情","犯罪","悬疑"],"regions":["美国"],"title":"控方证人","url":"https:\/\/movie.douban.com\/subject\/1296141\/","release_date":"1957-12-17","actor_count":51,"vote_count":564999,"score":"9.6","actors":["泰隆·鲍华","玛琳·黛德丽","查尔斯·劳顿","埃尔莎·兰彻斯特","约翰·威廉姆斯","亨利·丹尼尔","伊安·沃尔夫","托林·撒切尔","诺玛·威登","尤娜·奥康纳","茹塔·李","贝丝·弗劳尔斯","比尔·厄尔文","J·帕特·奥马利","本·怀特","Paul Kruger","Jack Raine","Paul Power","乔治·佩林","威廉·H·奥布莱恩","奥托拉内史密斯","Frank McClure","Colin Kenny","Jeanne Lafayette","Wilbur Mack","Fred Rapport","利奥达·理查德斯","Glen Walters","Arthur Tovey","伯特史蒂文斯","Cap Somers","Lucille Sewall","斯考特西顿","Norbert Schiller","杰弗里·塞尔","John Roy","Al Roberts","Art Howard","Stuart Hall","Francis Compton","Philip Tonge","帕特里克·艾亨","富兰克林·法纳姆","玛乔丽·伊顿","史蒂夫·卡鲁瑟斯","George Calliga","乔治布鲁格曼","丹尼·鲍沙其","Brandon Beach","埃迪·贝克","沃尔特·培根"],"is_watched":false}]

  有了这些json数据后,我们就可以编写对应的javaBean了,下面我们开始编写代码。

  a.添加相关依赖:

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'androidx.paging:paging-runtime:2.1.2'

  b.添加网络权限

  c.构建网络请求框架:

public interface Api {  
/\*\*  
 \* 获取电影院当前上映的电影  
 \* https://movie.douban.com/j/chart/top\_list?type=11&interval\_id=100%3A90&action=&start=40&limit=20  
 \*/  
@GET("j/chart/top\_list")  
Call<List<Movie>> getMovies(@Query("start") int start, @Query("limit") int limit);  

}

public class RetrofitClient {
private static final String BASE_URL="https://movie.douban.com";
private static RetrofitClient retrofitClient;
private final Retrofit retrofit;

private RetrofitClient(){  
    retrofit=new Retrofit.Builder()  
            .baseUrl(BASE\_URL)  
            .addConverterFactory(GsonConverterFactory.create())  //添加转换器  
            .client(getClient())  //添加OkHttp的拦截器  
            .build();  
}  
public static synchronized RetrofitClient getInstance(){  
    if(retrofitClient==null){  
        retrofitClient=new RetrofitClient();  
    }  
    return retrofitClient;  
}  
public Api getApi(){  
    return retrofit.create(Api.class);  
}  
private OkHttpClient getClient(){  
    OkHttpClient client=new OkHttpClient.Builder()  
            .addInterceptor(new Interceptor() {  
                @Override  
                public Response intercept(Chain chain) throws IOException { //拦截器用于添加请求参数,会在源url上进行修改  
                    Request original = chain.request();  
                    HttpUrl url = original.url();  
                    HttpUrl currentUrl = url.newBuilder()  
                            .addQueryParameter("interval\_id", "100:90")  
                            .addQueryParameter("type", "11")  
                            .addQueryParameter("action", "")  
                            .build();  
                    Request newRequest = original.newBuilder()  
                            .url(currentUrl)  
                            .build();  
                    return chain.proceed(newRequest);  
                }  
            })  
            .build();  
    return client;  
}  

}

  d.创建Model类

public class Movie{

private List<String> rating;  
private Integer    rank;  
private String cover\_url;  
private String    is\_playable;  
private String    id;  
private List<String> types;  
private List<String> regions;  
private String    title;  
private String    url;  
private String    release\_date;  
private Integer    actor\_count;  
private Integer    vote\_count;  
private String    score;  
private List<String> actors;  
private String    is\_watched;

public List<String> getRating() {  
    return this.rating;  
}

public void setRating(List<String> rating) {  
    this.rating = rating;  
}

public Integer getRank() {  
    return this.rank;  
}

public void setRank(Integer rank) {  
    this.rank = rank;  
}

public String getCover\_url() {  
    return this.cover\_url;  
}

public void setCover\_url(String cover\_url) {  
    this.cover\_url = cover\_url;  
}

public String getIs\_playable() {  
    return this.is\_playable;  
}

public void setIs\_playable(String is\_playable) {  
    this.is\_playable = is\_playable;  
}

public String getId() {  
    return this.id;  
}

public void setId(String id) {  
    this.id = id;  
}

public List<String> getTypes() {  
    return this.types;  
}

public void setTypes(List<String> types) {  
    this.types = types;  
}

public List<String> getRegions() {  
    return this.regions;  
}

public void setRegions(List<String> regions) {  
    this.regions = regions;  
}

public String getTitle() {  
    return this.title;  
}

public void setTitle(String title) {  
    this.title = title;  
}

public String getUrl() {  
    return this.url;  
}

public void setUrl(String url) {  
    this.url = url;  
}

public String getRelease\_date() {  
    return this.release\_date;  
}

public void setRelease\_date(String release\_date) {  
    this.release\_date = release\_date;  
}

public Integer getActor\_count() {  
    return this.actor\_count;  
}

public void setActor\_count(Integer actor\_count) {  
    this.actor\_count = actor\_count;  
}

public Integer getVote\_count() {  
    return this.vote\_count;  
}

public void setVote\_count(Integer vote\_count) {  
    this.vote\_count = vote\_count;  
}

public String getScore() {  
    return this.score;  
}

public void setScore(String score) {  
    this.score = score;  
}

public List<String> getActors() {  
    return this.actors;  
}

public void setActors(List<String> actors) {  
    this.actors = actors;  
}

public String getIs\_watched() {  
    return this.is\_watched;  
}

public void setIs\_watched(String is\_watched) {  
    this.is\_watched = is\_watched;  
}

@Override  
public String toString() {  
    return "Movie{" +  
            "rating=" + rating +  
            ", rank=" + rank +  
            ", cover\_url='" + cover\_url + '\\'' +  
            ", is\_playable='" + is\_playable + '\\'' +  
            ", id='" + id + '\\'' +  
            ", types=" + types +  
            ", regions=" + regions +  
            ", title='" + title + '\\'' +  
            ", url='" + url + '\\'' +  
            ", release\_date='" + release\_date + '\\'' +  
            ", actor\_count=" + actor\_count +  
            ", vote\_count=" + vote\_count +  
            ", score='" + score + '\\'' +  
            ", actors=" + actors +  
            ", is\_watched='" + is\_watched + '\\'' +  
            '}';  
}  

}

  e.编写一个类继承PositionalDataSource,并在此类中进行网络请求,获取服务器返回的数据。

public class MovieDataSource extends PositionalDataSource {
public static final int pageSize=5;
@Override
public void loadInitial(@NonNull LoadInitialParams loadInitialParams, @NonNull LoadInitialCallback loadInitialCallback) {//负责第一页数据的加载
int startPosition=0;
RetrofitClient.getInstance()
.getApi()
.getMovies(startPosition,pageSize)
.enqueue(new Callback>() {
@Override
public void onResponse(Call> call, Response> response) {
if(response.body()!=null){
loadInitialCallback.onResult(response.body(),0,250);//将数据返回给PagedList,250是指数据源中的数据总数,当调用了setEnablePlaceHolder(true)方法时,必须传入此参数,以便预留位置
}
}

                @Override  
                public void onFailure(Call<List<Movie>> call, Throwable t) {

                }  
            });  
}

@Override  
public void loadRange(@NonNull LoadRangeParams loadRangeParams, @NonNull LoadRangeCallback<Movie> loadRangeCallback) {//负责第一页之后数据的加载  
    RetrofitClient.getInstance()  
            .getApi()  
            .getMovies(loadRangeParams.startPosition,pageSize)  //在你滑动手机屏幕到底部请求下一页的数据时,loadRangeParams.startPosition会自动维护,不需要你手动修改  
            .enqueue(new Callback<List<Movie>>() {  
                @Override  
                public void onResponse(Call<List<Movie>> call, Response<List<Movie>> response) {  
                    if(response.body()!=null){  
                        loadRangeCallback.onResult(response.body());  
                    }  
                }

                @Override  
                public void onFailure(Call<List<Movie>> call, Throwable t) {

                }  
            });  
}  

}

  f.创建MovieDataSourceFactory类,负责MovieDataSource的创建。

public class MovieDataSourceFactory extends DataSource.Factory{
@NonNull
@Override
public DataSource create() {
return new MovieDataSource();
}
}

  g.有了Factory类之后,接下来需要创建ViewModel类,在这个类中通过LivePagedListBuilder类创建和配置PagedList,并使用LiveData包装PagedList,然后在MainActivity中监测PagedList中数据的变化,并更新页面。

public class MovieViewModel extends ViewModel {
public LiveData> moviePagedList;
public MovieViewModel(){
PagedList.Config config=new PagedList.Config.Builder()
.setEnablePlaceholders(true) //设置是否为那些数量已知,但尚未加载出来的数据预留位置
.setPageSize(5)
.setPrefetchDistance(3) //设置当距离底部还有多少数据时加载下一页数据
.setInitialLoadSizeHint(20) //设置首次加载数据的数量
.setMaxSize(65536*5) 
.build();
moviePagedList=new LivePagedListBuilder<>(new MovieDataSourceFactory(),config).build(); //MovieDataSource中的onResult方法会把服务器返回的数据传递到PagedList当中
}
}

  h.编写RecyclerView的适配器类,此类需要继承自PagedListAdapter。

public class MoviePagedListAdapter extends PagedListAdapter {
private final Context context;
public MoviePagedListAdapter(Context context){
super(diffCallback);
this.context=context;
}
private static final DiffUtil.ItemCallback diffCallback=new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull Movie oldItem, @NonNull Movie newItem) {//根据id来判断两条数据是否是同一条数据
return oldItem.getId().equals(newItem.getId());
}

    @SuppressLint("DiffUtilEquals")  
    @Override  
    public boolean areContentsTheSame(@NonNull Movie oldItem, @NonNull Movie newItem) {//比较两条数据的内容是否一样  
        return oldItem.equals(newItem);  
    }  
};  
@NonNull  
@Override  
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {  
    LayoutInflater inflater=LayoutInflater.from(context);  
    MovieItemBinding movieItemBinding= DataBindingUtil.inflate(inflater,R.layout.movie\_item,parent,false);  
    return new ViewHolder(movieItemBinding);  
}

@Override  
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {  
    Movie movie = getItem(position);//从PagedList中获取数据,如果没有的话,PagedList会通知DataSource获取下一页的数据  
    if(movie!=null){  
        holder.movieItemBinding.setMovie(movie);  
    }  
}

public static class ViewHolder extends RecyclerView.ViewHolder{  
    public MovieItemBinding movieItemBinding;  
    public ViewHolder(MovieItemBinding movieItemBinding) {  
        super(movieItemBinding.getRoot());  
        this.movieItemBinding=movieItemBinding;  
    }  
}  

}

  i.MainActivity实现:

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding activityMainBinding= DataBindingUtil.setContentView(this,R.layout.activity_main);
activityMainBinding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
MoviePagedListAdapter moviePagedListAdapter=new MoviePagedListAdapter(this);
activityMainBinding.recyclerView.setAdapter(moviePagedListAdapter);
MovieViewModel movieViewModel = new ViewModelProvider(this).get(MovieViewModel.class);
movieViewModel.moviePagedList.observe(this, new Observer>() {
@Override
public void onChanged(PagedList movies) {
moviePagedListAdapter.submitList(movies);//当PagedList数据发生变化时,通知适配器更新数据,然后用getItem()方法获取数据
}
});
}
}

  j.布局文件:

  activity_main.xml:


  movie_item.xml:

  到这里,就可以运行程序,查看效果了!

  不过,我在写代码的时候,遇到了一个bug调了很久,就是当我在MainActivity中将activityMainBinding.recyclerView.setHasFixedSize(true);这句代码加上时,加载不出来任何的数据;但是如果我将RecyclerView组件的布局高度改成match_parent后,即使设置了setHasFixedSize(true)也能加载出来数据,也不知道咋回事。