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组件的作用及使用场景,以及针对网络数据给出了3种不同的方法: PositionalDataSource、PageKeyedDataSource、ItemKeyedDataSource。我们详细地说明了PositionalDataSource、PageKeyedDataSource的使用方法。这篇文章,我们就来唠唠ItemKeyedDataSource的使用以及 网络数据+数据库在Paging中是如何使用的。
ItemKeyedDataSource的基本使用
ItemKeyedDataSource适用于当目标数据的下一页需要依赖于上一页数据中最后一个对象中的某个字段作为key的情况。
假设需求是从GitHub网站加载用户列表
API接口
https://api.github.com/users?since=0&per_page=6
需要注意的是,接口中的since参数并不是"Position"的意思,它不表示数据对象在数据源中的位置,它表示数据对象Item中的某个字段,以该字段作为请求下一页的key。参数per_page表示since所指代的对象之后的6条数据。
接口返回的数据
我们通过id这个字段,请求下一页的key,用来获取下一页的数据。
项目架构
和前面两个基本一致。
准备工作
1.导入依赖
//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'
2.添加网络权限
<uses-permission android:name="android.permission.INTERNET"/>
封装网络请求类
和前面所讲的一模一样,这里就不详细说明了。
public class RetrofitClient {
private static final String BASE_URL = "https://api.github.com/";
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);
}
}
Model类
public int id;
@SerializedName("login")
public String name;
@SerializedName("avatar_url")
public String avatar;
public User( String name, String avatar) {
this.name = name;
this.avatar = avatar;
}
DataSource类
public class UserDataSource extends ItemKeyedDataSource<Integer, User> {
public static final int PER_PAGE = 12;
@Override
public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<User> callback) {
int since = 0;
RetrofitClient.getInstance()
.getApi()
.getUsers(since,PER_PAGE)
.enqueue(new Callback<List<User>>() {
@Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
if(response.body()!=null){
callback.onResult(response.body());
}
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
Log.e("true",t.toString());
}
});
}
@Override
public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<User> callback) {
RetrofitClient.getInstance()
.getApi()
.getUsers(params.key,PER_PAGE)
.enqueue(new Callback<List<User>>() {
@Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
Log.e("true",params.key+"");
if(response.body()!=null){
callback.onResult(response.body());
}
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
Log.e("true",t.toString());
}
});
}
@Override
public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<User> callback) {
}
@NonNull
@Override
public Integer getKey(@NonNull User item) {
return item.id;
}
}
ItemKeyedDataSource: 主要有3个方法需要实现:loadInitial()、loadAfter()和getKey()
loadInitial(): 作用和前面基本一致,当页面首次加载数据时会调用loadInitial()方法。在该方法内调用API接口,请求key从0开始的第一页数据。
loadAfter(): 加载下一页的工作在该方法内进行,下一页的key通过LoadParams参数获得。
getKey(): 不同于前面所讲的两个数据源,我们不需要在loadInitial()和loadAfter()方法中设置page参数。我们通过Item对象的key(也就是id属性)作为请求下一页的key。因此,我们需要通过该方法将key告诉Paging组件。
item.id返回的数据如下图所示:
我们可以发现: item.id是当前页面最后一个item的id属性的值。
UserDataSourceFactory
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
在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();
}
}
UserPagedListAdapter
列表数据通过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.name.equals(newItem.name);
}
@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);
}
}
}
MainActivity
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);
}
}
MainActivity布局文件
<?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>
RecyclerView子布局
<?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>
运行程序,效果如下:
无限滚动的效果也出来了,是不是也像是一次性加载的。
BoundaryCallback
BoundaryCallback的意义
前面我们学了3种对网络数据进行分页的方法。在实际开发过程中,为了更好的用户体验,通常还需要对数据进行缓存。加入缓存之后,数据的来源从原来单一的网络数据源,变成了网络数据和本地数据组成的双数据源。多数据源会较大程度地增加应用程序的复杂度,需要处理好数据的时效性及新旧数据的切换更新等问题。为此,Google在Paging中加入了BoundaryCallback,通过BoundaryCallback可以简化应用的复杂度。
BoundaryCallback的使用流程分析
使用流程图如下:
在第一节的时候,当使用网络+数据库架构模型的时候,我们采用单一数据源作为解决方法。即从网络获取的数据,直接缓存进数据库,列表只从数据库这个唯一的数据源获取数据,所以数据库是页面的唯一数据来源。
上图实现步骤如下:
- 页面订阅了数据库的变化,当数据库中的数据发生时,会直接反映到页面上。若数据库中没有数据,会通知BoundaryCallback中的onZeroItemsLoaded()方法。若数据库中有数据,则当用户滑动到RecyclerView底部,且数据库中的数据以及全部加载完毕时,会通知BoundaryCallback中的onItemAtEndLoad()方法。后面我们会详细介绍。
- 当BoundaryCallback中的回调方法被调用时,需要在该方法内开启工作线程,请求网络数据。
- 当网络数据成功加载回来,我们并不直接展示这些数据,而是将其写入数据库。
- 由于已经设置了页面对数据库的订阅,当数据库有新数据写入时,会自动更新到页面。
- 当需要刷新数据时,可以通过页面的下拉刷新功能,在下拉过程中清空数据库。当数据库被清空时,由于数据库发生了变化,进而再次触发第1步,通知BoundaryCallback重新获取数据,由此形成了一个闭环。
我们在使用 ItemKeyedDataSource是基本使用基础上,使用BoundaryCallback和Room组件来实现上述步骤。
准备工作
在ItemKeyedDataSource的基础来,完成上述的功能
项目架构如下:
UserDataSource和UsersDataSourceFactort是没有使用到的,因为我是接着ItemKeyedDataSource的基础上来完成的。其中Api、RetrofitClient和UserAdapter类都没有变化。
导入依赖
在ItemKeyedDataSource的基础上导入Room数据库和下拉刷新插件
//导入Room
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
//下拉刷新
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
创建数据库
UserDatabase类
@Database(entities = {User.class},version = 1,exportSchema = false)
public abstract class UserDatabase extends RoomDatabase {
private static final String DATABASE_NAME = "user_db";
private static UserDatabase userDatabase;
public static synchronized UserDatabase getInstance(Context context){
if(userDatabase==null){
userDatabase = Room.databaseBuilder(
context.getApplicationContext(),
UserDatabase.class,
DATABASE_NAME)
.build();
}
return userDatabase;
}
public abstract UserDao userDao();
}
数据库Model类
我们在ItemKeyedDataSource的基础上对User类进行更改
@Entity(tableName = "user")
public class User {
@PrimaryKey()
@ColumnInfo(name = "id",typeAffinity = ColumnInfo.INTEGER)
public int id;
@ColumnInfo(name = "name",typeAffinity = ColumnInfo.TEXT)
@SerializedName("login")
public String name;
@ColumnInfo(name = "avatar",typeAffinity = ColumnInfo.TEXT)
@SerializedName("avatar_url")
public String avatar;
public User( int id,String name, String avatar) {
this.id = id;
this.name = name;
this.avatar = avatar;
}
}
数据库Dao文件
针对Model类实现对应的Dao文件,以方便对Model数据进行增/删/改查。
@Dao
public interface UserDao {
//插入数据
@Insert
void insertUsers(List<User> users);
//删除数据
@Query("DELETE FROM user")
void clear();
//查询数据
@Query("SELECT * FROM user")
DataSource.Factory<Integer,User> getUserList();
}
需要注意的是: getUserList()方法返回的是一个DataSource.Factory,前面提到过的页面对数据库的订阅便是通过这里实现的。
Room数据库便已经配置好了,接下来需要去Build一下。成功后,会生成下面两个类。
如果大家对Room数据库还不熟悉的话,可以去看看我写的关于Room的两篇文章。
实现BoundaryCallback
public class UserBoundaryCallback extends PagedList.BoundaryCallback<User> {
private String TAG = this.getClass().getName();
private Application application;
public UserBoundaryCallback(Application application) {
this.application = application;
}
@Override
public void onZeroItemsLoaded() {
super.onZeroItemsLoaded();
getTopData();
}
@Override
public void onItemAtFrontLoaded(@NonNull User itemAtFront) {
super.onItemAtFrontLoaded(itemAtFront);
}
@Override
public void onItemAtEndLoaded(@NonNull User itemAtEnd) {
super.onItemAtEndLoaded(itemAtEnd);
getTopAfterData(itemAtEnd);
}
//加载第一页数据
private void getTopData() {
int since = 0;
RetrofitClient.getInstance()
.getApi()
.getUsers(since,UserViewModel.PER_PAGE)
.enqueue(new Callback<List<User>>() {
@Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
if(response.body()!=null){
//将网络数据存储到数据库中
insertUsers(response.body());
}
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
}
});
}
//加载下一页数据
private void getTopAfterData(User itemAtEnd) {
RetrofitClient.getInstance()
.getApi()
.getUsers(itemAtEnd.id,UserViewModel.PER_PAGE)
.enqueue(new Callback<List<User>>() {
@Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
if(response.body()!=null){
insertUsers(response.body());
}
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
}
});
}
//插入数据到数据库中
private void insertUsers(List<User> body) {
AsyncTask.execute(new Runnable() {
@Override
public void run() {
UserDatabase.getInstance(application)
.userDao()
.insertUsers(body);
}
});
}
}
BoundaryCallback有3个回调方法,其中onZeroItemsLoaded()方法和onItemAtEndLoad()方法是需要关注的重点。
1.onZeroItemsLoaded():当数据库为空时,会回调该方法,在该方法内请求第一页的数据。
2.onItemAtEndLoad(): 当用户滑动到页面的最下方,并且数据库中的数据已全部加载完毕时,该方法会被回调,我们在该方法内请求下一页的数据。注意:该方法的参数返回的是数据库中最后一条数据,请求下一页所需的key就在该数据库中(为User对象的id字段。)
在这两个方法中,当数据请求成功后都是直接写入数据库的。不用担心数据不被展示,正如前面所说,页面订阅了数据库的变化,当数据库增加了新的数据时,会自动被展示出来。 代码如下:
public class UserViewModel extends AndroidViewModel {
public LiveData<PagedList<User>> userPageList;
public static final int PER_PAGE = 8;
public UserViewModel(Application application) {
super(application);
UserDatabase database = UserDatabase.getInstance(application);
userPageList = (new LivePagedListBuilder<>(
database.userDao().getUserList(),
UserViewModel.PER_PAGE))
.setBoundaryCallback(new UserBoundaryCallback(application))
.build();
}
//刷新数据
public void refresh(){
AsyncTask.execute(new Runnable() {
@Override
public void run() {
UserDatabase.getInstance(getApplication())
.userDao()
.clear();
}
});
}
Room组件对Paging组件提供了原生支持,因此LivePagedListBuilder在创建PagedList时,可以直接将Room作为数据源。接着,再通过setBoundaryCallback()方法,将PagedList与BoundaryCallback关联起来。
需要注意的是: 数据库的使用需要用到Context,所以UserViewModel需继承自AndroidViewModel.
随着页面的滑动,BoundaryCallback不断请求新数据并写入到数据库,页面也不断得到更新。 但是还存在一个问题,如何从头更新数据呢? 这里我们使用下拉刷新。
当用户执行下拉刷新时,调用refresh()方法,开启一个工作线程,清空数据库,然后BoundaryCallback就会重新去网络上请求数据,并写入到数据库,数据库中的数据就得以更新了。
//刷新数据
public void refresh(){
AsyncTask.execute(new Runnable() {
@Override
public void run() {
UserDatabase.getInstance(getApplication())
.userDao()
.clear();
}
});
更改MainActivity布局文件
在布局文件中为RecyclerView添加下拉刷新组件
<?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.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
MainActivity
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));
SwipeRefreshLayout swipeRefresh = findViewById(R.id.swipeRefresh);
final UserPagedListAdapter userPagedListAdapter = new UserPagedListAdapter(this);
// UserViewModel userViewModel = new ViewModelProvider(this).get(UserViewModel.class);
UserViewModel userViewModel = new ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication())).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);
//刷新数据
swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
userViewModel.refresh();
swipeRefresh.setRefreshing(false);
}
});
}
}
需要注意一点:
我们获取UserViewModel不能在通过如下代码进行获取了
UserViewModel userViewModel = new ViewModelProvider(this).get(UserViewModel.class);
会报如下错误
RuntimeException: Cannot create an instance of class com.example.android.roomwordssample.WordViewMod
我也不知道该错误是为啥,但是换成如下方法获取UserViewModel就可以正常运行了。
UserViewModel userViewModel = new ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication())).get(UserViewModel.class);
运行程序,效果如下:
除了拥有下拉刷新,当我们开启设备的飞行模式,再重新打开应用程序时,可以看到饮用程序会显示之前的缓存过的数据,这就是BoundaryCallback带来的好处。
总结
至此,我们已经学习了3种对网络数据进行分页的方法。具体选用哪种DataSource,我们需要根据服务端API接口的设计。 我们使用BoundaryCallback实现了网络+数据库进行分页。我们前面所说的Paging支持的3种架构模型,我们实现了2种,只剩下对数据库数据进行分页,大家可以自行去实现下,无非是更换数据源的问题。
好了 Paging组件就讲到这里,不足之处,欢迎大家留言。更加详情的Paging应用,大家可以自行去官网学习学习。