【翻译】安卓架构组件(7)-分页库
相关文章:
- 【翻译】安卓架构组件(1)-App架构指导
- 【翻译】安卓架构组件(2)-添加组件到你的项目中
- 【翻译】安卓架构组件(3)-处理生命周期
- 【翻译】安卓架构组件(4)-LiveData
- 【翻译】安卓架构组件(5)-ViewModel
- 【翻译】安卓架构组件(6)-Room持久化类库
分页库使你的app能够在需要从一个数据源逐步加载数据时更为轻松,避免了过度加载以及为了一个大型数据的查询等待太多时间。
综述
许多app伴随着大量的数据,但是在任意时刻往往只需要加载和显示其中的一小部分。一个app可能存在数以万计的数据以供显示,但在某一时刻仅仅需要访问其中的几十条。如果app没有认真处理这个这个问题,会请求实际上并不需要的数据,并导致不必要的性能负担和网络带宽。如果数据并没有被存储起来或同步到远程数据库,这同样会减慢app的运行速度以及浪费用户的流量。
当然现有的安卓API允许对内容进行分页,它们伴有明显的约束和缺点:
CursorAdapter
使得将数据库查询结果映射到ListView
列表项更为轻松,但它会在UI线程执行数据库查询,并且使用Cursor
的分页并不高效。对于使用CursorAdapter
的更多缺陷,请查询这里。AsyncListUtil
允许将基于位置的分页数据加载至RecyclerView
,但是并不允许无位置的分页,而且它会强迫使用空白占用位。
新的分页库致力于解决这些问题。这个类库包含一些类,以流水线的方式处理你所需数据的请求过程。这些类也可以和已经存在的架构类组件无缝结合,如Room
。
类
分页库提供以下的类以及额外的支持类:
DataSource
使用这个类定义一个你用来分页拉取的数据源。根据如何访问你的数据,你可以集成以下两个子类之一:
KeyedDataSource
如果你需要从第N个数据项获取第N+1个数据项。例如,如果你的线程在一个讨论的app获取评论,你可能需要传递一个评论的id来获取下一个评论。TiledDataSource
如果你需要从你的数据源中获取任意指定位置的分页数据。这个类支持从你选择的任何位置请求数据,例如:“返回从位置1200开始的20条数据”。
如果你使用Room
来管理你的数据,它会自动创建一个 TiledDataSource
,例如:
@Query("select * from users WHERE age > :age order by name DESC, id ASC")
TiledDataSource<User> usersOlderThan(int age);
PagedList
这个类从DataSource
加载数据。你可以配置在任何时刻该获取多少数据,以及需要预加载多少数据,以最小化用户等待数据被加载时等待的时间。这个类可以给其他类提供更新信号,例如RecyclerView.Adapter
,允许当数据分页加载时更新你的RecyclerView
。
PagedListAdapter
这个类是 RecyclerView.Adapter
的子类,使得数据从PagedList
中展现。例如,当新的一页数据被加载时,PagedListAdapter
通知RecyclerView
数据已经到达,这让RecyclerView
替换任何占位项,并展示合适的动画效果。
PagedListAdapter
使用后台线程计算从一个PagedList
到另一个的变化(例如,当数据库的变更导致数据的更新),并调用notifyItem...()
方法,并在需要时更新列表数据内容。之后RecyclerView
展现必要的变化。例如,当一个数据项在不同的PagedList
版本间变更位置时,RecyclerView
动画会展示数据项移动到新的位置。
LivePagedListProvider
这个生成从你提供的DataSource
生成一个LiveData<PagedList>
。此外,如果你使用Room
持久化类库来管理你的数据库,DAO可以使用TiledDataSource
生成LivePagedListProvider
,例如:
@Query("SELECT * from users order WHERE age > :age order by name DESC, id ASC")
public abstract LivePagedListProvider<Integer, User> usersOlderThan(int age);
Integer
参数告知Room基于位置加载的TiledDataSource
。
分页库组件在后台线程组织了数据流,并在UI线程展示。例如:当一个新的数据项被插入到你的数据库时,DataSource
会失效, 后台线程产生一个新的PagedList
。
新创建的PagedList
在UI线程被发送到PagedListAdapter
。之后PagedListAdapter
使用DiffUtil
在后台线程计算当前列表和新列表的不同。当比较结束时,PagedListAdapter
使用比较得到的差异信息适当地调用RecyclerView.Adapter.notifyItemInserted()
来通知新的数据项被插入。
RecyclerView
在UI线程知道仅仅需要绑定一个新的数据项,并使用动画展示在屏幕上。
下面的代码示例显示了所有相关部分的工作。当数据库添加、删除或者更新时,RecyclerView
的内容自动且高效地更新:
@Dao
interface UserDao {
@Query("SELECT * FROM user ORDER BY lastName ASC")
public abstract LivePagedListProvider<Integer, User> usersByLastName();
}
class MyViewModel extends ViewModel {
public final LiveData<PagedList<User>> usersList;
public MyViewModel(UserDao userDao) {
usersList = userDao.usersByLastName().create(
/* 初始化加载位置 */ 0,
new PagedList.Config.Builder()
.setPageSize(50)
.setPrefetchDistance(50)
.build());
}
}
class MyActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
RecyclerView recyclerView = findViewById(R.id.user_list);
UserAdapter<User> adapter = new UserAdapter();
viewModel.usersList.observe(this, pagedList -> adapter.setList(pagedList));
recyclerView.setAdapter(adapter);
}
}
class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
public UserAdapter() {
super(DIFF_CALLBACK);
}
@Override
public void onBindViewHolder(UserViewHolder holder, int position) {
User user = getItem(position);
if (user != null) {
holder.bindTo(user);
} else {
// NULL时定义了一个占位项——当实际的对象被从数据库加载时,PagedListAdapter会自动失效该行
holder.clear();
}
}
public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() {
@Override
public boolean areItemsTheSame(@NonNull User oldUser, @NonNull User newUser) {
// 用户属性可能会在重新加载时变化,但id是固定的。
return oldUser.getId() == newUser.getId();
}
@Override
public boolean areContentsTheSame(@NonNull User oldUser, @NonNull User newUser) {
// 注意:如果你使用equals,你的对象会重载Object.equals()方法
// 如果不正确地返回false会导致许多动画效果
return oldUser.equals(newUser);
}
}
}