Android架构组件-Paging库的使用

Android架构组件-App架构指南

Android架构组件-WorkManager

Android架构组件- Room数据库的使用

Android架构组件-Lifecycle

Android架构组件-Paging库的使用


版权声明:本文为博主原创文章,欢迎大家转载!

但是转载请标明出处: http://blog.csdn.net/guiying712/article/details/80386338 ,本文出自:【张华洋的博客】


1、Paging库概览

Paging 库可以使我们在应用程序的 RecyclerView 中轻松做到逐渐加载数据。

大多数应用程序都是从包含大量条目的数据源中获取数据,但一次只显示一小部分数据。

Paging 库可帮助我们的应用程序观察并显示该数据的合理子集,这个功能有几个优点:

  • 数据请求会消耗更少的网络带宽和更少的系统资源。网络流量比较宝贵的用户会喜欢这种关注数据的应用程序。

  • 即使在数据更新和刷新期间,应用程序也会继续快速响应用户输入。

如果你的应用程序已经包含了数据分页和显示列表的逻辑,下面的内容提供了如何更新现有应用程序的指导。

依赖Paging库

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

Google正在将未与Android操作系统捆绑的API包重构为新的命名空间 androidx。有关AndroidX重构的更多信息,请查看博文: Hello World, AndroidX

dependencies {
    def paging_version = "2.0.0-alpha1"

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

本指南概述了如何使用 Paging 库来请求和显示用户希望在更经济地消耗系统资源时看到的数据。有关特定App的结构指南,请查看以下相关章节:

注意:无论我们是仅使用设备内部数据库还是从应用程序的后端获取信息,Paging 库都可以帮助我们顺利地在UI列表容器中显示数据。要了解如何根据应用程序数据的位置最佳地使用 Paging 库的详细信息,请查看支持不同的数据结构

Paging结构


Paging Library的关键组件是 PagedList 类,它是一个异步加载应用程序数据块或页面的集合PagedList充当应用程序结构的其他部分之间的中介:

UI

PagedList 类与 PagedListAdapter 一起使用,可以将数据条目加载到 RecyclerView 中。这些类一起工作以在加载数据时获取和显示内容。

要了解更多信息,请查看UI组件和注意事项

数据

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

Paging Library支持各种应用程序体系结构,包括独立数据库和与后端服务器通信的数据库。

有关详细信息,请查看数据组件和注意事项

Paging Library 实现了App架构指南中建议的观察者模式 。特别是,Paging 库的核心组件创建了一个可以被 UI 观察 LiveData< PagedList> 的实例(或等效的基于RxJava2的类),然后,应用程序的UI 就可以在 PagedList 对象生成时呈现内容 ,同时遵循 UI控制器生命周期

支持不同的数据结构


Paging Library 支持应用程序仅从后端服务器获取数据,仅从设备上数据库或两个数据源的组合获取数据的应用程序体系结构。下面将为每种情况提供建议。

Google 提供了用于不同数据结构模式的示例,请参考 GitHub上的PagingWithNetwork 示例。

仅限网络

要显示来自后端服务器的数据,请使最新的 Retrofit API 版本将信息加载到我们自己的自定义DataSource 对象中

注意:Paging Library的 DataSource 对象不提供任何错误处理,因为不同的应用程序以不同的方式处理和显示错误UI。如果发生错误,请按照结果回调,稍后重试该请求。有关此行为的示例,请参考 PagingWithNetwork 示例。

仅数据库

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

网络和数据库

在开始观察数据库之后,我们可以通过使用 PagedList.BoundaryCallback 来监听数据库何时没有数据 ,然后,我们可以从网络中获取更多条目并将其插入数据库。

以下代码段显示了边界回调(boundary callback)的示例用法:

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

处理网络错误


当我们使用网络获取或分页 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 组件替换为 RoomPositionalDataSource,取决于 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 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,则可以创建一个 ObservableFlowable 对象:

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

然后,我们就可以使用以下代码,开始和停止观察数据:

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 的解决方案, ConcertDaoConcertAdapter 的代码是一样的。


2、UI组件和注意事项

接下来将讲解如何在 UI 中向用户展示信息列表,尤其当信息发生变化时。

建立UI与VIewModel的联系

要将 LiveData< PagedList>PagedListAdapter 建立关联,如以下代码段所示:

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

当数据源(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 = 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() 为底层的 PagedList 提供关于它应从 DataSource 中获取哪些 items 的提示 。

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

实现差异回调


上面的示例显示了手动实现 areContentsTheSame(),它比较了相关的对象字段。我们还可以使用基于Java的代码中的 Object.equals() 方法或基于Kotlin的代码中的 == 运算符来比较内容 ,但要确保要比较的对象实现 equals() 方法或使用基于Kotlin 的 data class.。

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

当然我也可以 选择不继承 PagedListAdapter - 例如当我们使用自己提供的适配器时 - 我们仍然可以通过直接使用 AsyncPagedListDiffer 对象来使用 Paging Library 适配器的 diffing 功能 。

在UI中提供占位符


如果想要UI在App完成获取数据之前显示列表,我们可以向用户显示占位符列表项。在 RecyclerView 处理这种情况,通过将列表项本身临时设置为null

注意:Paging 库默认启用此占位符行为。

占位符具有以下好处:

  • 支持滚动条PagedListPagedListAdapter 提供的列表项的数目 。此信息允许适配器绘制一个滚动条,传达列表的完整大小,加载新页面时,滚动条不会跳转,因为列表不会更改大小。

  • 无需加载微调器:由于列表大小已知,因此无需提醒用户正在加载更多项目,占位符本身传达了这些信息。

但是,在添加对占位符的支持之前,请记住以下前提条件:

  • 需要可数数据集:Room持久性库中的 DataSource 实例可以有效地计算其条目。但是,如果你使用的是自定义本地存储解决方案或仅限网络的数据结构,那么确定数据集中包含多少项可能很难甚至无法实现。

  • 需要适配器来考虑卸载的 items:用于准备列表的适配器或呈现机制需要处理空列表项。例如,将数据绑定到 ViewHolder 时,需要提供默认值来表示卸载的数据。

  • 需要相同大小的条目视图大小:如果列表条目的大小可以根据其内容(例如社交网络更新)进行更改,则条目之间的交叉淡化会看起来不太好。强烈建议在这种情况下禁用占位符。


3、数据组件和注意事项

这一节讨论如何自定义App的数据加载解决方案,,以满足应用程序的架构需求。

构造一个可观察的列表

通常,我们的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.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.java

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

LiveData<PagedList<Concert>> concertList = LivePagedListBuilder(myConcertDataSource, 20).build()

自定义分页配置


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

  • 页面大小: 每页中的条目数。

  • 预取距离: 给定应用UI中的最后一个可见项,超出最后一项的条目数,Paging 库应该提前尝试获取。此值应该是页面大小的几倍。

  • 占位符存在: 确定UI是否显示尚未完成加载的列表项的占位符。有关使用占位符的优缺点的讨论,请查看如何在UI中提供占位符

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

EventViewModel.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 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> mSourceLiveData =  new MutableLiveData<>();

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

内容更新时如何工作


在构建可观察 PagedList 对象时,我们需要考虑数据内容是如何更新的。如果我们直接从 Room数据库 加载数据,数据更新时将自动推送到我们应用的 UI 上。

如果我们使用分页的网络API,则通常会与用户交互,例如“滑动刷新”作为一个信号,使当前的 DataSource 无效,并请求一个新的 。如以下代码:

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

提供数据表示之间的映射

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

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

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, 30).build();
    }
}

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

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


Android架构组件-App架构指南

Android架构组件-WorkManager

Android架构组件- Room数据库的使用

Android架构组件-Lifecycle

Android架构组件-Paging库的使用

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页