Paging3加载网络数据指南

Paging3简介

Paging是一个分页库,是Google定义并实现的一套加载数据并显示的方案。从官方库中可以看到其重新定义了一个PagingDataAdapter,并以次为入口实现了网络加载和UI展示相关的逻辑。这些在原本的RecyclerView+Adapter模式中均是由业务开发者实现的,现在Google帮我们实现了,大大节约了开发成本,并且质量也有一定保证。

主要优点

1.在内存中缓存分页数据,确保您的 App 在使用分页数据时有效地使用系统资源。
2.提供数据去重功能,可以减少数据冗余并节约系统资源。
3.提供数据预取功能,保证下拉刷新流畅。
4.提供加载数据时加载样式header和footer的自定义功能。
5.提供加载错误以及重试的功能支持。
6.支持 Kotlin 协程和 Flow, 以及 LiveData 和 RxJava

注意事项

1.PagingConfig中maxSize有最小值限制,设置maxSize后只会缓存maxSize个数据,因此可能带来历史数据擦除问题,导致回显历史数据异常。
2.Paging必须限制服务端数据提供用于区分页码的唯一标示。
3.Paging从PagingDataAdapter的getItemCount得到的值有可能并不是真正的数据量,因为其中包含了PlaceHolder的个数。
4.Paging中RemoteMediator方案是先获取网络数据然后再缓存到Room数据库,UI展示的数据均从数据库中获取,这样相当于数据获取两次才到达UI展示这一步,是否一定要这样实现?
5.Paging中RemoteMediator方案中下拉一定页数后,新的一批数据加载时回展示在首部,历史数据也从UI中消失,效果类似一下刷新操作的结果。这样产品是否会接受?

配置依赖

在相应模块的build.gradle文件下配置:

	implementation deps.paging_runtime
    implementation deps.retrofit.gson
    implementation deps.okhttp_logging_interceptor
    implementation deps.activity.activity_ktx

相应的版本配置文件为:

def versions = [:]
versions.paging = "3.0.0-alpha01"
versions.retrofit = "2.9.0"
versions.glide = "4.8.0"
versions.okhttp_logging_interceptor = "3.9.0"
versions.lifecycle = "2.2.0"
versions.activity = '1.1.0'
versions.kotlin = "1.3.72"

def deps = [:]
deps.paging_runtime = "androidx.paging:paging-runtime:$versions.paging"
ext.deps = deps

def retrofit = [:]
retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit"
deps.retrofit = retrofit

def glide = [:]
glide.runtime = "com.github.bumptech.glide:glide:$versions.glide"
glide.compiler = "com.github.bumptech.glide:compiler:$versions.glide"
deps.glide = glide

deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:${versions.okhttp_logging_interceptor}"

def activity = [:]
activity.activity_ktx = "androidx.activity:activity-ktx:$versions.activity"
deps.activity = activity

网络仓库

在Paging中要求一个网络仓库主要包含一个Pager对象,它是Paging的主要入口点,它构建了一条可交互的PagingData流,Pager源码很简单,看一下:

class Pager<Key : Any, Value : Any>
@JvmOverloads constructor(
    config: PagingConfig,
    initialKey: Key? = null,
    @OptIn(ExperimentalPagingApi::class)
    remoteMediator: RemoteMediator<Key, Value>? = null,
    pagingSourceFactory: () -> PagingSource<Key, Value>
) {
    /**
     * A cold [Flow] of [PagingData], which emits new instances of [PagingData] once they become
     * invalidated by [PagingSource.invalidate] or calls to [AsyncPagingDataDiffer.refresh] or
     * [PagingDataAdapter.refresh].
     */
    val flow: Flow<PagingData<Value>> = PageFetcher(
        pagingSourceFactory,
        initialKey,
        config,
        remoteMediator
    ).flow
}

即构造一个PageFetcher对象,并将其的流对象flow赋值给自己的flow成员。Pager主要包含两个对象:接口参数PagingConfig、数据源PagingSource。下面是网络仓库代码:

class InMemoryByPageKeyRepository(private val redditApi: RedditApi) : RedditPostRepository {
    override fun postsOfSubreddit(subReddit: String, pageSize: Int) = Pager(
        PagingConfig(pageSize)
    ) {
        PageKeyedSubredditPagingSource(
            redditApi = redditApi,
            subredditName = subReddit
        )
    }.flow
}

1.PagingConfig

PagingConfig是用于描述怎么样从PagingSource加载内容的,包括页大小(必须)、初始页大小、预取距离、placeholder开关等,其中的字段如图:
在这里插入图片描述

2.PagingSource

PagingSource的结构图如图所示:
在这里插入图片描述
其中最主要的是一个虚方法onLoad()、LoadParams内部类、LoadResult内部类,onLoad()方法是提供给用户重写用于从网络获取数据的,其参数是LoadParams类型,返回结果是LoadResult类型。LoadParams的结构如图:
在这里插入图片描述
其中成员变量key是用于区分不同页的标示,loadSize、placeholderEnabled与上文PagingConfig相对应。其中的三个继承LoadParams的内部类Append、Refresh、Prepend分别对应后项加载、刷新、前向加载三种访问网络的类型。下面看一下LoadResult的结构图:
在这里插入图片描述
LoadResult中有两个内部类,分别继承LoadResult,Error标示获取网络数据失败,Page标示获取网络数据成功。Page中data即为加载成功的数据列表,preKey标示上一页的key,nextKey表示下一页的key,在触发加载更多的时候nextKey又会重新被封装进一个LoadParams的key字段中,告诉服务端需要拉取的页码。源码doLoad()方法中可以看到这一过程:

    private suspend fun doLoad(
        scope: CoroutineScope,
        state: PagerState<Key, Value>,
        loadType: LoadType,
        generationalHint: GenerationalViewportHint
    ) {
        require(loadType != REFRESH) { "Use doInitialLoad for LoadType == REFRESH" }

        var loadKey: Key? = stateLock.withLock {
            with(state) {
                generationalHint.hint.withCoercedHint { indexInPage, pageIndex, hintOffset ->
                    nextLoadKeyOrNull(
                        loadType,
                        generationalHint.generationId,
                        indexInPage,
                        pageIndex,
                        hintOffset
                    )?.also { setLoading(loadType, false) }
                }
            }
        }
   }

将数据流注入适配器PagingDataAdapter

通过上面几步已经构造了一个Pager对象,并可以拿到其流对象flow,下面在ViewModel中进一步使用该流对象:


class PagingViewModel(
    private val repository: RedditPostRepository,
    private val saveHandler: SavedStateHandle
) : ViewModel() {

    init {
        if (!saveHandler.contains(KEY_SUBREDDIT)) {
            Log.d(TAG, "set saveHandler")
            saveHandler.set(KEY_SUBREDDIT, DEFAULT_SUBREDDIT)
        }
    }

    val posts =
        saveHandler.getLiveData<String>(KEY_SUBREDDIT)
            .asFlow()
            .flatMapLatest {
                Log.d(TAG, "saveHandler: $it")
                repository.postsOfSubreddit(it, 30)
            }
}

SavedStateHandle对象是ViewModel提供的一个以key-value形式的持久化数据的对象,可以通过AbstractSavedStateViewModelFactory的creat()函数传入,这里用于用于存储搜索的关键字。这里获取了key为KEY_SUBREDDIT的搜索关键字的LiveData形式的结果,并且将其转换成流对象,最后将搜索的关键字流对象与上述的flow合成一个新的流对象posts,下面需要观察流对象并将其显示在RecyclerView上面。只要将该流对象赋值给适配器,就可以将数据显示在UI上。Paging专门提供了PagingDataAdapter类来配合 PagingData 使用,下面是在Activity中给Adapte注入数据的代码:

    private fun initAdapter() {
        val list = findViewById<RecyclerView>(R.id.list)
        val pagingAdapter = PagingAdapter()
        list.adapter = pagingAdapter

        lifecycleScope.launchWhenCreated {
            Log.d(TAG, "dataRefreshFlow scope")
            @OptIn(androidx.paging.ExperimentalPagingApi::class)
            pagingAdapter.dataRefreshFlow.collect {
                Log.d(TAG, "dataRefreshFlow collect: $it")
                list.scrollToPosition(0)
            }
        }

        lifecycleScope.launchWhenCreated {
            Log.d(TAG, "loadStateFlow scope")
            pagingAdapter.loadStateFlow.collect {
                Log.d(TAG, "loadStateFlow collect: $it")
//                swipe_refresh.isRefreshing = it.refresh == LoadState.Loading
            }
        }

        lifecycleScope.launchWhenCreated {
            Log.d(TAG, "posts scope")
            //观察流对象,并将数据提交给Adapter
            mViewModel.posts.collect {
                Log.d(TAG, "posts collect: $it")
                pagingAdapter.submitData(it)
            }
        }
    }

PagingAdapter的实际类型是PagingDataAdapter,其代码很简单:

class PagingAdapter :
    PagingDataAdapter<RedditPost, RedditPostViewHolder>(DIFFER_COMPARATOR) {

    override fun onBindViewHolder(holder: RedditPostViewHolder, position: Int) {
        holder.bind(getItem(position), holder.itemView.context)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RedditPostViewHolder {
        return RedditPostViewHolder.create(parent)
    }

    companion object {
        val DIFFER_COMPARATOR = object : DiffUtil.ItemCallback<RedditPost>() {
            override fun areItemsTheSame(oldItem: RedditPost, newItem: RedditPost): Boolean {
                return oldItem.name == newItem.name
            }

            override fun areContentsTheSame(oldItem: RedditPost, newItem: RedditPost): Boolean {
                return oldItem == newItem
            }

        }
    }

}

只需这样简单的几步,即可将网络数据展示在 RecyclerView 上,并且自带预加载功能。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值