Android Paging library详解(一)

官方文档翻译

1.概览

Paging 使您的应用程序配合RecyclerView更容易从数据源中高效优雅地加载所需的数据,不会因为数据库数据量大而造成查询时间过长。

在我们的实际项目中有大量的数据,但是我们常常只需要向用户展示一小部分信息。例如,一个应用中可能会有几千个item但是我们常常只需要显示十几个或者几十个。我们处理不当加载数据时可能会造成APP崩溃。如果数据被存储或与远程数据库同步,这也会减慢应用程序的速度,影响用户的体验。

Paging库能够提供给你的app观察和显示这个数据合理的子集。这个功能库有几个优点:

  1. 数据请求占用比较少的网络带宽和系统资源。
  2. 在数据更新和刷新期间,App也能够快速响应用户输入。
    如果你的app已经包含了分页处理的逻辑,我们已经提供了更新现有app的指导。

1.1 库架构

分页库关键组件是PagedList类,该类异步加载你的app需要的数据以及分页数据。这个类为你app的其他架构组件提供了联系。

Data

每个PagingList实例从DataSource为你的App提供最新的数据。数据流从你的app后台或者数据库流向PagingList对象。

分页库支持支持很多app架构,包括独立数据库或者服务器网络数据。

了解更多,请看Data组件以及相关事项。

UI

PagingList类和PagingListAdapter配合使用,加载items数据到RecyclerView中。这些类一起使用来获取和显示已经加载的数据,预加载以及观察数据变化并自动更新。

了解更多,请看UI组件以及相关事项。

分页库实现了观察者模式。实际上,这些组件会创建一个LiveData<PagedList>数据对象(或者Rxjava2基础类的可观察数据对象)你的app UI就可以将这些数据从PagingList中展示,同时具有可控的生命周期。

1.2 支持不同的数据架构

分页库支持三种数据获取:仅从网络获取;仅从本地数据库获取;从网络和数据库同时获取。

相关的示例代码可以参考 “PagingWithNetworkSample” 以及 PagingSample on GitHub

1.2.1 网络获取或者数据库

首先页面加载的数据来自于网络和数据库二者之一。如上所示,使用LiveData将数据加载进UI中。需要指定一个元数据传递DataSource.Factory至LivePagedListBuilder。

image

Figure 2.DataSource提供一个单一的元数据,Factory加载内容。

数据库获取时, 观察一个数据库时,数据库内容发生改变数据库就会pull一个新的PagedList。
RecyclerView加载本地数据,可以使用Room库。通过这种方式,当向数据库中添加、修改、删除数据,这些变化都会被观察到,从而触发UI自动刷新。

网络获取时, 下拉刷新加载网络加载时(后端不发送更新)会pull一个新的PagedList并使旧的无效。
从后台服务器获取,使用同步的Retrofit API加载网络数据到你的DataSource对象中。

注意:分页库的DataSource对象不提供任何错误处理,因为不同app处理和展示错误的方式不同。如果一个错误发生,将错误传递给callback中,尝试后续重新请求。可以查看PagingWithNetworkSample中的示例代码。

以上这两种方式的数据都是异步加载的。

分页加载本地数据库实例 PagingSample

怎么通过实现DataSource.Factory+Retrofit 网络加载处理下拉刷新,网络异常和重试PagingWithNetworkSample

1.2.2 网络和数据库同时获取

实例二,加载本地存储页,它本身会从网络加载附加数据。这通常是为了最小化网络负载并提供更好的低连接性体验——数据库被用作存储在后端的数据的缓存。

如图,LiveData从数据库获取一个连接,然后通过LivePagedListBuilder至BoundaryCallback,最后观察到数据完成的信号。

本地配合网络获取数据

Figure 3.数据库时网络数据的缓存,UI是从数据库中加载,如果数据库中没有再从网络中获取。

然后在回调中请求网络,将这些数据直接储存在数据库中。UI订阅了数据库的更新,因此,新的数据会自动流向正在观察的UI。

怎么通过实现DataSource.Factory+Retrofit 网络加载处理下拉刷新,网络异常,和重试PagingWithNetworkSample

当你已经加载了本地的数据库,你可以通过PagedList.BoundaryCallback监听数据库的边界,从而触发从网络加载更多数据添加到数据库中。如果你已经添加数据库的监听逻辑,那么UI可以实现自动刷新。

如下的代码展示如何使用BoundaryCallback

KOTLIN

class ConcertViewModel {
    fun search(query: String): ConcertSearchResult {
        val boundaryCallback =
                ConcertBoundaryCallback(query, myService, myCache)
        // Use a LiveData object to communicate your network's state back
        // to your app's UI, as in the following example. Note that error
        // handling isn't shown in this snippet.
        // val loadingState: LiveData<MyNetworkState> =
        //        boundaryCallback.loadingState
    }
}

class ConcertBoundaryCallback(
        private val query: String,
        private val service: MyService,
        private val cache: MyLocalCache
) : PagedList.BoundaryCallback<Concert>() {
    // Requests initial data from the network, replacing all content currently
    // in the database.
    override fun onZeroItemsLoaded() {
        requestAndReplaceInitialData(query)
    }

    // Requests additional data from the network, appending the results to the
    // end of the database's existing data.
    override fun onItemAtEndLoaded(itemAtEnd: Concert) {
        requestAndAppendData(query, itemAtEnd.key)
    }
}

JAVA

public class ConcertViewModel {
    public ConcertSearchResult search(String query) {
        ConcertBoundaryCallback boundaryCallback =
                new ConcertBoundaryCallback(query, myService, myCache);
        // Use a LiveData object to communicate your network's state back
        // to your app's UI, as in the following example. Note that error
        // handling isn't shown in this snippet.
        // LiveData<NetworkState> loadingState =
        //      boundaryCallback.getLoadingState();
    }
}

public class ConcertBoundaryCallback
        extends PagedList.BoundaryCallback<Concert> {
    private String mQuery;
    private MyService mService;
    private MyLocalCache mCache;

    public ConcertBoundaryCallback(String query, MyService service,
            MyLocalCache cache) {
        mQuery = query;
        // ...
    }

    // Requests initial data from the network, replacing all content currently
    // in the database.
    @Override
    public void onZeroItemsLoaded() {
        requestAndReplaceInitialData(mQuery);
    }

    // Requests additional data from the network, appending the results to the
    // end of the database's existing data.
    @Override
    public void onItemAtEndLoaded(@NonNull Concert itemAtEnd) {
        requestAndAppendData(mQuery, itemAtEnd.key);
    }
}

1.2.3 处理网络错误

当你使用分页库从网络获取数据并分页数据,不要把网络请求仅看做可用或者不可用,通常很多连接都是 间歇性的或片状的:某个特定的时间可能会返回失败的响应。也可能有的时候网络请求缓慢。你应该对每次的失败请求做优雅的处理。例如,你可以添加一个重试按钮让用户选择重新获取数据当数据刷新功能不可用的时候。如果实在数据分页过程中出错,做好是做重新请求该分页数据。

1.2.4 更新现有App

如果你的app已经从数据库或者网络获取了数据。直接升级为分页数据是有有可能的。该部分将展示如何升级一个已经设计好的app。

1.2.4.1 自定义分页解决

如果你用自定义功能去从你的DataSource中加载少量的数据集,你可以将这个逻辑用PagingList类来替换。PagingList实例提供了通用数据源的连接。这些实例也提供了将数据适配到RecyclerView的Adapter。

1.2.4.2 使用paging作为数据加载

如果你使用内存List作为后台数据源适配到UI中,如果数据项变得越来越大,那么考虑使用PagingList类。PagingList实例可以传递LiveData<PagedList>Observable<List>类型的数据到你的UI中,这样能极大缩短加载时间和内存使用。做好,替换List对象为PagingList对象,这个不会影响你APP的UI结构和数据更新逻辑。

1.2.4.3 使用CursorAdapter加载数据

你的app可能会使用CursorAdapter加载一个cursor数据源到你的ListView中。这种情况下,你通常需要从ListVIew迁移到RecyclerVIew作为你的UI容器,然后替换Cursor组件为Room或者PositionalDataSource,依赖于Cursor实例是否涉及到一个SQLite数据库。

在有些情形下,例如当使用Spinner时,你仅需要提供Adapter。一个数据加载库将数据加载到Adapter中并显示出来。这种情况下,更改你的Adapter的数据为LiveData<PagedList>类型,然后使用ArrayAdapter对象包装这个列表将数据显示到你的UI中。

1.2.4.4 使用AsyncListUtil异步加载数据

如果你正在使用AsyncListUtil对象作为分页加载,那么分页库让你加载数据变得更加简单:

你的数据不需要位置切分。分页库让你直接通过Keys加载网络数据

你的数据大小不确定。使用分页库,你可以加载数据到页里,直到所有数据加载完成。

你可以更容易观察你的数据。将你的数据包装进ViewModel这种可观察数据结构,分页库可以展现这些数据数据。

注意:如果你现有的app是通过SQLite数据库访问数据,请看Room持久库。

1.2.4.5 数据库实例

下面的代码将上面介绍的几个组件串联起来使用。

通过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>> =
            LivePagedListBuilder(
                    concertDao.concertsByDate(), /* page size */ 20).build()
}

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

class ConcertAdapter() :
        PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
    fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
        val 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()
        }
    }

    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): Boolean =
                    oldConcert.id == newConcert.id

            override fun areContentsTheSame(oldConcert: Concert,
                    newConcert: Concert): Boolean =
                    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 mConcertDao;
    public final LiveData<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        mConcertDao = concertDao;
    }

    concertList = new LivePagedListBuilder<>(
            mConcertDao.concertsByDate(), /* page size */ 20).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,你可以创建一个Observable或者Flowable对象:

KOTLN

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: Flowable<PagedList<Concert>> =
            RxPagedListBuilder(concertDao.concertsByDate(), /* page size */ 50)
                    .buildFlowable(BackpressureStrategy.LATEST)
}

JAVA

public class ConcertViewModel extends ViewModel {
    private ConcertDao mConcertDao;
    public final Flowable<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        mConcertDao = concertDao;

        concertList = new RxPagedListBuilder<>(
                mConcertDao.concertsByDate(), /* page size */ 50)
                        .buildFlowable(BackpressureStrategy.LATEST);
    }
}

你可以通过如下代码开始和停止观察数据
KOTLN

class ConcertActivity : AppCompatActivity() {
    private lateinit var 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::class.java!!)
        adapter = ConcertAdapter()
        recyclerView.setAdapter(adapter)
    }

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

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

JAVA

public class ConcertActivity extends AppCompatActivity {
    private ConcertAdapter mAdapter;
    private ConcertViewModel mViewModel;

    private CompositeDisposable mDisposable = new CompositeDisposable();

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

        mViewModel = ViewModelProviders.of(this).get(ConcertViewModel.class);
        mAdapter = new ConcertAdapter();
        recyclerView.setAdapter(mAdapter);
    }

    @Override
    protected void onStart() {
        super.onStart();
        mDisposable.add(mViewModel.concertList.subscribe(
                flowableList -> mAdapter.submitList(flowableList)
        ));
    }

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

基于RxJava2和基于LiveData解决方案,ConcertDao与ConcertAdapter代码结构是一致的。

2.现有分页库

当前实现分页加载的方法有两种:使用CursorAdapter;使用AsyncListUtil

  1. CursorAdapter 可以是ListView很容易的加载数据,但是他是运行在UI线程的,而且Cursor也会在页面上出现无效的现象。
  2. AsyncListUtil允许RecycleView基于position进行分页加载,但是不允许非position进行分页加载,在数据集中强制使用null作为修饰符。

Paging解决了这些问题。此库简化了请求数据的过程。

3.Paging用户指南

3.1 加入工程

在app Module的build.gradle中添加如下依赖

dependencies {
    def paging_version = "1.0.0"

    implementation "android.arch.paging:runtime:$paging_version"

    // alternatively - without Android dependencies for testing
    testImplementation "android.arch.paging:common:$paging_version"

    // optional - RxJava support, currently in release candidate
    implementation "android.arch.paging:rxjava2:1.0.0-rc1"
}

AndroidX(Android Jetpack)

dependencies {
    def paging_version = "2.0.0-beta01"

    implementation "androidx.paging:paging-runtime:$paging_version"

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

    // optional - RxJava support
    implementation "androidx.paging:paging-rxjava2:$paging_version"
}

3.2 UI组件以及相关事项

3.2.1 将UI和View Modle建立联系

你可以将一个 LiveData<PagedList>实例与一个PagedListAdapter联系起来,如下示例代码:

KOTLIN

private val adapter = ConcertAdapter()
private lateinit var viewModel: ConcertViewModel

override fun onCreate(savedInstanceState: Bundle?) {
    viewModel = ViewModelProviders.of(this).get(ConcertViewModel::class.java)
    viewModel.concerts.observe(this, adapter::submitList)
}

JAVA

public class ConcertActivity extends AppCompatActivity {
    private ConcertAdapter mAdapter;
    private ConcertViewModel mViewModel;

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

正如数据源提供了PagingList的新的实例,Activity发送这个对象到adapter中,PagedListAdapter实现了如何更新的算法,可以自动处理分页以及列表的数据差异。因此,你的ViewHolder只需要处理绑定实际的item。

KOTLIN

class ConcertAdapter() :
        PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
    override 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>() {
            // The ID property identifies when items are the same.
            override fun areItemsTheSame(oldItem: Concert, newItem: Concert) =
                    oldItem.id = newItem.id

            // Use the "==" operator to know when an item's content changes.
            // Implement equals(), or write custom data comparison logic here.
            override fun areContentsTheSame(
                    oldItem: Concert, newItem: Concert) = oldItem == newItem
        }
    }
}

JAVA

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 =
            new DiffUtil.ItemCallback<Concert>() {
        // The ID property identifies when items are the same.
        @Override
        public boolean areItemsTheSame(Concert oldItem, Concert newItem) {
            return oldItem.getId() == newItem.getId();
        }

        // Use Object.equals() to know when an item's content changes.
        // Implement equals(), or write custom data comparison logic here.
        @Override
        public boolean areContentsTheSame(Concert oldItem, Concert newItem) {
            return oldItem.equals(newItem);
        }
    };
}

.PagedListAdapter通过PagedList.Callback处理分页加载事件。当用户滚动的的时候,PagedListAdapter调用PagedList.loadAround()给PagingList提供触发事件,PagingList计算出它需要从数据源获取哪些items。

Note: PagedList is content-immutable. This means that, although new content can be loaded into an instance of PagedList, the loaded items themselves cannot change once loaded. As such, if content in a PagedList updates, the PagedListAdapter object receives a completely new PagedList that contains the updated information.

3.2.2 实现 diffing callback

The preceding sample展示了实现areContentsTheSame()接口,通过比较对象域。你也可以通过比较对象实现,确保实现对象的equals()函数。

Diffing using a different adapter type
If you choose not to inherit from PagedListAdapter—such as when you’re using a library that provides its own adapter—you can still use the Paging Library adapter’s diffing functionality by working directly with an AsyncPagedListDiffer object.

用不同的Adapter类型比较Item差异

如果你不选择继承PagingListAdapter-例如你使用了一个自己实现的Adapter库-你仍然可以使用Paging 库中的Adapter的diffing 功能,通过AsyncPagedListDiffer对象实现。

3.2.3 在你的UI中提供占位

在你的app获取到数据之前,如果你想让你的UI显示列表,你可以先向你的用户展示占位列表。RecyclerView可以处理列表的Item被设置为null数据的情形。

注意:默认情况下,placeHolder是开启的。

占位符有以下优点:

1.支持滚动条:PagingList提供了List item的数量给PagedListAdapter.这些信息允许Adapter绘制一个滚动条传达list的尺寸。当新的页数加载后,滚动条不会滚动,因为有的list没有改变尺寸。

2.没有加载Loading提示的必要:因为list尺寸已经知道了,没有必要告诉用户有更多的items正在被加载。placeholders已经传达了这些信息。

在使用占位支持之前,如下先决条件需要知道:

  1. 需要一个数据集大小是确定的:通过Room持久库获取的DataSource可以有效的确定它们的数量。如果你正在使用一个本地存储解决方案或者仅仅通过网路数据架构,那很可能无法确定你的数据集的数量。

  2. 需要Adapter触发未加载数据的加载:Adapter或者已存在的机制用于处理null items的情形。例如,当你bind数据到一个ViewHolder,你需要提供默认值给未加载的数据。

  3. 需要每个item有相同的view大小:如果对于社交网络更新(例如:聊天界面)item的大小依赖于内容。强烈建议你关掉placeholders功能。

3.3 数据组件以及相关事项

3.3.1 构建一个 observable的列表

典型的,你的UI代码观察一个LiveData<PagedList>对象(或者,你正在使用Rxjava2, 那么这个对象可以是mermaid flowchatable<PagedList>或者Observable<PagedList>对象),一个和你的App中ViewModel联系在一起的对象。这个可被观察的对象建立起了list数据中展示与内容之间联系。

为了创建一个observable类型的PagedList对象,传递一个DataSource.Factory的实例给一个LivePagedListBuilder 或者 RxPagedListBuilder对象。一个DataSource对象给一个PagingList加载分页数据。当数据更新的时候,这个工厂类将创建一个PagedList新的实例,例如数据库表更新或者网络刷新。Room库能够提供给你一个DataSource.Factory, 你也可以自己创建一个你自己的DataSource.Factory。

如下的代码片段,展示了如何创建一个新的LiveData<PagedList>在你的ViewModel类中,通过Room的DataSource.Factory类的构造能力:

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

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 = LivePagedListBuilder(
    myConcertDataSource, /* page size */ 20).build()

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 */ 20).build()

观察数据的更新

Paging使用下列类来构造PagedList容器实时更新:

1.LivePageListBuilder,此类是从DataSource.Factory构建LiveData。如果使用Room来管理数据库,Dao可以生成DataSource.Factory,使用PositionnalDataSource,如下:

LiveData<PagedList<Item>> pagedItems =
        LivePagedListBuilder(myDataSource, /* page size */ 50)
                .setFetchExecutor(myNetworkExecutor)
                .build();

2.RXPagedListBuilder,此类支持RXJava。使用此类实现PagedList来构造Flowable和Observable如下所示:

Flowable<PagedList<Item>> pagedItems =
        RxPagedListBuilder(myDataSource, /* page size */ 50)
                .setFetchScheduler(myNetworkScheduler)
                .buildFlowable(BackpressureStrategy.LATEST);

3.3.2 定义你自己的paging配置

Paging配置可以通过如下属性设置完成:
1.Page size:每一页item数量。
2.Prefetch distance:预加载数量,通常是page size整数倍
3.Placeholder presence:是否启用占位功能。

如果你想控制更多paging加载过程,你可以传递一定制化的Executor对象到LivePagedListBuilder中,如下代码片段:

KOTLIN

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

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

val concertList = LivePagedListBuilder(myConcertDataSource, myPagingConfig)
        .setFetchExecutor(myExecutor)
        .build()

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

3.3.3 选择正确的数据源类型

使用DataSource定义你需要提取分页的数据源。根据使用场景使用的不同使用它不同的子类,如下

1.使用PageKeyDataSource,让你加载的页面插入到下一个或者以前的key,例如:例如:你要从网络获取社交媒体的帖子,你就需要通过nextPage加载到后续的加载中。

2.使用ItemKeyDataSource,如果你需要让使用的数据的item从N条增加到N+1条请使用。例如:你需要从嵌套评论中获取一条评论,需要通过一条评论的ID获取下一条评论的内容。

3.PositionalDataSource,如果你需要从事数据存储的任意位置来获取数据页,此类支持你从任意你选择的位置开始请求item的数据集。比如从第1200条返回20条item。

如果使用Room来管理数据,就要使用DataSource.Factory来初始化PositionalDataSource。如下:

@Query("select * from users WHERE age > :age order by name DESC, id ASC")
DataSource.Factory<Integer, User> usersOlderThan(int age);

3.3.4 数据刷新

当你的数据过时,你需要刷新DataSource,并通知其他层更新

注意:app UI可以触发数据重绘,通过使用下拉刷新模型swipe to refresh

3.3.5 构建你自己的数据源

如果你用一个自定义的本地数据解决,或者直接从网络获取,你可以实现一个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.key, 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创建数据源:

KOTLIN

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

JAVA

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

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

3.3.6 考虑内容如何更新

如果你构造了observable PagedList对象,考虑你的内容如何更新。如果你通过Room数据,那么你可以获取推送更新,从而自动更新UI。

如果你通过网络分页api,你需要一个GUI接口,例如,“swipe to refresh”,提供一个刷新的动作重新获取数据源。示例代码如下:

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 concertTimeDataSource = dataSourceFactory.create()
    val concertList: LiveData<PagedList<Concert>> =
            LivePagedListBuilder(dataSourceFactory, 20)
                .setFetchExecutor(myExecutor)
                .build()

    fun invalidateDataSource() = concertTimeDataSource.invalidate()
}

JAVA

public class ConcertActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        // ...
        mViewModel.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() {
                mViewModel.invalidateDataSource();
            }
        });
    }
}

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

    public ConcertTimeViewModel(Date firstConcertStartTime) {
        ConcertTimeDataSourceFactory dataSourceFactory =
                new ConcertTimeDataSourceFactory(firstConcertStartTime);
        mConcertTimeDataSource = dataSourceFactory.create();
        mConcertList = new LivePagedListBuilder<>(dataSourceFactory, 20)
                .setFetchExecutor(myExecutor)
                .build();
    }

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

3.3.7 提供数据表示的映射

分页库支持item-based和page-based的 item数据的转换, 如下代码展示了将concert实体类中name和date组合成一个字符串的功能:

KOTLIN

class ConcertViewModel : ViewModel() {
    val concertDescriptions : LiveData<PagedList<String>>
        init {
            val factory = database.allConcertsFactory()
                    .map { concert ->
                           concert.name + " - " + concert.date
                    }
            concerts = LivePagedListBuilder(factory, /* page size */ 30).build()
        }
    }
}

JAVA

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

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

这个非常有用,如果你想要包装,转换,或者准备items在它们被加载后。因为这些工作实在获取执行器中完成,你可以执行耗时的工作,例如从磁盘读取或者从数据库查询。

3.3.8 PaingList将数据加载进内存

PagedList类加载的数据来自于DataSource。你可以配置一次加载多少数据,可以预先获取多少数据,尽量减少加载数据的时间。此类也可以向其他类提供数据更新的信号,比如:RecycleView.Adapter一样,为RecycleView页允许数据加载。

PagedList数据加载机制:

  1. PagedListAdapter 类实现自RecycleView.Adapter,并且从PagedList中加载数据。例如:当一个新的页面被加载,PagedListAdapter就会发信号通知RecycleView数据已经加载完成,然后RecycleView就是显示和数据量相等的item,并执行适当的动画。

  2. PagedListAdapter对于来自一个PagedList的下一条数据会在后台线程进行计算。(例如:将数据库的新数据更新到PagedList中),调用notifyItem…()方法更新需要的数据列表的内容。然后RecycleView再执行必要的更改。例如:iitem的position的顺序的改变。

3.4 Paging数据加载原理图

Paging的数据流是在后台线程生产,在UI线程中显示。例如:当一条新的item插入到数据库中,DataSource被初始化,LiveData或者Flowable后台线程就会创建一个新的PagedList
image

Figure 1.
Paging在后台线程中完成大部分工作,所以不会给主线程带来负担。

这个新建的PagedList会被发送到UI线程的PagedListAdapter。PagedListAdapter使用DiffUtil在对比现在的表单和新建表单的差异。当对比结束,PagedListAdapter通过调用RecycleView.Adapter.notifyItemInserted()将新的item插入到适当的位置。RecycleView就会知道他需要绑定一个新的item,并将其显示。

官网地址:https://developer.android.google.cn/topic/libraries/architecture/paging

Sunflower:https://github.com/googlesamples/android-sunflower

PagingSample:https://github.com/googlesamples/android-architecture-components/tree/master/PagingSample

PagingWithNetworkSample:https://github.com/googlesamples/android-architecture-components/tree/master/PagingWithNetworkSample

猜你喜欢

转载自blog.csdn.net/u011897062/article/details/82107846