Jetpack系列之 Paging 详解

  在此之前,我一直对Jetpack的Paging感觉到很迷茫,单单一个分页为啥 Android 官方会出一个组件? 在我们眼中,分页不就是添加两个参数pageSize和pageIndex 么?这么简单逻辑Android官方能耍得起什么样的波浪么?带着这个问题,阅读了一些文档,加上自己的理解,然后有了这篇文章。

基本原理

Paging目前来说,是需要和RecyclerView配合使用的,毕竟Android 目前展示列表数据,差不多就是RecyclerView了。首先简单介绍一下Paging的工作原理,
在这里插入图片描述
完成以上步骤,涉及到几个类分别为:

  1. RecyclerView 负责列表展示;
  2. PagedListAdapter RecyclerView的适配器 同时负责通知PagedList何时加载更多数据
  3. PagedList 控制分页加载的逻辑,比如加载的数量,每页的大小,是否显示 item PlaceHolder等等;
  4. DataSource 执行数据获取的逻辑,它本身并不存储数据,获取到数据丢给PagedList存储。

以上的四个核心类理解完成之后,大致流程如下:
RecyclerView对应的Adapter为PagedListAdapter,通知PagedList需要获取数据,此时PagedList通过DataSource真正执行获取数据的逻辑,返回的数据给PagedList,然后PagedList将数据传递给PagedListAdapter,最后在RecyclerView中显示。

Paging支持的三种分页方式

PositionalDataSource

支持从任意位置开始,取多少条数据的方式,类似SQL中 " XX > id limit 100", 又类似 "start=100&count=20" , 意味着从第100的位置开始,向后取20条数据。

PageKeyedDataSource

这是我们最熟悉的模式,即"pageIndex=1&pageSize=20"的模式。使用以”页“的方式请求数据。

ItemKeyedDataSource

”maxId=nextId&count=200“模式,此次请求依赖上一次的的数据。这种请求方式一般在社交评论中用得比较多,只有这一次请求成功了,下一次请求才能依赖本次的某种参数继续请求。

当然,这只是Google设计的三种用得比较多的请求方式,最终需要你选择的是你的服务器适合哪种方式,然后你再去采用这种方式。

分页实践

首先我们定义接口:
按照上面三种类型分别定义三种接口

获取用户的列表,从start开始,获取count个用户信息。

http://api.com/userList?start=0&count=20

获取用户的列表,从pageIndex页开始开始,获取pageSize个用户信息。

http://api.com/userList2?pageIndex=0&pageSize=20

获取用户的列表,从nextId起始点开始,获取pageCount个用户信息

http://api.com/userList3?nextId=0&pageCount=20

大概的返回结果我们也可以定义一下:

{
	"code":200,
	"msg": "success",
	"data": {
		"total":"100",
		"userList" : [
			{	
				"userId":1,
				"userName":"tom",
				"userAge": 20,
				"userAvatar" : "http://api.com/user/avatar.png"
			},
		    {	
				"userId":2,
				"userName":"Jerry",
				"userAge": 18,
				"userAvatar" : "http://api.com/user/avatar2.png"
			}
		]
	}
}

针对三种API,Paging分别提供了三个抽象类:PositionalDataSourcePageKeyedDataSourceItemKeyedDataSource满足以上接口分类。我们一个一个来。先看一下我们的项目目录结构:
在这里插入图片描述
使用到的 dependences为:

    implementation 'com.squareup.retrofit2:retrofit:2.7.2'
    implementation 'com.squareup.retrofit2:converter-gson:2.7.2'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    implementation 'androidx.paging:paging-runtime:2.1.2'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'

首先定义Model类:

public class UserInfo {

    public int userId;
    public String userName;
    public String userAvatar;
}

public class UserModel {

    public int code;
    public String msg;
    public UserList data;

    public static class UserList{
        public int total;
        public List<UserInfo> userList;
    }
}

然后定义我们的Api类:

public interface Api {

    @GET("/userList/")
    Call<UserModel> getUserListByPositional(@Query("start") int start,
                                            @Query("count") int count);

    @GET("/userList2/")
    Call<UserModel> getUserListByPageSize(@Query("pageIndex") int pageIndex,
                                          @Query("pageSize") int pageSize);


    @GET("/userList3/")
    Call<UserModel> getUserListByItemPaged(@Query("nextId") int nextId,
                                           @Query("pageCount") int pageSize);

}

然后定义我们的 RetrofitClient类,都比较简单,贴一下代码:

public class RetrofitClient {

    private static final String BASE_URL = "https://your.host.url";

    private static RetrofitClient instance ;

    private Retrofit mRetrofit ;

    private RetrofitClient() {
        mRetrofit = new Retrofit.Builder().
                baseUrl(BASE_URL).
                addConverterFactory(GsonConverterFactory.create()).
                client(new OkHttpClient()).build();
    }

    public synchronized static RetrofitClient getInstance() {
        if(null == instance) {
            instance = new RetrofitClient();
        }

        return instance;
    }

    public Api getApi() {
        return mRetrofit.create(Api.class);
    }
}

PositionalUserDataSource

针对于 PositionalDataSource,来看我们的代码:

public class PositionalUserDataSource extends PositionalDataSource<UserInfo> {

    public static final int PAGE_SIZE = 20 ;

    /**
     * 首次加载数据
     */
    @Override
    public void loadInitial(@NonNull LoadInitialParams params,
                            @NonNull final LoadInitialCallback<UserInfo> callback) {
        final int startPosition = 0;

        RetrofitClient.
                getInstance().
                getApi().
                getUserListByPositional(startPosition,PAGE_SIZE).
                enqueue(new Callback<UserModel>() {
                    @Override
                    public void onResponse(@NonNull Call<UserModel> call,
                                           @NonNull Response<UserModel> response) {

                        if(response.isSuccessful()) {
                            UserModel body = response.body();
                            if(null != body) {
                                callback.onResult(body.data.userList,startPosition, body.data.total);
                            }
                        }
                    }

                    @Override
                    public void onFailure(@NonNull Call<UserModel> call, @NonNull Throwable t) {

                    }
                });
    }

    /**
     *
     * 第 N 页加载数据
     *
     * @param params
     * @param callback
     */
    @Override
    public void loadRange(@NonNull LoadRangeParams params,
                          @NonNull final LoadRangeCallback<UserInfo> callback) {

        RetrofitClient.
                getInstance().
                getApi().
                getUserListByPositional(params.startPosition,PAGE_SIZE).
                enqueue(new Callback<UserModel>() {
                    @Override
                    public void onResponse(@NonNull Call<UserModel> call, @NonNull Response<UserModel> response) {
                        if(response.isSuccessful() && null != response.body()){
                            callback.onResult(response.body().data.userList);
                        }
                    }

                    @Override
                    public void onFailure(@NonNull Call<UserModel> call, @NonNull Throwable t) {}
                });
    }
}

有两个抽象方法需要我们重写:loadInitialloadRange方法:

其中 loadInitial 代表加载第一页的方法,但是需要注意的是 LoadInitialCallback.onResult方法:
在这里插入图片描述
是要告之请求的List的总长度,如果我们在 PagedList.Config中设置了 setEnablePlaceHolders() 方法为true,那么此处我们就应该设置List的totalCount,否则我们大程序就会报错。

loadRange 方法可以理解为加载下一页的动作,数据加载完成之后,仍然在 LoadInitialCallback.onResult中显示结果。

PagedKeyUserDataSource

同理,PagedKeyUserDataSource继承自抽象类PageKeyedDataSource

public class PagedKeyUserDataSource extends PageKeyedDataSource<Integer, UserInfo> {

    public static final int FIRST_PAGE = 1 ;
    public static final int PAGE_SIZE = 20 ;


    /**
     * 加载第一页数据
     *
     * @param params
     * @param callback
     */
    @Override
    public void loadInitial(@NonNull LoadInitialParams<Integer> params,
                            @NonNull final LoadInitialCallback<Integer, UserInfo> callback) {

        RetrofitClient.getInstance().getApi().
                getUserListByPageSize(FIRST_PAGE,FIRST_PAGE).
                enqueue(new Callback<UserModel>() {
                    @Override
                    public void onResponse(@NonNull Call<UserModel> call,
                                           @NonNull Response<UserModel> response) {

                        if(response.isSuccessful() && null != response.body()){
                            callback.onResult(response.body().data.userList,
                                null,
                               FIRST_PAGE + 1);
                        }
                    }

                    @Override
                    public void onFailure(Call<UserModel> call, Throwable t) {

                    }
                });

    }

    /**
     * 加载下一页的数据
     *
     * @param params
     * @param callback
     */
    @Override
    public void loadAfter(@NonNull final LoadParams<Integer> params,
                          @NonNull final LoadCallback<Integer, UserInfo> callback) {

        RetrofitClient.getInstance().getApi().
                getUserListByPageSize(params.key, PAGE_SIZE).
                enqueue(new Callback<UserModel>() {
                    @Override
                    public void onResponse(@NonNull Call<UserModel> call, @NonNull Response<UserModel> response) {
                        if(response.isSuccessful() && null != response.body()){
                            UserModel.UserList userList = response.body().data;
                            boolean hasMoreData = userList != null && userList.userList.size() >= PAGE_SIZE;

                            callback.onResult(userList.userList, hasMoreData ? params.key + 1 : null);
                        }
                    }

                    @Override
                    public void onFailure(@NonNull Call<UserModel> call, @NonNull Throwable t) {

                    }
                });
    }


    @Override
    public void loadBefore(@NonNull LoadParams<Integer> params,
                           @NonNull LoadCallback<Integer, UserInfo> callback) {

    }

}

同理它也有三个抽象方法需要重写:loadInitialloadAfterloadBefore方法,意义与 PositionalDataSource 中方法意义类型,都是加载第一页和第N页的逻辑,对于新增的 loadBefore代表在加载之前做的事情,个人现在用不到,感觉没什么太大的意义。

ItemKeyedUserDataSource

ItemKeyedUserDataSource也是继承自ItemKeyedDataSource抽象类,代码如下:

public class ItemKeyedUserDataSource extends ItemKeyedDataSource<Integer, UserInfo> {

    public static final int PAGE_SIZE = 20 ;

    @Override
    public void loadInitial(@NonNull LoadInitialParams<Integer> params,
                            @NonNull final LoadInitialCallback<UserInfo> callback) {

        RetrofitClient.
                getInstance().
                getApi().
                getUserListByItemPaged(0, PAGE_SIZE).
                enqueue(new Callback<UserModel>() {
                    @Override
                    public void onResponse(@NonNull Call<UserModel> call, @NonNull Response<UserModel> response) {
                        if(response.isSuccessful() && null != response.body()) {
                            callback.onResult(response.body().data.userList);
                        }
                    }

                    @Override
                    public void onFailure(@NonNull Call<UserModel> call, @NonNull Throwable t) {

                    }
                });
    }

    @Override
    public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull final LoadCallback<UserInfo> callback) {
        RetrofitClient.getInstance().
                getApi().
                getUserListByItemPaged(params.key, PAGE_SIZE).
                enqueue(new Callback<UserModel>() {
                    @Override
                    public void onResponse(@NonNull Call<UserModel> call, @NonNull Response<UserModel> response) {
                        if(response.isSuccessful() && null != response.body()) {
                            callback.onResult(response.body().data.userList);
                        }
                    }

                    @Override
                    public void onFailure(@NonNull Call<UserModel> call, @NonNull Throwable t) {

                    }
                });
    }

    @Override
    public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<UserInfo> callback) {}

    @NonNull
    @Override
    public Integer getKey(@NonNull UserInfo item) {
        return item.userId;
    }

它有四个抽象方法需要重写:loadInitialloadAfterloadBeforegetKey。前三个方法和前面的意义类似,这里就不说了;对于getKey方法,就是我们获取下一页nextId的地方,也比较简单。

对于我们的 三种 DataSource获取完成之后,来看一下我们如果将数据展示在 RecyclerView 上了:

首先定义一个我们的SourceFactory:

public class UserDataSourceFactory extends DataSource.Factory<Integer, UserInfo> {

	// 这里可以根据需求换成另外两种DataSource即可。
    private MutableLiveData<PositionalUserDataSource> liveDataSource = new MutableLiveData<>();

    @NonNull
    @Override
    public DataSource<Integer, UserInfo> create() {
        PositionalUserDataSource source = new PositionalUserDataSource();
        liveDataSource.postValue(source);
        return source;
    }

}

然后定义一下我们的ViewModel

public class UserViewModel extends ViewModel {

    public LiveData<PagedList<UserInfo>> userPagedList;

    public UserViewModel() {
        PagedList.Config config = new PagedList.Config.Builder().
                                    // 用于控件占位
                                    setEnablePlaceholders(true).
                                    // 设置每页的大小
                                    setPageSize(PositionalUserDataSource.PAGE_SIZE).
                                    // 设置当距离底部还有多少条数据时开始加载下一页
                                    setPrefetchDistance(3).
                                    // 设置首次加载数据的数量  默认为 page_size 的三倍
                                    setInitialLoadSizeHint(PositionalUserDataSource.PAGE_SIZE * 3).
                                    // 设置pagedList 所能承受的最大数量
                                    setMaxSize(65536 * PositionalUserDataSource.PAGE_SIZE).
                                    build();

        userPagedList = new LivePagedListBuilder<>(new UserDataSourceFactory(),config).build();
    }

}

设置并定义下PagedList.Config,其中几个比较重要的方法含义已经贴上去了。

最后,贴一下Adapter,一般使用Paging组件的话,都会使用androidx.paging.PagedListAdapter:

public class UserPagedListAdapter extends PagedListAdapter<UserInfo, UserPagedListAdapter.UserItemViewHolder> {

    private Context mContext;

    public UserPagedListAdapter(Context context) {
        super(DIFF_CALLBACK);
        this.mContext = context;
    }

    private static DiffUtil.ItemCallback<UserInfo> DIFF_CALLBACK = new DiffUtil.ItemCallback<UserInfo>() {
        @Override
        public boolean areItemsTheSame(@NonNull UserInfo oldItem, @NonNull UserInfo newItem) {
            return oldItem.userId == newItem.userId;
        }

        @Override
        public boolean areContentsTheSame(@NonNull UserInfo oldItem, @NonNull UserInfo newItem) {
            return oldItem.userName.equals(newItem.userName) &&
                   oldItem.userAvatar.equals(newItem.userAvatar) ;
        }
    };

    @NonNull
    @Override
    public UserItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new UserItemViewHolder(LayoutInflater.from(mContext).
                inflate(R.layout.positional_user_item_layout,parent,false));
    }

    @Override
    public void onBindViewHolder(@NonNull UserItemViewHolder holder, int position) {
        //TODO
    }

   static class UserItemViewHolder extends RecyclerView.ViewHolder {

        public UserItemViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }
}

最后在Activity中:

public class MainActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;

    private UserPagedListAdapter mPositionalAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mRecyclerView = findViewById(R.id.id_recycler_view);

        initPositionalAdapter();

        initPositionalObserve();
    }

    private void initPositionalAdapter() {
        mPositionalAdapter = new UserPagedListAdapter(this);
        mRecyclerView.setAdapter(mPositionalAdapter);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mRecyclerView.setHasFixedSize(true);
    }

    private void initPositionalObserve() {
        UserViewModel viewModel = new ViewModelProvider(this).
                                                get(UserViewModel.class);
        viewModel.userPagedList.observe(this, new Observer<PagedList<UserInfo>>() {
            @Override
            public void onChanged(PagedList<UserInfo> userInfoPagedList) {
                mPositionalAdapter.submitList(userInfoPagedList);
            }
        });
    }
}

基本原理大概就这么多了。

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值