Jetpack —— Paging 让分页加载更高效

Paging 库可以帮助我们加载显示一小部分数据。按需加载部分数据可以减少网络带宽和系统资源的占用。

本指南提供了库的几个概念性示例和它们是如何工作的概述。

在项目中引入 Paging,需要添加以下依赖:

dependencies {
  def paging_version = "2.1.0"

  implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
}

Paging 架构

本节主要描述 Paging 库的主要组成部分。

PagedList

Paging 库的关键就是 PagedList 类,它会加载页面或者应用数据的一部分。由于需要很多的数据,因为将它们分页到现有的 PagedList 对象当中。如果任何加载的数据发生更改,则会从 基于 LiveData 或 RxJava2 的对象向可观察数据持有者发出新的 PagedList 实例。生成新的 PagedList 对象时,应用程序的 UI 会显示其内容,同时遵守 UI 控制器的生命周期。

下面的代码显示了如何使用 PagedList 对象的 LiveData 持有者配置应用程序的视图模型以加载和显示数据:

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    // Creates a PagedList object with 50 items per page.
    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
                concertDao.concertsByDate(), 50).build();
    }
}

Data

PagedList 的每个实例都从其相应的 DataSource 对象加载应用程序数据的最新快照。数据从应用程序的后端或数据库流入 PagedList 对象。

以下示例使用 Room 库来组织应用程序的数据,但如果要使用其他方法存储数据,还可以提供自己的数据源工厂。

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a
    // PositionalDataSource object.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

要了解有关如何将数据加载到 PagedList 对象的更多信息,请参阅有关如何加载分页数据的指南。

UI

在 RecyclerView 中设置 PagedListAdapter 和 PagedList类 ,这些类一起工作以在加载内容时获取和显示内容,预取视图内容并动画内容更改。

支持不同的数据架构

Paging 库支持从以下数据结构:

  • 仅从服务器获取数据
  • 仅从设备上存储的数据库中获取数据
  • 从其他源获取数据,设备上的数据库作为缓存的组合方式

上图显示了每种架构中数据的流动方式。对于仅限网络或者仅限数据库的方案,数据直接流向程序的 UI 模型。如果您使用的是组合方式,则数据会从后端服务器流向设备上的数据库,然后流入程序的 UI 模型。每隔一段时间,每个数据流的端点就会耗尽要加载的数据,此时它会从提供数据的组件请求更多的数据,例如当设备上的数据库用完数据时,它会从服务器请求更多的数据。

仅从网络获取数据

要显示来自服务器的数据,请使用 Retrofit API 的同步版本将信息加载到你自己定义的 DataSource 对象中。

注意,Paging 库的 DataSource 对象不提供任何错误处理,因为不同的程序使用不同的方式处理和显示错误 UI。如果发生错误,请遵循结果回调,稍候重试该请求。有关这样的例子,请参阅 PagingWithNetwork 示例

仅从本地数据库获取数据

我们需要设置 RecyclerView 观察本地存储,最好使用 Room数据库,这样,无论何时在程序的数据库中插入或修改数据,这些更改都会自动反映在显示此数据的 RecyclerView 中。

数据库和网络搭配使用

在开始观察数据库后,可以使用 PagedList.BoundaryCallback 监听实时监控什么时候没有数据。然后就可以从网络中获取更多并将其插入到数据库中。

处理网络错误

当我们使用网络获取或分页 Paging Library 显示的数据时,不要一直将网络视为“可用”或“不可用”,因为许多连接是断断续续性的或片段状的:

  • 服务器可能无法响应网络请求。
  • 设备可能连接到缓慢或弱的网络。

相反,我们的应用应检查每个失败请求,并在网络不可用的情况下尽可能地恢复。例如,我们可以提供“重试”按钮,在数据刷新步骤不起作用时,可以供用户选择。如果在数据分页步骤期间发生错误,则最好自动重试分页请求。

更新现有应用

如果 APP 从数据库或后端中获取数据,可以直接升级到 Paging 库提供的功能。本节介绍如何升级现有设计的应用程序。

定制分页解决方案

如果你使用自定义功能从App的数据源分页加载数据,就可以将此逻辑替换为 PagedList 类中的逻辑 。PagedList 的实例提供与数据源的内置连接 ,PagedList 的实例还为 UI 中的 RecyclerView 提供适配器。

使用列表而不是页面加载数据

如果你为 UI 适配器 使用处于内存中的列表作为后备数据结构,如果列表中的条目数可能增大,可以考虑使用 PagedList 类来观察数据更新。PagedList 的实例可以使用 LiveData< PagedList>Observable<List> 将数据更新传递到应用程序的UI,从而最大限度地减少加载时间和内存使用量。而且使用 PagedList 对象替换应用程序中的 List 的不需要对应用程序的 UI 结构或数据更新逻辑进行任何更改。

使用CursorAdapter将数据光标与列表视图相关联

如果你的应用程序使用 CursorAdapter 将数据从一个 Cursor 关联到 ListView。在这种情况下,你通常需要从 ListView 迁移到 RecyclerView 作为应用程序的列表UI容器,然后将该 Cursor 组件替换为 Room 或 PositionalDataSource,取决于 Cursor 实例是否访问 SQLite数据库。

在某些情况下,例如在处理 Spinner 实例时 ,你只需提供适配器本身,然后将获取到的数据加载到该适配器中并显示。在这些情况下,将适配器数据类型更改为 LiveData< PagedList> ,然后将此列表封装在 ArrayAdapter 对象中。

使用 AsyncListUtil 异步加载内容

如果我们使用 AyncListUtil 对象异步加载和显示信息,Paging Library 可以让我们更轻松地加载数据:

  • 无需知道我们的数据源。Paging 库允许我们通过网络请求直接从后端加载数据。
  • 无需关心我们的数据大小。使用 Paging 库,我们可以将数据分页加载到页面中,直到没有剩余数据。
  • 我们可以更轻松地观察数据。因为 ViewModel 持有一个可观察的数据结构。

注意:如果你的应用程序需要访问SQLite数据库,可以查看 Android架构组件- Room数据库的使用

数据库示例

以下代码片段显示了几种可能的协同工作方法。

使用LiveData观察分页数据

以下代码段显示了一起工作的所有部分。 随着在数据库中添加,删除或更改 Concert 事件,RecyclerView 中的内容将自动且高效地更新:

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
            concertDao.concertsByDate(), /* page size */ 50).build();
    }
}

public class ConcertActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ConcertViewModel viewModel =
                ViewModelProviders.of(this).get(ConcertViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.concert_list);
        ConcertAdapter adapter = new ConcertAdapter();
        viewModel.concertList.observe(this, adapter::submitList);
        recyclerView.setAdapter(adapter);
    }
}

public class ConcertAdapter
        extends PagedListAdapter<Concert, ConcertViewHolder> {
    protected ConcertAdapter() {
        super(DIFF_CALLBACK);
    }

    @Override
    public void onBindViewHolder(@NonNull ConcertViewHolder holder,
            int position) {
        Concert concert = getItem(position);
        if (concert != null) {
            holder.bindTo(concert);
        } else {
            // Null defines a placeholder item - PagedListAdapter automatically
            // invalidates this row when the actual object is loaded from the
            // database.
            holder.clear();
        }
    }

    private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<Concert>() {
        // Concert details may have changed if reloaded from the database,
        // but ID is fixed.
        @Override
        public boolean areItemsTheSame(Concert oldConcert, Concert newConcert) {
            return oldConcert.getId() == newConcert.getId();
        }

        @Override
        public boolean areContentsTheSame(Concert oldConcert,
                Concert newConcert) {
            return oldConcert.equals(newConcert);
        }
    };
}

使用RxJava2观察分页数据

如果你更喜欢使用 RxJava2 而不是 LiveData,则可以创建一个 ObservableFlowable 对象:

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final Observable<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;

        concertList = new RxPagedListBuilder<>(
                concertDao.concertsByDate(), /* page size */ 50)
                        .buildObservable();
    }
}

然后你可以使用下面的代码开始和停止观察数据:

public class ConcertActivity extends AppCompatActivity {
    private ConcertAdapter adapter = new ConcertAdapter();
    private ConcertViewModel viewModel;

    private CompositeDisposable disposable = new CompositeDisposable();

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        RecyclerView recyclerView = findViewById(R.id.concert_list);

        viewModel = ViewModelProviders.of(this).get(ConcertViewModel.class);
        recyclerView.setAdapter(adapter);
    }

    @Override
    protected void onStart() {
        super.onStart();
        disposable.add(viewModel.concertList
                .subscribe(adapter.submitList(flowableList)
        ));
    }

    @Override
    protected void onStop() {
        super.onStop();
        disposable.clear();
    }
}

不管是基于 RxJava2 的解决方案还是基于 LiveData 的解决方案, ConcertDaoConcertAdapter 的代码是一样的。

显示分页列表

本节以上面的内容为基础,描述了如何在应用 UI 中向用户展示 List,尤其是当 List 信息发生变化时。

连接 UI 到视图模型

你可以将 LiveData<PagedList> 的实例连接到 PagedListAdapter,就像下面一样

public class ConcertActivity extends AppCompatActivity {
    private ConcertAdapter adapter = new ConcertAdapter();
    private ConcertViewModel viewModel;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewModel = ViewModelProviders.of(this).get(ConcertViewModel.class);
        viewModel.concertList.observe(this, adapter::submitList);
    }
}

当数据源(LiveData<PagedList<Value>>)中的 PagedList 有新实例时,activity 会将这些对象发送给适配器。实现了 PagedListAdapter 的类定义了如何计算更新,并自动处理分页和列表差异。因此,我们的 ViewHolder 只需要绑定到特定的提供 Item :

public class ConcertAdapter
        extends PagedListAdapter<Concert, ConcertViewHolder> {
    protected ConcertAdapter() {
        super(DIFF_CALLBACK);
    }

    @Override
    public void onBindViewHolder(@NonNull ConcertViewHolder holder,
            int position) {
        Concert concert = getItem(position);

        // Note that "concert" can be null if it's a placeholder.
        holder.bindTo(concert);
    }

    private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK
            = ... // See Implement the diffing callback section.
}

PagedListAdapter 使用 PagedList.Callback 对象来处理分页加载事件。当用户滑动列表时,PagedListAdapter 会调用 PagedList.loadAround() 为底层的 PagedList 提供关于它应从 DataSource 中获取哪些 items 的提示 。

注意:PagedList 是内容不可变的。这意味着,虽然可以将新内容加载到 PagedList 的实例中,但加载的 items 本身一旦加载就无法更改。因此,如果要更新 PagedList 中的内容,则 PagedListAdapter 对象会接收到包含更新信息的全新内容。

实现差异回调

以下例子显示了 areContentsTheSame() 的实现,它比较了相关的对象字段:

private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
        new DiffUtil.ItemCallback<Concert>() {

    @Override
    public boolean areItemsTheSame(Concert oldItem, Concert newItem) {
        // The ID property identifies when items are the same.
        return oldItem.getId() == newItem.getId();
    }

    @Override
    public boolean areContentsTheSame(Concert oldItem, Concert newItem) {
        // Don't use the "==" operator here. Either implement and use .equals(),
        // or write custom data comparison logic here.
        return oldItem.equals(newItem);
    }
};

由于适配器比较项的定义,因此适配器会在加载新的 PagedList 对时自动检测对这些项的更改。因此,适配器会在 RecyclerView 对象中出发高效的动画。

使用不同的适配器类型进行区分

如果你不继承 PagedListAdapter,例如当你使用自己的适配器时,你仍然可以通过直接使用 AsyncPagedListDiffer 对象来使用 Paging Library 适配器的 diffing 功能 。

在UI中提供占位符

如果你希望 UI 在应用完成获取数据之前就显示列表,你可以向用户显示占位符列表项。PagedList 通过将列表项数据显示为 null 来处理这样的情况,知道数据加载。

默认情况下,Paging 库启用占位符

占位符有以下好处:

  • 支持滚动条:PagedList 为 PagedListAdapter 提供列表项的数量,该信息允许适配器绘制一个滚动条,传达列表的完整大小。加载新页面时,滚动条不会跳转,因为列表不会更改大小。
  • 无须加载微调器:因为列表大小是已知的,因此无须提醒用户正在加载更多项目。占位符本身传到了这些信息。

在添加占位符支持之前,需要记住以下几点:

  • 需要可数数据集:Room持久性库中的 DataSource 实例可以有效地计算其条目。但是,如果你使用的是自定义本地存储解决方案或仅限网络的数据结构,那么确定数据集中包含多少项可能很难甚至无法实现。
  • 需要适配器来考虑卸载的 items:用于准备列表的适配器或呈现机制需要处理空列表项。例如,将数据绑定到 ViewHolder 时,需要提供默认值来表示卸载的数据。
  • 需要相同大小的条目视图大小:如果列表条目的大小可以根据其内容(例如社交网络更新)进行更改,则条目之间的交叉淡化会看起来不太好。强烈建议在这种情况下禁用占位符。

收集分页数据

这节内容以 Paging 库为基础,讨论如何自定义程序的数据加载方法以满足应用的需求。

构建可观察列表

通常,我们的UI代码会观察一个 LiveData< PagedList> 对象(如果是 RxJava2,则是一个 Flowable<PagedList>Observable<PagedList> 对象),它位于我们的应用程序中的 ViewModel 中。可观察对象将 App 列表数据的表示和内容连接在了一起。

为了创建可观察 PagedList 对象,需要将 DataSource.Factory 的实例传递给 LivePagedListBuilderRxPagedListBuilder 对象。一个 DataSource 对象加载单个PagedList 的页面。工厂类创建 PagedList 的新实例 以响应内容更新,例如数据库表失效和网络刷新。Room 数据库 可以为我们提供 DataSource.Factory 对象,或者我们也可以构建自己的对象。

以下代码段展示如何使用 Room 的 DataSource.Factory 构建功能 在 App 的 ViewModel 类中创建 LiveData< PagedList> 的新实例:

ConcertDao

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

ConcertViewModel

// The Integer type argument corresponds to a PositionalDataSource object.
DataSource.Factory<Integer, Concert> myConcertDataSource =
       concertDao.concertsByDate();

LiveData<PagedList<Concert>> concertList =
        LivePagedListBuilder(myConcertDataSource, /* page size */ 50).build();

定义分页配置

想要为高级用例进一步配置 LiveData< PagedList> ,我们还可以定义自己的分页配置。特别是,我们可以定义以下属性:

  • 页面大小: 每页中的条目数。
  • 预取距离: 给定应用UI中的最后一个可见项,超出最后一项的条目数,Paging 库应该提前尝试获取。此值应该是页面大小的几倍。
  • 占位符存在: 确定UI是否显示尚未完成加载的列表项的占位符。有关使用占位符的优缺点的讨论,请查看如何在UI中提供占位符。

如果我们希望更好地控制 Paging 库何时从 App 的数据库加载列表,需要将自定义 Executor 对象传递给 LivePagedListBuilder,如下面的代码片段所示:

ConcertViewModel

PagedList.Config myPagingConfig = new PagedList.Config.Builder()
        .setPageSize(50)
        .setPrefetchDistance(150)
        .setEnablePlaceholders(true)
        .build();

// The Integer type argument corresponds to a PositionalDataSource object.
DataSource.Factory<Integer, Concert> myConcertDataSource =
        concertDao.concertsByDate();

LiveData<PagedList<Concert>> concertList =
        new LivePagedListBuilder<>(myConcertDataSource, myPagingConfig)
            .setFetchExecutor(myExecutor)
            .build();

选择正确的数据源

连接到处理源数据结构的数据源非常重要:

  • 如果我们加载的页面中嵌入了下一个/上一个键,则需要使用 PageKeyedDataSource 。例如,如果从网络中获取社交媒体帖子,则可能需要将 nextPage token 从一个加载传递到后续加载。
  • 如果我们需要使用 条目 N 中的数据获取条目 N+1 ,则需要使用 ItemKeyedDataSource。例如,如果我们要为讨论应用获取线性评论,则可能需要传递最后一条评论的ID以获取下一条评论的内容。
  • 如果我们需要从数据存储中选择任何位置来获取页面数据 ,可以使用 PositionalDataSource 。此类支持从我们选择的任何位置开始请求一组数据项。例如,请求可能需要返回以位置 1200 开头的 20 个数据项。

数据失效时通知

当使用 Paging Library 时,如果数据过期或失效,由数据层通知应用程序的其他层。为此,我们可以选择调用 DataSourceinvalidate() 方法。

注意:应用程序的UI可以使用滑动刷新触发此数据失效功能。

构建自己的数据源

如果我们使用自定义本地数据的解决方案,或者直接从网络加载数据,则可以实现 DataSource 的一个子类。以下代码显示了一个数据源,该数据源取决于给定Concert的开始时间:

public class ConcertTimeDataSource
        extends ItemKeyedDataSource<Date, Concert> {
    @NonNull
    @Override
    public Date getKey(@NonNull Concert item) {
        return item.getStartTime();
    }

    @Override
    public void loadInitial(@NonNull LoadInitialParams<Date> params,
            @NonNull LoadInitialCallback<Concert> callback) {
        List<Concert> items =
            fetchItems(params.key, params.requestedLoadSize);
        callback.onResult(items);
    }

    @Override
    public void loadAfter(@NonNull LoadParams<Date> params,
            @NonNull LoadCallback<Concert> callback) {
        List<Concert> items =
            fetchItemsAfter(params.key, params.requestedLoadSize);
        callback.onResult(items);
    }

然后你可以通过创建 DataSource.Factory 的具体子类将该自定义数据加载到 PagedList 对象中。下面的代码显示了如何生成前面代码中定义的自定义数据源的新对象:

public class ConcertTimeDataSourceFactory
        extends DataSource.Factory<Date, Concert> {
    private MutableLiveData<ConcertTimeDataSource> sourceLiveData =
            new MutableLiveData<>();

    private ConcertDataSource latestSource;

    @Override
    public DataSource<Date, Concert> create() {
        latestSource = new ConcertTimeDataSource();
        sourceLiveData.postValue(latestSource);
        return latestSource;
    }
}

内容更新的工作原理

在构造可观察的 PagedList 对象时,请考虑内容更新的工作方式。如果您直接从 Room 中加载数据,则会自动将更新推送到应用的 UI。

使用分配网络 API 时,您通常会进行用户交互,例如“滑动刷新”,作为最近使用的数据源失效的信号。然后你再请求该数据源的新实例。下面的代码演示了这个操作:

public class ConcertActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        // ...
        viewModel.getRefreshState()
                .observe(this, new Observer<NetworkState>() {
            // Shows one possible way of triggering a refresh operation.
            @Override
            public void onChanged(@Nullable MyNetworkState networkState) {
                swipeRefreshLayout.isRefreshing =
                        networkState == MyNetworkState.LOADING;
            }
        };

        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshListener() {
            @Override
            public void onRefresh() {
                viewModel.invalidateDataSource();
            }
        });
    }
}

public class ConcertTimeViewModel extends ViewModel {
    private LiveData<PagedList<Concert>> concertList;
    private DataSource<Date, Concert> mostRecentDataSource;

    public ConcertTimeViewModel(Date firstConcertStartTime) {
        ConcertTimeDataSourceFactory dataSourceFactory =
                new ConcertTimeDataSourceFactory(firstConcertStartTime);
        mostRecentDataSource = dataSourceFactory.create();
        concertList = new LivePagedListBuilder<>(dataSourceFactory, 50)
                .setFetchExecutor(myExecutor)
                .build();
    }

    public void invalidateDataSource() {
        mostRecentDataSource.invalidate();
    }
}

提供数据映射

Paging Library 支持 从 DataSource 加载基于 item 和基于页面 转换 的 item 。

在以下代码段中,Concert 名称和 Concert 日期的组合会被映射到包含名称和日期的单个字符串中:

public class ConcertViewModel extends ViewModel {
    private LiveData<PagedList<String>> concertDescriptions;

    public ConcertViewModel(MyDatabase database) {
        DataSource.Factory<Integer, Concert> factory =
                database.allConcertsFactory().map(concert ->
                    concert.getName() + "-" + concert.getDate());
        concertDescriptions = new LivePagedListBuilder<>(
            factory, /* page size */ 50).build();
    }
}

如果我们要在加载 items 后对其进行封装,转换或准备,这可能很有用。由于此工作是在 executor(线程池)上完成的,因此我们可以执行可能一些代价高昂的工作,例如从磁盘读取或查询一个单独的数据库。

注意:JOIN 查询总是更高效,可以作为 map() 的一部分进行重新查询。