Android 11 开发者预览版现已推出;快来测试并分享您的反馈吧

分页库概览   Android Jetpack 的一部分。

分页库可帮助您一次加载和显示一小块数据。按需载入部分数据会减少网络带宽和系统资源的使用量。

本指南提供了该库的几个概念性示例,并概述了相应的运作方式。要查看该库运作方式的完整示例,请尝试其他资源部分中的 Codelab 和示例。

库架构

本节介绍并展示分页库的主要组件。

PagedList

分页库的关键组件是 PagedList 类,用于加载应用数据块或页面。随着所需数据的增多,系统会将其分页到现有的 PagedList 对象中。如果任何已加载的数据发生更改,会从 LiveData 或基于 RxJava2 的对象向可观察数据存储器发出一个新的 PagedList 实例。随着 PagedList 对象的生成,应用界面会呈现其内容,同时还会考虑界面控件的生命周期

以下代码段展示了如何配置应用的视图模型,以便使用 PagedList 对象的 LiveData 存储器加载和显示数据:

Kotlin

    class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
        val concertList: LiveData<PagedList<Concert>> =
                concertDao.concertsByDate().toLiveData(pageSize = 50)
    }
    

Java

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

数据

每个 PagedList 实例都会从对应的 DataSource 对象加载应用数据的最新快照。数据从您应用的后端或数据库流向 PagedList 对象。

以下示例使用 Room 持久性库来整理应用数据,但如果要通过其他方式存储数据,也可以提供自己的数据源工厂

Kotlin

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

要详细了解如何将数据加载到 PagedList 对象中,请参阅有关如何加载分页数据的指南。

界面

PagedList 类使用 PagedListAdapter 将项加载到 RecyclerView。这些类共同作用,在内容加载时抓取和显示内容,预取不在视线范围内的内容以及针对内容更改添加动画。

要了解详情,请参阅有关如何显示分页列表的指南。

支持不同的数据架构

分页库支持以下数据架构:

  • 仅从后端服务器提供。
  • 仅存储在设备上的数据库中。
  • 使用设备上的数据库作为缓存的其他来源组合。

图 1 展示了这些架构场景中的数据流动情况。对于仅限网络或仅限数据库的解决方案,数据会直接流向应用界面模型。如果您使用的是组合方式,数据会从您的后端服务器流向设备上的数据库,然后流向应用界面模型。每隔一段时间,每个数据流的端点就会耗尽要加载的数据,此时它会从提供数据的组件请求更多数据。例如,当设备上的数据库耗尽数据时,它会从服务器请求更多数据。

数据流图
图 1. 分页库支持的各种架构中的数据流动情况

本节其余内容提供了有关配置各个数据流用例的建议。

仅限网络

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

仅限数据库

设置您的 RecyclerView 以观察本地存储空间,最好使用 Room 持久性库。这样,无论您何时在应用数据库中插入或修改数据,这些更改都会自动反映在显示此数据的 RecyclerView 中。

网络和数据库

在开始观察数据库之后,您可以使用 PagedList.BoundaryCallback 监听数据库中的数据何时耗尽。然后,您可以从网络中获取更多项目并将它们插入到数据库中。如果界面正在观察数据库,则您只需执行此操作即可。

处理网络错误

通过分页库,使用网络对要显示的数据进行抓取或分页时,请务必不要始终将网络视为“可用”或“不可用”,因为许多连接会断断续续或不稳定:

  • 特定服务器可能无法响应网络请求。
  • 设备可能连接到速度较慢或信号较弱的网络。

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

更新现有应用

如果您的应用已经耗尽了数据库或后端来源中的数据,则可以直接升级到分页库提供的功能。本节介绍如何升级采用现有通用设计的应用。

自定义分页解析

如果您使用自定义功能从应用的数据源加载较小的数据子集,则可以将此逻辑替换为 PagedList 类中的逻辑。PagedList 实例提供了与常见数据源的内置连接。这些实例还为应用界面中可能包含的 RecyclerView 对象提供了适配器。

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

如果您使用内存中列表作为界面适配器的后备数据结构,并且列表中的项目数量可能会变得非常大,请考虑使用 PagedList 类观察数据更新。PagedList 实例可以使用 LiveData<PagedList>Observable<List> 向您的应用界面传递数据更新,从而最大限度地缩短加载时间并减少内存用量。在应用中将 List 对象替换成 PagedList 对象会得到更理想的结果,因为后者不需要对应用界面结构或数据更新逻辑进行任何更改。

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

您的应用可能会使用 CursorAdapterCursor 的数据与 ListView 相关联。在这种情况下,您通常需要从 ListView 迁移到 RecyclerView,以后者作为应用的列表界面容器,然后将 Cursor 组件替换为 RoomPositionalDataSource,具体取决于 Cursor 实例是否会访问 SQLite 数据库。

在某些情况下,例如在使用 Spinner 的实例时,您只需提供适配器本身。然后,库将获取加载到该适配器中的数据,并为您显示这些数据。在这类情况下,请将适配器的数据类型更改为 LiveData<PagedList>,然后将此列表封装到 ArrayAdapter 对象中,再尝试让库类扩充界面中的这些项目。

使用 AsyncListUtil 异步加载内容

如果您使用 AsyncListUtil 对象来异步加载和显示信息组,则通过分页库可以更轻松地加载数据:

  • 您的数据无需固定位置。通过分页库,您可以使用网络提供的密钥直接从后端加载数据。
  • 您的数据可能会非常庞大。通过分页库,您可以将数据加载到网页中,直到没有剩余数据为止。
  • 您可以更轻松地观察数据。分页库可以为您呈现应用 ViewModel 存储在可观察数据结构中的数据。

数据库示例

以下代码段显示了将所有部分组合在一起的几种可行方式。

使用 LiveData 观察分页数据

以下代码段显示了完整代码。随着在数据库中添加、移除或更改 concert 事件,RecyclerView 中的内容会自动且高效地更新:

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>
    }

    class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
        val concertList: LiveData<PagedList<Concert>> =
                concertDao.concertsByDate().toLiveData(pageSize = 50)
    }

    class ConcertActivity : AppCompatActivity() {
        public override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val viewModel = ViewModelProviders.of(this)
                    .get<ConcertViewModel>()
            val recyclerView = findViewById(R.id.concert_list)
            val adapter = ConcertAdapter()
            viewModel.livePagedList.observe(this, PagedList(adapter::submitList))
            recyclerView.setAdapter(adapter)
        }
    }

    class ConcertAdapter() :
            PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
        fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
            val concert: Concert? = getItem(position)

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

        companion object {
            private val DIFF_CALLBACK = object :
                    DiffUtil.ItemCallback<Concert>() {
                // Concert details may have changed if reloaded from the database,
                // but ID is fixed.
                override fun areItemsTheSame(oldConcert: Concert,
                        newConcert: Concert) = oldConcert.id == newConcert.id

                override fun areContentsTheSame(oldConcert: Concert,
                        newConcert: Concert) = oldConcert == newConcert
            }
        }
    }
    

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

    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 对象:

Kotlin

    class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
        val concertList: Observable<PagedList<Concert>> =
                concertDao.concertsByDate().toObservable(pageSize = 50)
    }
    

Java

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

然后,您可以使用以下代码段中的代码来开始和停止观察数据:

Kotlin

    class ConcertActivity : AppCompatActivity() {
        private val adapter: ConcertAdapter()
        private lateinit var viewModel: ConcertViewModel

        private val disposable = CompositeDisposable()

        public override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val recyclerView = findViewById(R.id.concert_list)
            viewModel = ViewModelProviders.of(this)
                    .get<ConcertViewModel>()
            recyclerView.setAdapter(adapter)
        }

        override fun onStart() {
            super.onStart()
            disposable.add(viewModel.concertList
                    .subscribe(adapter::submitList)))
        }

        override fun onStop() {
            super.onStop()
            disposable.clear()
        }
    }
    

Java

    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 的解决方案,ConcertDaoConcertAdapter 的代码是相同的,对于基于 LiveData 的解决方案也是如此。

提供反馈

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

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

其他资源

要详细了解分页库,请参阅以下资源。

示例

Codelab

视频