收集分页数据

本指南建立在 分页库概述 的基础上,讨论了如何自定义应用的数据加载解决方案以满足应用的架构需求。

构建可观察的列表

通常,您的 UI 代码会观察一个 LiveData<PagedList> 对象(或者,如果您使用的是 RxJava2,则为 Flowable<PagedList>Observable<PagedList> 对象),该对象位于应用的 ViewModel 中。这个可观察的对象在应用列表数据的呈现和内容之间建立了连接。

为了创建这些可观察的 PagedList 对象之一,请将 DataSource.Factory 的实例传递给 LivePagedListBuilderRxPagedListBuilder 对象。一个 DataSource 对象为单个 PagedList 加载页面。工厂类会创建新的 PagedList 实例,以响应内容更新(例如,数据库表失效和网络刷新)。Room 持久性库 可以为您提供 DataSource.Factory 对象,或者您可以 构建自己的

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

ConcertDao

Kotlin

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

Java

@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

Kotlin

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

val concertList = myConcertDataSource.toLiveData(pageSize = 50)

Java

// 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 中的最后一个可见项目,分页库应尝试提前获取的超出此最后一个项目的项目数。此值应是页面大小的几倍。
  • 占位符存在: 确定 UI 是否显示尚未完成加载的列表项目的占位符。有关使用占位符的优缺点的讨论,请了解如何 在您的 UI 中提供占位符

如果您希望更多地控制分页库何时从您的应用程序数据库加载列表,请将自定义 Executor 对象传递给 LivePagedListBuilder,如以下代码片段所示

ConcertViewModel

Kotlin

val myPagingConfig = Config(
        pageSize = 50,
        prefetchDistance = 150,
        enablePlaceholders = true
)

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

val concertList = myConcertDataSource.toLiveData(
        pagingConfig = myPagingConfig,
        fetchExecutor = myExecutor
)

Java

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 令牌从一次加载传递到后续加载。
  • 如果您需要使用项目 N 中的数据来获取项目 N+1,请使用 ItemKeyedDataSource。例如,如果您正在为讨论应用程序获取线程化评论,则可能需要传递最后一个评论的 ID 来获取下一个评论的内容。
  • 如果您需要从数据存储中您选择的任何位置获取数据页面,请使用 PositionalDataSource。此类支持请求从您选择的任何位置开始的一组数据项。例如,请求可能会返回从位置 1500 开始的 50 个数据项。

通知数据无效时

使用分页库时,由数据层通知应用程序的其他层表格或行何时已过期。为此,从您为应用程序选择的 DataSource 类中调用 invalidate()

构建您自己的数据源

如果您使用自定义本地数据解决方案,或者直接从网络加载数据,您可以实现 DataSource 子类之一。以下代码片段显示了一个根据给定音乐会开始时间进行键控的数据源

Kotlin

class ConcertTimeDataSource() :
        ItemKeyedDataSource<Date, Concert>() {
    override fun getKey(item: Concert) = item.startTime

    override fun loadInitial(
            params: LoadInitialParams<Date>,
            callback: LoadInitialCallback<Concert>) {
        val items = fetchItems(params.requestedInitialKey,
                params.requestedLoadSize)
        callback.onResult(items)
    }

    override fun loadAfter(
            params: LoadParams<Date>,
            callback: LoadCallback<Concert>) {
        val items = fetchItemsAfter(
            date = params.key,
            limit = params.requestedLoadSize)
        callback.onResult(items)
    }
}

Java

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 对象中。以下代码片段显示了如何生成前一个代码片段中定义的自定义数据源的新实例

Kotlin

class ConcertTimeDataSourceFactory :
        DataSource.Factory<Date, Concert>() {
    val sourceLiveData = MutableLiveData<ConcertTimeDataSource>()
    var latestSource: ConcertDataSource?
    override fun create(): DataSource<Date, Concert> {
        latestSource = ConcertTimeDataSource()
        sourceLiveData.postValue(latestSource)
        return latestSource
    }
}

Java

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 时,通常需要用户交互,例如“滑动刷新”,作为使您最近使用的 DataSource 失效的信号。然后,您请求该数据源的新实例。以下代码片段演示了此行为

Kotlin

class ConcertActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        concertTimeViewModel.refreshState.observe(this, Observer {
            // Shows one possible way of triggering a refresh operation.
            swipeRefreshLayout.isRefreshing =
                    it == MyNetworkState.LOADING
        })
        swipeRefreshLayout.setOnRefreshListener {
            concertTimeViewModel.invalidateDataSource()
        }
    }
}

class ConcertTimeViewModel(firstConcertStartTime: Date) : ViewModel() {
    val dataSourceFactory = ConcertTimeDataSourceFactory(firstConcertStartTime)
    val concertList: LiveData<PagedList<Concert>> =
            dataSourceFactory.toLiveData(
                pageSize = 50,
                fetchExecutor = myExecutor
            )

    fun invalidateDataSource() =
            dataSourceFactory.sourceLiveData.value?.invalidate()
}

Java

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();
    }
}

提供数据映射

分页库支持对由 DataSource 加载的项目的基于项目和基于页面的转换。

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

Kotlin

class ConcertViewModel : ViewModel() {
    val concertDescriptions : LiveData<PagedList<String>>
        init {
            val concerts = database.allConcertsFactory()
                    .map { "${it.name} - ${it.date}" }
                    .toLiveData(pageSize = 50)
        }
}

Java

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();
    }
}

如果要在加载项目后包装、转换或准备项目,这将很有用。由于这项工作是在提取执行程序上完成的,因此您可以执行可能昂贵的操作,例如从磁盘读取或查询单独的数据库。

提供反馈

通过以下资源与我们分享您的反馈和想法

问题跟踪器
报告问题,以便我们修复错误。

其他资源

要了解有关分页库的更多信息,请参阅以下资源。

示例

Codelabs

视频