赞
踩
Jetpack组件系列文章
Android架构之LifeCycle组件
Android架构之Navigation组件(一)
Android架构之Navigation组件(二)
Android架构之Navigation组件(三)
Android架构之Navigation组件(四)
Android架构之ViewModel组件
Android架构之LiveData组件
Android架构之Room组件(一)
Android架构之Room组件(二)
Android架构之WorkManager组件
Android架构之DataBinding(一)
Android架构之DataBinding(二)
Android架构之Paging组件(一)
Android架构之Paging组件(二)
Jetpack与MVVM架构
分页加载是在应用程序开发过程中十分常见的需求。我们经常需要以列表的形式加载大量的数据,一次性加载所有的数据,必然会消耗大量的时间和数据流量。然而用户只需要部分数据。这时候就有了分页加载。分页加载是对数据进行按需加载。 分页加载有两种模式:
Paging组件就是基于无限滚动模式而设计的。
比如说:京东的App就是就是基于无限滚动模式进行分页加载的。当我们滑动到一定量的数据的时候,会自动请求加载下一页的数据。这让用户会感觉到,所有的数据都是一次性而加载成的。
对网络数据进行分页加载,是最常见的一种分页需求。不同的公司针对分页机制所设计的API接口通常也不太一样,但总体而言可以归纳为3种。为此,Paging组件提供了3种不同的方法,以应对不同的分页机制。
数据库与网络的分页加载大同小异,无非就是数据源的替换。
出于用户体验的考虑,通常我们会对网络数据进行缓存,以便下次用户打开应用程序,应用程序可以先展示缓存数据。我们通常会利用数据库对网络数据进行缓存。所以,我们采用单一数据源作为解决方法。即从网络获取的数据,直接缓存进数据库,列表只从数据库这个唯一的数据源获取数据。
1.在RecyclerView的滑动过程中,会触发PagedListAdapter类中的onBindViewHolder()方法。数据与RecycleView中Item布局的UI控件正是在该方法中进行绑定的。
2.当RecyclerView滑动到底部时,在onBindViewHolder()方法中所调用的getItem()方法会通知PagedList,当前需要载入更多数据。
3.接着,PagedList会根据PageList.Config中的配置通知DataSource执行具体的数据获取工作。
4.DataSource从网络/本地数据库取得数据后,交给PagedList,PagedList将持有这些数据。
5.PagedList将数据交给PagedListAdapter中的DiffUtil进行比对和处理。
6.数据在经过处理后,交由RecyclerView进行展示.
1.PagedListAdapter:
2.PageList:
3.DataSource
1.PositionalDataSource
2.PageKeyedDataSource
3.ItemKeyedDataSource
比如说豆瓣的Api接口
api.douban.com/v2/movie/in_theaters?apikey=XXXXX&start=0&count=8
参数start表示可以从任意位置开始获取数据;参数count表示从start位置往后的count条数据。 但是由于豆瓣Api接口不可以使用,你们测试的时候,只需要找到类似于这种的接口,修改下参数和接口返回数据对应的Model类即可使用。这里以豆瓣返回的数据演示(上网查找的).
在接口返回的字段中,去掉了我们不需要的字段
类似于如下架构, 这里等下我们所讲PageKeyedDataSource的用法类和方法。
这里我们使用Retrofit作为网络请求库,Glide作为图片加载库。项目中还用到了LiveData和ViewModel,因此还需要添加LifeCycle的依赖,还需要Paging(分页加载数据)和RecyclerView(显示数据)的依赖
//paging的依赖
def paging_version = "2.1.2"
implementation "androidx.paging:paging-runtime:$paging_version"
testImplementation "androidx.paging:paging-common:$paging_version"
//recyclerView的依赖
implementation 'androidx.recyclerview:recyclerview:1.1.0'
//viewModel的依赖
implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"
//retrofit的依赖
implementation "com.squareup.retrofit2:retrofit:2.6.2"
implementation "com.squareup.retrofit2:converter-gson:2.6.2"
//glide的依赖
implementation 'com.github.bumptech.glide:glide:4.9.0'
<uses-permission android:name="android.permission.INTERNET"/>
就是将我们retrofit给进行了一层封装,也就是我们api文件里面的类
这里没学过retrofit的朋友,可以去自行了解以下(这里就不多说了)。
public interface Api {
@GET("movie/in_theaters")
Call<Movies> getMovies(
@Query("start") int since,
@Query("count") int perPage,
);
}
public class RetrofitClient { private static final String BASE_URL = "https://api.douban.com/v2/"; private static RetrofitClient retrofitClient; private Retrofit retrofit; public RetrofitClient() { retrofit = new Retrofit.Builder().baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build(); } public static synchronized RetrofitClient getInstance(){ if(retrofitClient == null){ retrofitClient = new RetrofitClient(); } return retrofitClient; } public Api getApi(){ return retrofit.create(Api.class); } }
到时候你们用的时候,需要修改Api接口和RetrofitClient类中的BASE_URL
用来保存每次服务端返回的数据
这个类中的数据是返回给PagedList中的 保存了所有的电影
public class Movies
{
//当前返回的数列
public int count;
//起始位置
public int start;
//一共多少数据
public int total;
//返回的电影列表
@SerializedName("subjects")
public List<Movie> movieList;
}
Movie保存每一部电影的信息
public class Movie
{
public String id;
public String title;
public String year;
public Images images;
public class Images{
public String small;
}
}
我们根据下图一步步来完成Paging组件分页请求数据并显示
MovieDataSource继承自PositionalDataSource,通过API Service得到网络数据
public class MovieDataSource extends PositionalDataSource<Movie> { //每次加载的数据 public static final int PER_PAGE = 8; //第一次请求会调用这个方法 @Override public void loadInitial(final LoadInitialParam params,final LoadInitialCallback<Movie> callback){ //从第0条数据开始加载 int startPosition = 0; //调用我们之前封装好的RetrofitClient请求网络数据 RetrofitClient.getInstance() .getApi() .getMovies(startPosition,PER_PAGE) .enqueue(new Callback<Movies>(){ @Override public void onResponse(Call<Movies> call,Response<Movies> response) { if(response.body()!=null){ //如果请求到了数据,就将请求到的数据发送出去 callback.onResult(response.body().movieList, response.body().start, response.body().total); } } @Override public void onFailure(Call<Movies> call, Throwable t) { Log.e("true",t.toString()); } }) ) //接下来的每次请求都会调用该方法 @Override public void loadInitial(final LoadInitialParam params,final LoadInitialCallback<Movie> callback){ //调用我们之前封装好的RetrofitClient请求网络数据 RetrofitClient.getInstance() .getApi() .getMovies(startPosition,PER_PAGE) .enqueue(new Callback<Movies>(){ @Override public void onResponse(Call<Movies> call,Response<Movies> response) { if(response.body()!=null){ //如果请求到了数据,就将请求到的数据发送出去 callback.onResult(response.body().movieList); } } @Override public void onFailure(Call<Movies> call, Throwable t) { Log.e("true",t.toString()); } }) ) } }
当页面首次加载数据时会调用loadInitial()方法。在该方法内,我们调用API接口,并设置从第1条数据开始加载。加载成功后,需要通过callback.onResult()方法将数据返回给PagedList,否则数据不会呗展示。
在 callback.onResult()方法中,需要注意的是第3个参数 int totalCount.如果在PagedList.Config中设置了setEnablePlaceholders()方法的值为true,那么需要通过totalCount参数告知PagedList当前服务端数据的总数。
setEnablePlaceholders()方法的作用是,是否需要为那些"数量已知,但尚未加载处理的数据"预留位置。 例如,我们通过loadInitial()方法首次请求数据,获取了8部电影的数据,并获知一共有70部新电信在上映,如果设置setEnablePlaceholders()为true,并且通过callback.onResult()方法的totalCount将70告诉了PagedList.那么RecyclerView一共会为你预留70个Item的位置。此时,将网络关闭。也可以看到,另外68个Item的效果。
需要注意的是:setEnablePlaceholders()默认为true,如果数据量很大的话,请设置为false,不然会消耗不必要的性能
loadRange()是加载下一页的数据。加载成功后,也需要通过callback.onResult()方法将数据返回给PagedList. 需要注意的是:start参数并不需要我们手动进行管理,Paging组件内部已经替我们完成了这些工作。
MovieDataSourceFactory 负责创建MovieDataSource,并使用LiveData包装MovieDataSource,将其暴露给MoveiViewModel
public class MovieDataSourceFactory extends DataSource.Factory(Integer,Moive){
private MutableLiveData<MovieDataSource> liveDataSource=new MutableLiveData<>();
@Override
public DataSource<Integer,Movie> create()
{
MovieDataSource dataSource = new MovieDataSource();
liveDataSource.postValue(dataSource);
return dataSource;
}
}
在MovieViewModel中通过LivePagedListBuilder创建和配置PagedList,并使用LiveData包装PagedList,将其暴露给MainActivity
public class MovieViewModel extends ViewModel { public LiveData<PageList<Movie>> moviePagedList; public MovieViewModel() { PagedList.Config config = (new PagedList.Config.Builder()) .setEnablePlaceholders(true) .setPageSize(MovieDataSource.PER_PAGE) .setPrefetchDistance(3) .setInitialLoadSizeHint(MovieDataSource.PER_PAGE*4) .setMaxSize(65536*MovieDataSource.PER_PAGE) .build(); moviePagedList= (new LivePagedListBuilder<>(new MovieDataSourceFactory(),config)).build(); } }
PagedList.Config中的几个重要方法:
列表数据通过MoviePagedListAdapter进行展示
public class MoviePagedListAdapter extends PagedListAdapter<Movie,MoviePagedListAdapter.MovieViewHolder > { private Context context; public MoviePagedListAdapter (Context context) { super(DIFF_CALLBACK); this.context = context; } private static DiffUtil.ItemCallback<Movie> DIFF_CALLBACK = new DiffUtil.ItemCallback<Movie>() { @Override public boolean areItemsTheSame(@NonNull MovieoldItem, @NonNull Movie newItem) { return oldItem.id.equals(newItem.id); } @SuppressLint("DiffUtilEquals") @Override public boolean areContentsTheSame(@NonNull MovieoldItem, @NonNull Movie newItem) { return oldItem.equals(newItem); } }; @NonNull @Override public MoviePagedListAdapter.MovieViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(context).inflate(R.layout.user_item,parent,false); return new UserViewHolder(view); } @Override public void onBindViewHolder(@NonNull MoviePagedListAdapter .MovieViewHolder holder, int position) { Movie movie = getItem(position); Log.e("true",movie .name); if(movie !=null){ Glide.with(context).load(movie .avatar).placeholder(R.drawable.ic_launcher_background).into(holder.imageView); holder.textView.setText(movie .name); }else{ holder.imageView.setImageResource(R.drawable.ic_launcher_background); holder.textView.setText("小鑫好看錒"); } } class MovieViewHolder extends RecyclerView.ViewHolder{ TextView textView; ImageView imageView; public UserViewHolder(@NonNull View itemView) { super(itemView); textView = itemView.findViewById(R.id.tv_text); imageView = itemView.findViewById(R.id.iv_img); } } }
MoviePagedListAdapter 需要继承自PagedListAdapter。在onBindViewHolder()方法中调用getItem()方法。若当前有数据,则直接将数据与UI控件进行绑定;若没有数据,则getItem()会通知PagedList去获取下一页的数据,PagedList收到通知后,让DataSource执行具体的数据获取工作。
DiffUtil工具用于计算两个数据列表之间的差异。在此之前,当我们更数据时,需要通过notifyDataSetChanged()方法对整个数据源进行刷新,这样的作法效率并不高,如果使用DiffUtil的话,它只会更新需要更新的数据,而不需要更新整个数据源。
DiffUtil中主要有两个方法,通过这两个方法,让更新数据变得更高效。
在其中,我们将RecyclerView与PagedListAdapter进行绑定。当数据发生变化时,该变化通过LiveData传递过来,再通过PagedLIstAdapter.submitList()方法刷新数据。
public class MainActivity extends AppCompatActivity{ private RecyclerView recyclerView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView = findViewById(R.id.recycleView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); final MoviePagedListAdapter moviePagedListAdapter = new MoviePagedListAdapter (this); MovieViewModel movieViewModel = new ViewModelProvider(this).get(MovieViewModel .class); movieViewModel.moviePageList.observe(this, new Observer<PagedList<User>>() { @Override public void onChanged(PagedList<Movie> movies) { Log.e("true",movies.toString()); moviePagedListAdapter .submitList(movies); } }); recyclerView.setAdapter(moviePagedListAdapter ); } }
好了,到这里PositionalDataSource的基本使用就结束了,你们需要更改下API接口,与API接口返回数据对应的Model即可使用了。
前面我们已经介绍了,PageKeyedDataSource适用于数据源以"页"的方式进行请求的情况。接下来,我们来看看PageKeyedDataSource是如何使用的。
https://api.stackexchange.com/2.2/users?page=1&pagesize=6&site=stackoverflow
参数page表示从第一页开始获取数据。参数pagesize表示每页6天数据。site参数表示数据来源是StavkOverflow网站,该参数是API接口必须要携带的,对于分页并没有意义。
由于返回的数据过多,我们定义的Model只选取了我们 需要的数据。
//导入viewModel
implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"
//导入retrofit
implementation "com.squareup.retrofit2:retrofit:2.6.2"
implementation "com.squareup.retrofit2:converter-gson:2.6.2"
//导入glide
implementation 'com.github.bumptech.glide:glide:4.9.0'
//导入paging
def paging_version = "2.1.2"
implementation "androidx.paging:paging-runtime:$paging_version"
testImplementation "androidx.paging:paging-common:$paging_version"
//导入recyclerView
implementation 'androidx.recyclerview:recyclerview:1.1.0'
由于需要用到网络请求,我们需要添加网络权限
<uses-permission android:name="android.permission.INTERNET"/>
public interface Api {
@GET("users")
Call<UserResponse> getUsers(
@Query("page") int page,
@Query("pagesize") int pageSize,
@Query("site") String site
);
}
public class RetrofitClient { private static final String BASE_URL = "https://api.stackexchange.com/2.2/"; private static RetrofitClient retrofitClient; private Retrofit retrofit; public RetrofitClient() { retrofit = new Retrofit.Builder().baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build(); } public static synchronized RetrofitClient getInstance(){ if(retrofitClient == null){ retrofitClient = new RetrofitClient(); } return retrofitClient; } public Api getApi(){ return retrofit.create(Api.class); } }
Retrofit的知识就不多说了,直接就可以代码了,不懂的可以自己去百度百度。
public class User { @SerializedName("account_id") public String id; @SerializedName("display_name") public String name; @SerializedName("profile_image") public String avatar; public User(String id, String name, String avatar) { this.id = id; this.name = name; this.avatar = avatar; } }
public class UserResponse {
@SerializedName("items")
public List<User> users;
@SerializedName("has_more")
public boolean hasMore;
}
hasMore代表该接口是否还有更多的数据,这里我们用它进行判断,是否还需要加载下一页的数据。
public class UserDataSource extends PageKeyedDataSource<Integer, User> { public static final int FIRST_PAGE = 1; public static final int PER_PAGE = 8; public static final String SITE = "stackoverflow"; @Override public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<Integer, User> callback) { RetrofitClient.getInstance() .getApi() .getUsers(FIRST_PAGE,PER_PAGE,SITE) .enqueue(new Callback<UserResponse>() { @Override public void onResponse(Call<UserResponse> call, Response<UserResponse> response) { Log.e("true",response.body().users.toString()); if(response.body()!=null){ callback.onResult(response.body().users,null,FIRST_PAGE+1); } } @Override public void onFailure(Call<UserResponse> call, Throwable t) { Log.e("true",t.toString()); } }); } @Override public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, User> callback) { } @Override public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, User> callback) { RetrofitClient.getInstance() .getApi() .getUsers(params.key,PER_PAGE,SITE) .enqueue(new Callback<UserResponse>() { @Override public void onResponse(Call<UserResponse> call, Response<UserResponse> response) { if(response.body()!=null){ Log.e("true",params.key+""); Integer nextKey = response.body().hasMore?params.key+1:null; callback.onResult(response.body().users,nextKey); } } @Override public void onFailure(Call<UserResponse> call, Throwable t) { } }); } }
该类继承自PageKeyedDataSource.。我们主要实现两个方法,loadInitial()与loadAfter()方法
loadInitial()方法: 和PositionalDataSource中的loadInitial()是一样的作用,都是加载第一页的数据,但是加载成功后的,都需要callback.onResult()方法将数据返回给PagedList。 但是传递的参数却不同。
上面我们详细说了PositionalDataSource中的callback.onResult()方法,这里就只说PageKeyedDataSource中的callback.onResult()方法
public abstract void onResult(@NonNull List<Value> data, @Nullable Key previousPageKey,
@Nullable Key nextPageKey);
第1个参数是加载得到的数据,将其交给PagedList。第2个参数是上一页的key。在此,由于当前加载的是第一页,不存在上一页,所以设置为null。第3个参数为下一页的key,即当前页key的值加上1,若不存在下一页,则返回null.
loadAfter()方法: 加载下一页的工作在该方法内进行。需要注意的是LoadParams params参数,我们在loadInitial()方法中设置的nextPageKey,正是通过LoadParams传递过来的。LoadParams.key得到的是下一页的key,通过这个key,我们就可以请求下一页。请求下一页成功后,同样也是通过callback.onResult()方法将数据返回给PagedList,同时再设置下一页的key。注意:在设置下一页之前,需要判断是否还有更多的数据,这时候的API接口返回的hasMore参数就起作用了,可以通过该参数,判断是否还有下一页数据,若没有数据,则将下一页的key设置为null.
Log.e("true",params.key+"");
Integer nextKey = response.body().hasMore?params.key+1:null;
callback.onResult(response.body().users,nextKey);
当我们不断滑动的时候,params.key是不断在变化,直到没有下一页数据的时候,会返回null。
UserDataSourceFactory负责创建UserDataSource,并使用LiveData包装UserDataSource,将其暴露给UserViewModel
public class UsersDataSourceFactory extends DataSource.Factory<Integer, User> {
private MutableLiveData<UserDataSource> liveData = new MutableLiveData<>();
@NonNull
@Override
public DataSource<Integer, User> create() {
UserDataSource dataSource = new UserDataSource();
liveData.postValue(dataSource);
return dataSource;
}
}
在UserViewModel中通过LivePagedListBuilder创建和配置PagedList,并使用LiveData包装PagedList,将其暴露给MainActivity
public class UserViewModel extends ViewModel {
public LiveData<PagedList<User>> userPageList;
public UserViewModel() {
PagedList.Config config = (new PagedList.Config.Builder())
.setEnablePlaceholders(true)
.setPageSize(UserDataSource.PER_PAGE)
.setPrefetchDistance(3)
.setInitialLoadSizeHint(UserDataSource.PER_PAGE*4)
.setMaxSize(65536*UserDataSource.PER_PAGE)
.build();
userPageList = (new LivePagedListBuilder<>(new UsersDataSourceFactory(),config)).build();
}
}
上面详细说了config中各个参数的作用,这里就不再细说了。
列表数据通过UserPagedListAdapter进行展示
public class UserPagedListAdapter extends PagedListAdapter<User,UserPagedListAdapter.UserViewHolder> { private Context context; public UserPagedListAdapter(Context context) { super(DIFF_CALLBACK); this.context = context; } private static DiffUtil.ItemCallback<User> DIFF_CALLBACK = new DiffUtil.ItemCallback<User>() { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.id.equals(newItem.id); } @SuppressLint("DiffUtilEquals") @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }; @NonNull @Override public UserPagedListAdapter.UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(context).inflate(R.layout.user_item,parent,false); return new UserViewHolder(view); } @Override public void onBindViewHolder(@NonNull UserPagedListAdapter.UserViewHolder holder, int position) { User user = getItem(position); // Log.e("true",user.name); if(user!=null){ Glide.with(context).load(user.avatar).placeholder(R.drawable.ic_launcher_background).into(holder.imageView); holder.textView.setText(user.name); }else{ holder.imageView.setImageResource(R.drawable.ic_launcher_background); holder.textView.setText("小鑫好看錒"); } } class UserViewHolder extends RecyclerView.ViewHolder{ TextView textView; ImageView imageView; public UserViewHolder(@NonNull View itemView) { super(itemView); textView = itemView.findViewById(R.id.tv_text); imageView = itemView.findViewById(R.id.iv_img); } } }
public class MainActivity extends AppCompatActivity{ private RecyclerView recyclerView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView = findViewById(R.id.recycleView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); final UserPagedListAdapter userPagedListAdapter = new UserPagedListAdapter(this); UserViewModel userViewModel = new ViewModelProvider(this).get(UserViewModel.class); userViewModel.userPageList.observe(this, new Observer<PagedList<User>>() { @Override public void onChanged(PagedList<User> users) { // Log.e("true",users.toString()); userPagedListAdapter.submitList(users); } }); recyclerView.setAdapter(userPagedListAdapter); } }
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content"> <ImageView android:id="@+id/iv_img" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher_background" /> <TextView android:id="@+id/tv_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="25sp" android:text="25"/> </LinearLayout>
无限滚动的效果就出来了,是不是感觉就是一次加载出来的。
好了,这节课篇幅也比较长了,剩下的paging知识点就放在下一节唠叨把。不足之处,欢迎大家留言,大家下期再见。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。