【翻译】安卓架构组件(7)-分页库

相关文章:

分页库使你的app能够在需要从一个数据源逐步加载数据时更为轻松,避免了过度加载以及为了一个大型数据的查询等待太多时间。

综述

许多app伴随着大量的数据,但是在任意时刻往往只需要加载和显示其中的一小部分。一个app可能存在数以万计的数据以供显示,但在某一时刻仅仅需要访问其中的几十条。如果app没有认真处理这个这个问题,会请求实际上并不需要的数据,并导致不必要的性能负担和网络带宽。如果数据并没有被存储起来或同步到远程数据库,这同样会减慢app的运行速度以及浪费用户的流量。

当然现有的安卓API允许对内容进行分页,它们伴有明显的约束和缺点:

新的分页库致力于解决这些问题。这个类库包含一些类,以流水线的方式处理你所需数据的请求过程。这些类也可以和已经存在的架构类组件无缝结合,如Room

分页库提供以下的类以及额外的支持类:

DataSource

使用这个类定义一个你用来分页拉取的数据源。根据如何访问你的数据,你可以集成以下两个子类之一:

如果你使用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

图1 分页库组件在后台线程执行大部分工作,因此并不会影响UI线程

新创建的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);
        }
    }
}