一.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:90&action=&start=0&limit=250,接口返回的前五条数据如下所示:
代码语言:javascript复制[{"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.添加相关依赖:
代码语言:javascript复制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.构建网络请求框架:
代码语言:javascript复制 public interface Api {
/**
* 获取电影院当前上映的电影
* https://movie.douban.com/j/chart/top_list?type=11&interval_id=100:90&action=&start=40&limit=20
*/
@GET("j/chart/top_list")
Call<List<Movie>> getMovies(@Query("start") int start, @Query("limit") int limit);
}
代码语言:javascript复制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类
代码语言:javascript复制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,并在此类中进行网络请求,获取服务器返回的数据。
代码语言:javascript复制public class MovieDataSource extends PositionalDataSource<Movie> {
public static final int pageSize=5;
@Override
public void loadInitial(@NonNull LoadInitialParams loadInitialParams, @NonNull LoadInitialCallback<Movie> loadInitialCallback) {//负责第一页数据的加载
int startPosition=0;
RetrofitClient.getInstance()
.getApi()
.getMovies(startPosition,pageSize)
.enqueue(new Callback<List<Movie>>() {
@Override
public void onResponse(Call<List<Movie>> call, Response<List<Movie>> 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的创建。
代码语言:javascript复制public class MovieDataSourceFactory extends DataSource.Factory<Integer, Movie>{
@NonNull
@Override
public DataSource<Integer, Movie> create() {
return new MovieDataSource();
}
}
g.有了Factory类之后,接下来需要创建ViewModel类,在这个类中通过LivePagedListBuilder类创建和配置PagedList,并使用LiveData包装PagedList,然后在MainActivity中监测PagedList中数据的变化,并更新页面。
代码语言:javascript复制public class MovieViewModel extends ViewModel {
public LiveData<PagedList<Movie>> 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。
代码语言:javascript复制public class MoviePagedListAdapter extends PagedListAdapter<Movie, MoviePagedListAdapter.ViewHolder> {
private final Context context;
public MoviePagedListAdapter(Context context){
super(diffCallback);
this.context=context;
}
private static final DiffUtil.ItemCallback<Movie> diffCallback=new DiffUtil.ItemCallback<Movie>() {
@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实现:
代码语言:javascript复制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<PagedList<Movie>>() {
@Override
public void onChanged(PagedList<Movie> movies) {
moviePagedListAdapter.submitList(movies);//当PagedList数据发生变化时,通知适配器更新数据,然后用getItem()方法获取数据
}
});
}
}
j.布局文件:
activity_main.xml:
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@ id/recyclerView"/>
</LinearLayout>
</layout>
movie_item.xml:
代码语言:javascript复制<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="movie"
type="com.example.paging.model.Movie" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@ id/movieName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{movie.title}"/>
</LinearLayout>
</layout>
到这里,就可以运行程序,查看效果了!
不过,我在写代码的时候,遇到了一个bug调了很久,就是当我在MainActivity中将activityMainBinding.recyclerView.setHasFixedSize(true);这句代码加上时,加载不出来任何的数据;但是如果我将RecyclerView组件的布局高度改成match_parent后,即使设置了setHasFixedSize(true)也能加载出来数据,也不知道咋回事。
五.BoundaryCallback的使用方法
在实际的开发过程中,为了更好的用户体验,通常还需要对数据进行缓存。加入了缓存后,数据的来源从原来的网络数据源变成了网络数据和本地数据组成的双数据源。我们知道,多数据源会增加应用程序的复杂度,需要处理好数据的时效性及新旧数据的切换更新等问题。为此,Google在Paging中加入了BoundaryCallback来实现数据的单一架构。
BoundaryCallback的使用流程如下图所示:
下面对流程图的每一步进行分析:
1.首先需要注意的是数据库是页面的唯一数据来源,页面订阅了数据库的变化,当数据库中的数据发生变化时,会直接反应在页面上。
2.当数据库中没有数据的时候,会通知BoundaryCallback中的onZeroItemsLoaded()方法;若数据库中有数据,则当用户滑到RecyclerView的底部时,且数据库中的数据已经加载完毕了,会通知BoundaryCallback中的onItemAtEndLoad()方法。
3.当BoundaryCallback中的回调方法被调用的时候,需要在方法内部请求网络数据,然后写入数据库,而不是直接展示网络数据。
4.当用户在下拉刷新的时候,清空本地数据库,进而再次触发BoundaryCallback去请求第一页数据和后续数据。
项目展示:
1.项目架构图
2.代码分析
a.在build.gradle文件中加入Room的依赖:
代码语言:javascript复制 implementation 'androidx.room:room-runtime:2.4.2'
annotationProcessor 'androidx.room:room-compiler:2.4.2'
b.创建Room数据库
代码语言:javascript复制@Database(entities = {Video.class},version = 1)
public abstract class VideoDatabase extends RoomDatabase {
private static final String DATABASE_NAME="video.db";
private static VideoDatabase videoDatabase;
@NonNull
@Override
protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config) {
return null;
}
@NonNull
@Override
protected InvalidationTracker createInvalidationTracker() {
return null;
}
@Override
public void clearAllTables() {
}
public static VideoDatabase getInstance(Context context){
if(videoDatabase==null){
videoDatabase= Room.databaseBuilder(context,VideoDatabase.class,DATABASE_NAME).build();
}
return videoDatabase;
}
public abstract VideoDao videoDao();
}
c.数据库model类:
代码语言:javascript复制@Entity(tableName = "video")
public class Video {
@PrimaryKey
@NonNull
private String id;
@Ignore
private List<String> rating;
private Integer rank;
private String cover_url;
private String is_playable;
@Ignore
private List<String> types;
@Ignore
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;
@Ignore
private List<String> actors;
private String is_watched;
public Video() {
}
public Video(String id, Integer rank, String cover_url, String is_playable, List<String> types, String title, String url, String release_date, Integer actor_count, Integer vote_count, String score, String is_watched) {
this.id = id;
this.rank = rank;
this.cover_url = cover_url;
this.is_playable = is_playable;
this.types = types;
this.title = title;
this.url = url;
this.release_date = release_date;
this.actor_count = actor_count;
this.vote_count = vote_count;
this.score = score;
this.is_watched = 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 '''
'}';
}
}
d.针对model类编写对应的Dao文件:
代码语言:javascript复制@Dao
public interface VideoDao {
@Query("delete from video")
void clear();
@Query("select * from video")
DataSource.Factory<Integer,Video> getAllVideos(); //这里的返回值要修改成DataSource.Factory类型
@Query("select * from video")
List<Video> getAllVideo2();
@Insert
void insert(Video video);
}
e.实现BoundaryCallback:
代码语言:javascript复制public class VideoBoundaryCallback extends PagedList.BoundaryCallback<Video>{
private String TAG=getClass().getName();
private Application application;
private int pageNum;
private int pageSize=8;
public VideoBoundaryCallback(Application application){
this.application=application;
getAllVideoIds(new VideoIdsCallback() {
@Override
public void videoIdCallback(List<String> ids) {
pageNum=ids.size()/8;//根据数据库中的记录数量计算当前加载到几页数据,下次重新进入程序的时候这些数据就不用重新加载了
}
});
}
@Override
public void onZeroItemsLoaded() {//数据库为空时调用该方法,在该方法中请求第一页的数据
super.onZeroItemsLoaded();
getTopData();
}
@Override
public void onItemAtFrontLoaded(@NonNull Video itemAtFront) {
super.onItemAtFrontLoaded(itemAtFront);
}
@Override
public void onItemAtEndLoaded(@NonNull Video itemAtEnd) {//请求下一页数据
/**
* itemAtEnd 返回的是数据库中最后一条数据
*/
super.onItemAtEndLoaded(itemAtEnd);
getTopAfterData(pageNum);
pageNum ;
}
/**
*加载第一页数据
*/
private void getTopData(){
final int startPosition=0;
RetrofitClient.getInstance()
.getApi()
.getVideos(startPosition,pageSize)
.enqueue(new Callback<List<Video>>() {
@Override
public void onResponse(Call<List<Video>> call, Response<List<Video>> response) {
if(response.body()!=null){
getAllVideoIds(new VideoIdsCallback() {
@Override
public void videoIdCallback(List<String> ids) {
for(Video video:response.body()){
if(!ids.contains(video.getId())){
insert(video);
}
}
}
});
}
}
@Override
public void onFailure(Call<List<Video>> call, Throwable t) {
}
});
}
/**
* 加载下一页数据
*/
private void getTopAfterData(int pageNum){
RetrofitClient.getInstance()
.getApi()
.getVideos(pageSize*pageNum,pageSize)
.enqueue(new Callback<List<Video>>() {
@Override
public void onResponse(Call<List<Video>> call, Response<List<Video>> response) {
if(response.body()!=null){
getAllVideoIds(new VideoIdsCallback() {
@Override
public void videoIdCallback(List<String> ids) {
for(Video video:response.body()){
if(!ids.contains(video.getId())){
insert(video);
}
}
}
});
}
}
@Override
public void onFailure(Call<List<Video>> call, Throwable t) {
}
});
}
/**
* 插入数据到数据库
*/
private void insert(Video video){
new Thread(new Runnable() {
@Override
public void run() {
VideoDatabase.getInstance(application)
.videoDao()
.insert(video);
}
}).start();
}
public void clear(){
new Thread(new Runnable() {
@Override
public void run() {
VideoDatabase.getInstance(application)
.videoDao()
.clear();
}
}).start();
pageNum=1;
}
/**
* 获取数据库中所有视频的id
*/
private void getAllVideoIds(VideoIdsCallback videoIdsCallback){
new Thread(new Runnable() {
@Override
public void run() {
List<String> video_ids=new ArrayList<>();
List<Video> videos = VideoDatabase.getInstance(application)
.videoDao()
.getAllVideo2();
for(Video video:videos){
video_ids.add(video.getId());
}
videoIdsCallback.videoIdCallback(video_ids);
}
}).start();
}
public interface VideoIdsCallback{
void videoIdCallback(List<String> ids);
}
}
f.编写ViewModel类,将数据存入PagedList中:
代码语言:javascript复制public class VideoViewModel extends AndroidViewModel {
private LiveData<PagedList<Video>> videoPagedList;
private static final int pageSize=8;
private VideoBoundaryCallback videoBoundaryCallback;
public VideoViewModel(Application application){
super(application);
VideoDatabase videoDatabase=VideoDatabase.getInstance(application);
videoBoundaryCallback=new VideoBoundaryCallback(application);
videoPagedList=new LivePagedListBuilder<>(videoDatabase.videoDao().getAllVideos(),pageSize)
.setBoundaryCallback(videoBoundaryCallback)
.build();
}
public LiveData<PagedList<Video>> getVideoPagedList(){
return videoPagedList;
}
public void refresh(){
videoBoundaryCallback.clear();
}//下拉刷新
}
g.在布局文件中添加下拉刷新组件:implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
代码语言:javascript复制<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@ id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@ id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
</layout>
h.MainActivity实现:
代码语言:javascript复制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));
activityMainBinding.recyclerView.setHasFixedSize(true);
VideoAdapter videoAdapter = new VideoAdapter(this);
activityMainBinding.recyclerView.setAdapter(videoAdapter);
VideoViewModel videoViewModel=new ViewModelProvider(this,new MyViewModelFactory(getApplication())).get(VideoViewModel.class);
videoViewModel.getVideoPagedList().observe(this, new Observer<PagedList<Video>>() {
@Override
public void onChanged(PagedList<Video> videos) {
videoAdapter.submitList(videos);
}
});
activityMainBinding.swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
videoViewModel.refresh();
activityMainBinding.swipeRefresh.setRefreshing(false);
}
});
}
}
代码语言:javascript复制public class MyViewModelFactory implements ViewModelProvider.Factory {
private Application application;
public MyViewModelFactory(Application application){
this.application=application;
}
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return (T)new VideoViewModel(application);
}
}
适配器和api接口和之前的一样,只是将model类换了个名字,将movie换成了video,其实这两个类一模一样。