Jetpack 源码分析(六) - Paging3源码分析(下)

  本篇是Paging3源码分析的下篇,将重点介绍RemoteMediator的实现原理。网络上有很多的文章介绍这个多级数据源工具类,但是多多少少有点问题,一般都没有彻底理解清楚RemoteMediator整个过请求的流程。本文将从源码角度解析RemoteMediator的实现原理,同时也会分享RemoteMediator的一些小建议。
  本文内容续接上篇内容,建议先看一下上篇文章:Jetpack 源码分析(五) - Paging3源码分析(上)
  本文主要内容如下:

  1. 多级数据源的请求过程。
  2. 分别分析RemoteMediatorPagingSource的实现细节。
  3. Refresh操作,Prepend操作,Append操作在多级数据源和单一数据源中的不同。
  4. 关于RemoteMediator使用的一些小建议。

  本文参考资料:

  1. Page from network and database
  2. 使用 Paging 3 实现分页加载
  3. Android Jetpack组件之数据库Room详解(三)

  注意,本文Paging源码均来自于3.0.0-alpha08版本。

1. RemoteMediator的请求过程

  相比于单一数据源,RemoteMediator多了一个过程--从网络上获取数据放到数据库中。那么在多级数据源中,怎么将数据库中的数据拿到UI层去显示呢?这个就要说到PagingSource
  在这之前,我先对PaingSourceRemoteMediator做一个解释,方便大家理解,因为它俩的工作是不一样的。

  1. PaingSource:用于获取UI层需要的数据,且只能从一个地方获取,这也就是所谓的单一数据源。UI层需要的数据都是通过该类来获取的,包括在多级数据源里面,PaingSource负责从数据库里面获取数据。
  2. RemoteMediator:主要的作用也是获取数据,只是它是从网络上(或者其他地方)获取数据,然后放到本地数据库,供PaingSource从本地数据库中获取数据。需要特别注意的是,RemoteMediator的数据不会直接用于UI显示,而是保存在数据库中。

  同时,我画了一张图来帮助大家来理解这两个类是如何配合工作的(官网的流程图很容易误解,以为RemoteMediator获取的数据也会用于Ui层显示)。

  整个请求过程如下:

首先,当是第一次请求时,RemoteMediator会进行一次刷新操作,此时会请求到第一批数据,同时会将这这批数据放到本地数据库里面,此时对应的PagingSource对应的从数据库加载数据(需要注意的是,RemoteMediatorPagingSource是搭配使用的)。PagingSource的请求过程跟普通的请求类似,这个我们已经在上篇文章介绍过了,有兴趣的同学可以看看:Jetpack 源码分析(五) - Paging3源码分析(上)。最终会通过发送PageEvent将PagingSource获取的数据传递给Ui层。
不过在这个过程中,我们有两个问题:

  1. 当PagingSource发现数据不够了,怎么通知RemoteMediator继续请求。
  2. 除了第一个页面数据的加载,RemoteMediator是怎么加载其他页面的数据,以及加载完成之后怎么通知PagingSource获取的呢?
    对于这两个问题,这里就不展开分析。下面我们将重点分析这两个问题和其他很多的问题,比如,之前介绍PagPresenter的时候,说它内部存储了所有的数据,在RemoteMediator中就不成立;以及,即使不手动刷新,PagingSource也会进行Refresh操作。

  那么多级数据源是怎么从数据库里面获取的数据呢?我们在ViewModel内部定义Flow时,直接从Dao里面获取一个PagingSource 对象,并不是我们自己定义的。这个PagingSource实际上是一个LegacyPagingSource,是Paging3框架内部的一个 实现。从数据库获取数据的整个过程,主要是通过LegacyPagingSource的load方法,调到了LimitOffsetDataSource里面去了(注意这里是DataSource,对的,就是Paging2里面的DataSource),LimitOffsetDataSourcePositionalDataSource的子类,内部处理了从数据库获取数据的操作。也就是说,LegacyPagingSource其实只是一个Wrapper,用来抹平Paging2和Paging3之间的差异。

2. RemoteMediator的触发

  我们知道RemoteMediator请求是通过load方法进行,那么哪里在调用这个方法呢?在PageFetcherPageFetcherSnapshot内部并没有直接调用调用RemoteMediator的方法,而是通过RemoteMediatorAccessor来辅助调用的。RemoteMediatorAccessor内部封装了很多RemoteMediator的调用逻辑,包括首次加载和加载更多,主要是通过内部的launchRefreshlaunchBoundary完成对RemoteMediator的调用。同时关于RemoteMediatorAccessor,我们还需要注意一点,该对象只会在首次刷新创建一次,这一点跟PageFetcherSnapshot有很大的不同,之所以要这样做,是因为RemoteMediatorAccessor有很多全局的状态,不能因为Refresh而丢失了。
  RemoteMediator的触发请求主要分为两种:Refresh和Append(Prepend)。我们分开来看一下他们的细节。

(1). Refresh

  在PageFetcherSnapshot内部有一个Flow对象--pageEventFlow,这个对象初始化的时候,定义几段代码,用来实现RemoteMediator触发Refresh请求,主要代码如下:

        if (triggerRemoteRefresh) {
            remoteMediatorConnection?.let {
                val pagingState = stateLock.withLock { state.currentPagingState(null) }
                it.requestLoad(REFRESH, pagingState)
            }
        }

  这段代码非常的简单,但是内部蕴含的信息可不少,主要有三点:

  1. 首先,判断是否triggerRemoteRefresh是否为true,为true进行Refresh操作。为啥要判断这个变量呢?因为这段代码会调用的话,表示在进行Refresh操作,但是不代表RemoteMediator必须要刷新数据(RemoteMediator刷新数据时,需要将数据库中旧数据清除掉。)。在单一数据源中,只要不手动Refresh,可能永远不会有第二次Refrsh操作进行(这里只是说的可能,因为不能保证100%,PagingConfig里面的jumpThreshold字段会打破这个规则),但是在RemoteMediator中,如果本地数据库中的数据不够了,PagingSource可能会触发多次Refresh(正常滑动触发的),所以上述的代码可能会调用多次。因此需要通过triggerRemoteRefresh来过滤条件。同时从另一个方面来看,其他地方可以手动的调用PagingSource的invalidate和Adapter的refresh方法来触发刷新,那么这两个方法有啥区别:
     (1). invalidate只是表示当前PagingSource失效了,会重新创建的创建一个新的PagingSource,这个过程不会影响原来的已有的数据。这个方法一般不允许外部手动调用。
     (2). refresh表示需要所有的数据清空,重新进行请求。比如说,我们进行了下拉刷新,此时就会调用这个方法。
    同时,我们从两个方法实现也能看出来区别,refreshrefreshChannel传的是true,即triggerRemoteRefresh为true;invalidate方法传的是false,即triggerRemoteRefresh为false。为了理解清晰,介绍简单,我将refresh触发的刷新称之为完全刷新invalidate触发的刷新称之为不完全刷新,下述内容统一用这个来表示。
  2. PageFetcherSnapshotState内部的PagingState设置为null,这一步主要是为了辅助完全刷新。在Paging刷新过程中,会获取Refresh key,用来判断加载哪部分的数据;如果这个key为空,表示是完全刷新,如果是不为空,那么表示是不完全刷新,这部分的代码在LegacyPagingSourcegetRefreshKey方法里面,有兴趣的同学可以看看。
  3. 调用RemoteMediatorConnectionrequestLoad方法,进行刷新的数据请求。requestLoad方法非常的重要,因为RemoteMediator在触发网络请求时,都是通过这个方法实现的。

  接下来,我们来分析一下requestLoad方法,直接来看代码:

    override fun requestLoad(loadType: LoadType, pagingState: PagingState<Key, Value>) {
        // 1. 往任务队列中添加一个任务。
        val newRequest = accessorState.use {
            it.add(loadType, pagingState)
        }
        // 进行网络请求。
        if (newRequest) {
            when (loadType) {
                LoadType.REFRESH -> launchRefresh()
                else -> launchBoundary()
            }
        }
    }

  requestLoad方法内部主要是做了两件事:

  1. 通过add方法往任务队列里面添加一个任务。在RemoteMediatorAccessImpl内部,维护了一个pendingRequests队列,里面存储着三种LoadType的任务。在添加的时候主要是check两件事:首先判断当前任务队列中是否已经有对应LoadType的任务;其次,当前任务是否处于未锁定的状态。只有这两个条件同时满足,才能添加成功,也才能进行第二步操作。
  2. 调用launchRefresh方法进行网络请求。

  我们来看一下launchRefresh:

    private fun launchRefresh() {
        scope.launch {
            var launchAppendPrepend = false
            isolationRunner.runInIsolation(
                priority = PRIORITY_REFRESH
            ) {
                val pendingPagingState = accessorState.use {
                    it.getPendingRefresh()
                }
                pendingPagingState?.let {
                    // 调用RemoteMediator的load方法,进行网络请求。
                    val loadResult = remoteMediator.load(LoadType.REFRESH, pendingPagingState)
                    launchAppendPrepend = when (loadResult) {
                        is MediatorResult.Success -> {
                            // 更新状态,并且从队列中移除相关任务。
                            accessorState.use {
                                it.clearPendingRequests()
                                it.setBlockState(LoadType.APPEND, UNBLOCKED)
                                it.setBlockState(LoadType.PREPEND, UNBLOCKED)
                                it.setError(LoadType.APPEND, null)
                                it.setError(LoadType.PREPEND, null)
                            }
                            false
                        }
                        is MediatorResult.Error -> {
                            // 如果请求失败,那么看看队列中是否Append或者Prepend的任务,如果有的话,那么就
                            // 执行。
                            accessorState.use {
                                it.clearPendingRequest(LoadType.REFRESH)
                                it.setError(LoadType.REFRESH, LoadState.Error(loadResult.throwable))
                                it.getPendingBoundary() != null
                            }
                        }
                    }
                }
            }
            if (launchAppendPrepend) {
                launchBoundary()
            }
        }
    }

  在launchRefresh方法里面主要是做了两件事:

  1. 调用RemoteMediator的load方法。因为load方法是自己定义,所以做了啥事,我们都很清楚,这里就不展开了。
  2. 其次,就是更新对应的状态。如果请求成功的话,那么会把APPENDPREPEND释放,保证后面可以正常进行操作;如果是请求失败的话,除了更新状态之外,还通过调用getPendingBoundary判断当前任务队列是否有AppendPrepend的任务,如果有的话,就会调用进行请求,即调用launchBoundary

(2). Append

  为了简单起见,这里只看Append的场景,Prepend跟Append比较类似,这里就不赘述了。
  Refresh的触发过程,我们已经理解了,接下来我们来看一下Append的触发过程。Append是怎么触发的呢?用一句话来总结,就是当PagingSource加载完成数据后,根据请求回来的数据(包括PagingSource的Refresh和Append两种方式)来判断是否需要触发RemoteMediator的Append操作。比如下述代码:

                if (remoteMediatorConnection != null) {
                    if (result.prevKey == null || result.nextKey == null) {
                        val pagingState =
                            stateLock.withLock { state.currentPagingState(lastHint) }

                        if (result.prevKey == null) {
                            remoteMediatorConnection.requestLoad(PREPEND, pagingState)
                        }

                        if (result.nextKey == null) {
                            remoteMediatorConnection.requestLoad(APPEND, pagingState)
                        }
                    }
                }

  PagingSource的Refresh和Append关于是否进行RemoteMediator的Append操作的判断条件非常相似,就是看请求回来的数据的nextKey(prevKey)是否为空,如果为空就表示需要进行RemoteMediator的Append操作,即需要从网络拉取新的数据。那么nextKey为空表示的是什么意思呢?

总的来说,nextKey为空就表示当前数据已经加载到数据库种的已有数据的边界,此时就必须要从网络网络上加载下一页数据了,否则的话,用户马上就要滑不动了。那么为啥nextKey就表示已经到了数据边界呢?在Paging2里面,数据请求有一个概念,就是totalCount,如果最后一个数据项的位置等于这个totalCount,那么表示已经到了边界,此时nextKey就会为空。
这里的nextKey涉及到了PagingSource的加载过程,以及LegacyPagingSourceLimitOffsetDataSource的加载,我们先不展开分析,后续有内容会重点分析这个。

  Append的触发最终也调用到了RemoteMediatorConnectionrequestLoad方法里面,我们之前已经看过这个方法了,这里直接看RemoteMediatorAccessorlaunchBoundary方法:

    private fun launchBoundary() {
        scope.launch {
            isolationRunner.runInIsolation(
                priority = PRIORITY_APPEND_PREPEND
            ) {
                while (true) {
                    val (loadType, pendingPagingState) = accessorState.use {
                        it.getPendingBoundary()
                    } ?: break
                    when (val loadResult = remoteMediator.load(loadType, pendingPagingState)) {
                        is MediatorResult.Success -> {
                            accessorState.use {
                                it.clearPendingRequest(loadType)
                                if (loadResult.endOfPaginationReached) {
                                    it.setBlockState(loadType, COMPLETED)
                                }
                            }
                        }
                        is MediatorResult.Error -> {
                            accessorState.use {
                                it.clearPendingRequest(loadType)
                                it.setError(loadType, LoadState.Error(loadResult.throwable))
                            }
                        }
                    }
                }
            }
        }
    }

  launchBoundary做的事非常的简单,就是调用RemoteMediatorload方法,请求下一批数据到数据库中。操作跟Refresh基本类似,这里就不再分析了。

  到此,关于RemoteMediator触发过程的内容就结束了,在这里,我们对此做一个小小的总结,以便大家脑海中有一个印象。

Paging3内部的刷新可以分为两种:完全刷新(调用PageFetcherrefresh方法)和不完全刷新(调用PageFetcherinvalidate方法)。这两种刷新不同点在于,完全刷新时,RemoteMediator会清空已有的数据,重新请求数据;而不完全刷新则不会。这个主要通过PageFetcherSnapshottriggerRemoteRefresh来控制的。RemoteMediator的刷新主要是通过RemoteMediatorConnection的requestLoad方法触发,其方法内部调用launchRefresh方法,进而调用了RemoteMediator的load方法。需要特别注意的是,requestLoad方法是外部(PageFetcherSnapshot)触发RemoteMediator加载网络数据唯一途径。
RemoteMediator加载更多的触发是在PagingSource请求完成之后才进行的,当发现已经到了数据边界,此时通过requestLoad方法加载下一页的数据。RemoteMediatorConnection内部通过launchBoundary方法触发RemoteMediator的load方法。关于数据边界,主要是通过nextKey是为空来判断的,这个涉及到PagingSource的加载过程,我们马上会分析。

3. PagingSource和DataSource的加载

  前面已经说过了,在多级数据源中,PagingSource是LegacyPagingSource,DataSource是LimitOffsetDataSource。其中LegacyPagingSource只起到了一个桥梁作用,保证在Paging3里面能使用Paging2的DataSource。
  我们直接来看LegacyPagingSourceload方法:

    override suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value> {
        val type = when (params) {
            is LoadParams.Refresh -> REFRESH
            is LoadParams.Append -> APPEND
            is LoadParams.Prepend -> PREPEND
        }
        val dataSourceParams = Params(
            type,
            params.key,
            params.loadSize,
            params.placeholdersEnabled,
            @Suppress("DEPRECATION")
            params.pageSize
        )

        return withContext(fetchDispatcher) {
            dataSource.load(dataSourceParams).run {
                LoadResult.Page(
                    data,
                    @Suppress("UNCHECKED_CAST")
                    if (data.isEmpty() && params is LoadParams.Prepend) null else prevKey as Key?,
                    @Suppress("UNCHECKED_CAST")
                    if (data.isEmpty() && params is LoadParams.Append) null else nextKey as Key?,
                    itemsBefore,
                    itemsAfter
                )
            }
        }
    }

  load方法的实现很简单,最终调用了LimitOffsetDataSource的load方法。不过这里有一点我们需要注意:

当请求返回的数据为空,key会为空。数据为空,表示本地数据库没有更多的数据可以加载,也就是说已经加载到边界了,所以需要告诉RemoteMediator从网络上请求更多的数据。
关于这种情况,有一个问题:当执行PagingSource发现没有更多的数据,此时需要从网络上获取数据,那么数据请求回来之后,怎么通知PagingSource来重新加载数据呢?这个就得说说Room的实现,Room在初始化的时候,给我们的表创建了一个触发器,用以监听表的更新,插入和删除三种操作。当有新的数据更新到数据库中去的时候,触发器会发送一个invalidate的通知,这个通知会调用PagingSource的invalidate方法,从而导致PagingSource重新创建和重新加载,此时就是所谓在上下滑动过程也会触发PagingSource的Refresh操作。关于触发器的逻辑,有兴趣的同学可以看看Room的一个类:InvalidationTracker。但是按照正常逻辑来说,PagingSource重建之后,Refresh获取数据应该是全新的,怎么能保证数据前后衔接上呢?这是因为initialKey的存在,因为在scan方法的时候会拿到创建之前的key,如下:

    val flow: Flow<PagingData<Value>> = channelFlow {
        // ......
        refreshChannel.asFlow()
            .onStart {
                // ......
            }
            .scan(null) {
                // ......
                @OptIn(ExperimentalPagingApi::class)
                val initialKey: Key? = previousGeneration?.refreshKeyInfo()
                    ?.let { pagingSource.getRefreshKey(it) }
                    ?: initialKey

                // ......
             }
             // ......
    }

  这里获取initialKey主要是通过PagingSource的getRefreshKey方法。这个方法在之前说不完全刷新时就提到了,感兴趣的同学可以看看(内部主要通过PagingStateanchorPosition来计算key,完全刷新时,anchorPosition要么为0,要么为空,所以不会衔接之前的数据)。
  这里给大家补充了一下额外的知识,我们继续看一下DataSource的loadInitial方法(load方法调用了loadInitial方法,只是在load方法里面有一些计算,这些计算逻辑有兴趣的同学可以自行看看,这里就不讲解了):

       internal suspend fun loadInitial(params: LoadInitialParams) =
        suspendCancellableCoroutine<BaseResult<T>> { cont ->
            loadInitial(
                params,
                object : LoadInitialCallback<T>() {
                    override fun onResult(data: List<T>, position: Int, totalCount: Int) {
                        if (isInvalid) {
                           //......
                        } else {
                            val nextKey = position + data.size
                            resume(
                                params,
                                BaseResult(
                                    data = data,
                                    // skip passing prevKey if nothing else to load
                                    prevKey = if (position == 0) null else position,
                                    // skip passing nextKey if nothing else to load
                                    nextKey = if (nextKey == totalCount) null else nextKey,
                                    itemsBefore = position,
                                    itemsAfter = totalCount - data.size - position
                                )
                            )
                        }
                    }
                    // ......
            )
        }

  loadInitial方法里面主要做了两件事:

  1. 调用另一个loadInitial方法获取数据。这个loadInitial方法就是从数据库里面获取数据,有兴趣的同学可以看看,这里就不展开了。
  2. 根据请求的结果,返回一个BaseResult。这里我们特别注意的是,当nextKey == totalCount时,返回nextKey,这个验证了我们之前的说法。

  至此PagingSource的Refresh加载就结束了,这里我省略Append的过程分析,因为Append过程和Refresh过程非常相似,只不过他们在调用的方法不一样而已。Refresh调用的是loadInitial方法,Append 调用的loadRange方法,其他地方都比较类似的,这里就不过多的分析了。
  在这里,我猜测大家心里面还有疑惑,PagingSource的加载(Refresh 和Append)还是不理解,这两个操作是怎么关联起来的呢?接下来,我将继续给大家解疑答惑。

4. PagingSource的Refresh和Append关联

  在单一数据源中,我们都知道PagingSource一次完整的加载过程包括:一次Refresh + 多次Append + 多次Prepend。但是在多级数据源中却不是这样的,在多级数据源中,PagingSource完整加载过程是:[一次Refresh + 多次Append + 多次Prepend] + [一次Refresh + 多次Append + 多次Prepend]......

注意,在多级数据源中,RemoteMediator的完整加载过程是:一次Refresh + 多次Append + 多次Prepend。PagingSource的完整过程不是这样的,这一点一定要明确。

  上面已经简单的说明了PagingSource完整加载过程,在这里我们详细的解释一下。主要从两个方面来说:

  1. Refresh + Append:当Refresh一次之后,本地会预取一批(不只是一页数据,这里默认为pagSize大小的数据量为一页)的数据,我们通过向下滑动的操作会将预取的数据一页一页(即每次取pageSize大小的数据)的Append到UI层。当这次Refresh的数量被消费完毕,即滑动到边界(或者说,预取的数据已经被完全加载到UI层了),此时nextKey为会空,从而再次触发RemoteMediator的网络请求(此时RemoteMediator的loadType是Append)。RemoteMediator请求完成之后,更新到数据库中,触发器会通知PagingSource重新创建并且Refresh(不完全刷新),再开始一轮的Append。同理Prepend也是类似的。
  2. Prepend:首先,这里Prepend说的不是从数据库获取新的数据,而是获取旧的数据。比如说,当前,我们向下滑动到200位置,在向上滑动,会进行Prepend操作(即从数据库获取之前的数据)。为啥会这样呢?因为多级数据源中,PagePresenter不会保存所有的数据(即打破了保存所有数据的规则,前面已经说过),最多会保留initialLoadSize大小的数据。其他的数据都会用占位符来代替,即PagePresenter里面的placeholdersBeforeplaceholdersAfter,当再次需要使用的时候,会重新从数据库中加载并且显示。

  上述第二点,我们从代码找到答案,就拿PositionalDataSourceloadInitial方法来说:

                    override fun onResult(data: List<T>, position: Int, totalCount: Int) {
                        if (isInvalid) {
                            // NOTE: this isInvalid check works around
                            // https://issuetracker.google.com/issues/124511903
                            cont.resume(BaseResult.empty())
                        } else {
                            val nextKey = position + data.size
                            resume(
                                params,
                                BaseResult(
                                    data = data,
                                    // skip passing prevKey if nothing else to load
                                    prevKey = if (position == 0) null else position,
                                    // skip passing nextKey if nothing else to load
                                    nextKey = if (nextKey == totalCount) null else nextKey,
                                    itemsBefore = position,
                                    itemsAfter = totalCount - data.size - position
                                )
                            )
                        }
                    }

  随着我们不断向下滑动,position会变得越来越大,因此itemsBefore就会变得越来越大(即PagePresenterplaceholdersBefore)。但是,我们的有效数据总量不会变大,始终是initialLoadSize这么多。因此,当我们使用RemoteMediator时,不要尝试获取任意位置的数据,因为获取的有可能是空。

5. 使用RemoteMediator的一些小建议

  至此,源码分析我们算是结束了,在这里,我对使用RemoteMediator提一些小建议。

(1). 不要随意的调用Adapter的getItem方法获取任意位置的数据

  因为PagePresenter只会保留initialLoadSize大小的有效数据,其他位置都会用null来填充,所以通过getItem方法获取的数据很有可能为空,容易造成不必要的错误。

(2). 尽量将PageConfig的initialLoadSize设置的大一点

  因为在滑动过程中,PagingSource的Append和Prepend操作消费的是RemoteMediator获取的initialLoadSize大小的数据,将initialLoadSize设置大一点,可以减少RemoteMediator的请求。其次最好将initialLoadSize设置为pageSize整数倍,避免在Append和Prepend时出现断页的情况。

(3). 最好RemoteMediator每次请求的数量量都设置成为一样,且都为initialLoadSize

  对于RemoteMediator来说,每次请求虽然loadType不一样,但是本质都是差不多的,都是PagingSource发现数据不够了,需要新增数据。所以每次请求的数据都一样,能保证逻辑简单且统一。参考代码如下:

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, Message>
    ): MediatorResult {
        val startIndex = when (loadType) {
            LoadType.REFRESH -> 0
            LoadType.PREPEND -> return MediatorResult.Success(true)
            LoadType.APPEND -> {
                val stringBuilder = StringBuilder()
                state.pages.forEach {
                    stringBuilder.append("size = ${it.data.size}, count = ${it.data.count()}\n")
                }
                Log.i("pby123", stringBuilder.toString())
                count += state.config.initialLoadSize
                count
            }
        }
        Log.i("pby123", "CustomRemoteMediator,loadType = $loadType")
        return try {
            val messages = Service.create().getMessage(state.config.initialLoadSize, startIndex)
            DataBaseHelper.dataBase.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    mMessageDao.clearMessage()
                }
                mMessageDao.insertMessage(messages)
            }
            MediatorResult.Success(messages.isEmpty())
        } catch (e: Exception) {
            MediatorResult.Error(e)
        }

    }

(4). RemoteMediator的load方法的PagingState存储的数据不是所有的数据

  PagingState内部存储的数据并不是所有的数据,而是上一次Refresh的数据,不要尝试通过这个变量来计算所有数据的总数。不过,可以通过如下代码计算,但是不能保证100%靠谱,因为itemsBeforeitemsAfter可能是无效值。

        val pages = state.pages
        var totalCount = 0
        if(pages.isNotEmpty()){
            pages.forEach {
                totalCount += it.data.size + it.itemsBefore + it.itemsAfter
            }
        }

6. 总结

  到这里,Paging3源码分析的内容就结束了,我做了一个简单的总结:

  1. 正常情况下,RemoteMediator只会Refresh一次,除非手动Refresh;PagingSource可能会多次Refresh,除了第一个初始化Refresh之外,当RemoteMediator从网络上获取,放到数据库时,PagingSource也会Refresh。
  2. 在Paging3里面,分为两种刷新,分别是不完全刷新,即调用PageFetcherinvalidate方法,在这种情况下,本地数据库的数据不会清空,只会新增数据;完全刷新,即调用PageFetcherrefresh方法,此种刷新会清空本地数据库的数据。
  3. 多级数据源中的PagingSource完整加载过程是:[一次Refresh + 多次Append + 多次Prepend] + [一次Refresh + 多次Append + 多次Prepend]......,这个跟单一数据源不一样。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容