Paging 2 库概览   Android Jetpack 的一部分。

Paging 库可帮助您一次加载和显示小块数据。按需加载部分数据可以减少网络带宽和系统资源的消耗。

本指南提供了一些关于该库的概念性示例,并概述了它的工作原理。要查看该库完整的工作示例,请参阅其他资源部分中的 Codelab 和示例。

设置

要将 Paging 组件导入您的 Android 应用,请将以下依赖项添加到应用的 build.gradle 文件中

Groovy

dependencies {
  def paging_version = "2.1.2"

  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
}

Kotlin

dependencies {
  val paging_version = "2.1.2"

  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 库的主要组件。

PagedList

Paging 库的关键组件是 PagedList 类,该类会加载应用数据的块或。当需要更多数据时,数据会被分页到现有的 PagedList 对象中。如果任何已加载的数据发生变化,就会从基于 LiveData 或 RxJava2 的对象向可观察数据持有者发出新的 PagedList 对象实例。生成 PagedList 对象后,应用界面会呈现其内容,同时还会遵循界面控制器的生命周期

以下代码段显示了如何配置应用 ViewModel 以使用 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 对象中,请参阅关于如何加载分页数据的指南。

UI

PagedList 类与 PagedListAdapter 配合使用,将项目加载到 RecyclerView 中。这些类协同工作,在内容加载时提取和显示内容,预取屏幕外的内容并对内容变化应用动画效果。

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

支持不同的数据架构

Paging 库支持以下数据架构

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

图 1 显示了在每种架构场景中数据如何流动。对于仅使用网络或仅使用数据库的解决方案,数据直接流向您的应用 UI 模型。如果您使用组合方法,数据会从后端服务器流向设备上的数据库,然后再流向应用的 UI 模型。偶尔,每个数据流的端点都会用尽要加载的数据,此时它会向提供数据的组件请求更多数据。例如,当设备上的数据库数据不足时,它会向服务器请求更多数据。

Diagrams of data flows
图 1. Paging 库支持的每种架构中数据如何流动

本节的其余部分提供了配置每种数据流用例的建议。

仅网络

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

仅数据库

将您的 RecyclerView 设置为观察本地存储,最好使用 Room 持久性库。这样一来,只要应用数据库中插入或修改了数据,这些更改就会自动反映在显示这些数据的 RecyclerView 中。

网络和数据库

开始观察数据库后,您可以使用 PagedList.BoundaryCallback 监听数据库何时数据不足。然后,您可以从网络中获取更多项目并将其插入数据库。如果您的 UI 正在观察数据库,那么您只需执行此操作即可。

处理网络错误

使用网络提取或分页要使用 Paging 库显示的数据时,切勿将网络视为始终“可用”或“不可用”,因为许多连接都是间歇性或不稳定的

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

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

更新现有应用

如果您的应用已在使用数据库或后端来源的数据,则可以直接升级到 Paging 库提供的功能。本部分介绍如何升级具有常见现有设计的应用。

自定义分页解决方案

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

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

如果您使用内存中的列表作为 UI 适配器的底层数据结构,并且列表中的项目数量可能会很大,请考虑使用 PagedList 类观察数据更新。PagedList 实例可以使用 LiveData<PagedList>Observable<List> 将数据更新传递给您的应用 UI,从而最大限度地缩短加载时间并减少内存使用量。更好的是,在应用中将 List 对象替换为 PagedList 对象无需更改应用的 UI 结构或数据更新逻辑。

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

您的应用可能使用 CursorAdapter 将来自 Cursor 的数据与 ListView 关联。在这种情况下,您通常需要将列表 UI 容器从 ListView 迁移到 RecyclerView,然后根据 Cursor 实例是否访问 SQLite 数据库,将 Cursor 组件替换为 RoomPositionalDataSource

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

使用 AsyncListUtil 异步加载内容

如果您使用 AsyncListUtil 对象异步加载和显示信息组,Paging 库可以更轻松地加载数据

  • 您的数据不必是定位数据。 Paging 库允许您使用网络提供的键直接从后端加载数据。
  • 您的数据可能非常大。 使用 Paging 库,您可以将数据分页加载,直到没有剩余数据。
  • 您可以更轻松地观察您的数据。 Paging 库可以将应用 ViewModel 持有的数据呈现在可观察数据结构中。

数据库示例

以下代码段展示了让所有部分协同工作的几种可能方式。

使用 LiveData 观察分页数据

以下代码段展示了所有部分如何协同工作。随着演唱会事件在数据库中添加、删除或更改,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)
        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: ConcertViewModel by viewModels()
        val recyclerView = findViewById(R.id.concert_list)
        val adapter = ConcertAdapter()
        viewModel.concertList.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 =
                new ViewModelProvider(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()

    // Use the 'by viewModels()' Kotlin property delegate
    // from the activity-ktx artifact
    private val viewModel: ConcertViewModel by viewModels()

    private val disposable = CompositeDisposable()

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val recyclerView = findViewById(R.id.concert_list)
        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 = new ViewModelProvider(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 的解决方案的代码相同。

提供反馈

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

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

其他资源

如需详细了解 Paging 库,请查阅以下资源。

示例

Codelab

视频