Paging内部原理

下拉刷新内部逻辑

当用户触发下拉刷新时,会调用PagingDataAdapter的reresh()方法,我们就从该方法切入,一步步看看内部的调用逻辑。首先,我们看一下函数时序图:
refresh内部时序
续:
在这里插入图片描述
根据以上的时序图,我们来详细看一下源码的实现逻辑。

1.PagingDataAdapter.reresh()代码逻辑

首先看一下PagingDataAdapter的reresh()方法:

fun refresh() {
        differ.refresh()
    }

可以看到该方法内部调用differ的refresh()方法,differ的初始化为:

private val differ = AsyncPagingDataDiffer(
        diffCallback = diffCallback,
        updateCallback = AdapterListUpdateCallback(this),
        mainDispatcher = mainDispatcher,
        workerDispatcher = workerDispatcher
    )

其中diffCallback,mainDispatcher和workerDispatcher都是从PagingDataAdapter的构造函数传进来的。AsyncPagingDataDiffer的refresh方法为:

fun refresh() {
        differBase.refresh()
    }

继续调用内部differBase的refresh()方法,differBase的类型为PagingDataDiffer,看一下它的初始化:

 private val differBase = object : PagingDataDiffer<T>(mainDispatcher) {
        override suspend fun performDiff(
            previousList: NullPaddedList<T>,
            newList: NullPaddedList<T>,
            newCombinedLoadStates: CombinedLoadStates,
            lastAccessedIndex: Int
        ): Int? {
            return withContext(mainDispatcher) {
                when {
                    previousList.size == 0 -> {
                        // fast path for no items -> some items
                        callback.onInserted(0, newList.size)
                        callback.dispatchLoadStates(newCombinedLoadStates)
                        return@withContext null
                    }
                    newList.size == 0 -> {
                        // fast path for some items -> no items
                        callback.onRemoved(0, previousList.size)
                        callback.dispatchLoadStates(newCombinedLoadStates)
                        return@withContext null
                    }
                    else -> { // full diff
                        val diffResult = withContext(workerDispatcher) {
                            previousList.computeDiff(newList, diffCallback)
                        }

                        previousList.dispatchDiff(updateCallback, newList, diffResult)
                        callback.dispatchLoadStates(newCombinedLoadStates)

                        return@withContext previousList.transformAnchorIndex(
                            diffResult = diffResult,
                            newList = newList,
                            oldPosition = lastAccessedIndex
                        )
                    }
                }
            }
        }

        /**
         * Return if [getItem] is running to post any data modifications.
         *
         * This must be done because RecyclerView can't be modified during an onBind, when
         * [getItem] is generally called.
         */
        override fun postEvents(): Boolean {
            return inGetItem
        }
    }

可以看到,上述代码初始化了一个父类为PagingDataDiffer的匿名内部内对象,其中重写了performDiff()虚方法和postEvents()方法。performDiff()的目的是比较原列表与新列表的区别,其的逻辑有三个分支:

  1. 原来列表的长度为0;
  2. 新列表的长度为0;
  3. 原列表和新列表长度均不为0;

从代码看1和2的逻辑都很简单,3的逻辑相对复杂,大致逻辑是先比较原列表与新列表的区别,并记录在diffResult中,然后再更新原列表。注意一下3中计算原列表与新列表的区别时用到的diffCallback是在App层构造PagingDataAdapter子类对象时传入的,为用户自定义的。下面看一下这两个方法调用的地方,在PagingDataDiffer类中collectFrom()方法调用:

    suspend fun collectFrom(
       pagingData: PagingData<T>,
       callback: PresenterCallback
   ) = collectFromRunner.runInIsolation {
       receiver = pagingData.receiver

       pagingData.flow.collect { event ->
           withContext(mainDispatcher) {
               if (event is PageEvent.Insert && event.loadType == REFRESH) {
                   val newPresenter = PagePresenter(event)
                   val transformedLastAccessedIndex = performDiff(
                       previousList = presenter,
                       newList = newPresenter,
                       newCombinedLoadStates = event.combinedLoadStates,
                       lastAccessedIndex = lastAccessedIndex
                   )
                   presenter = newPresenter

                   // Dispatch ListUpdate as soon as we are done diffing.
                   dataRefreshedListeners.forEach { listener ->
                       listener(event.pages.all { page -> page.data.isEmpty() })
                   }
                   //
                   transformedLastAccessedIndex?.let { newIndex ->
                       lastAccessedIndex = newIndex
                       receiver?.addHint(presenter.loadAround(newIndex))
                   }
               } else {
                   if (postEvents()) {
                       yield()
                   }

                   // Send event to presenter to be shown to the UI.
                   presenter.processEvent(event, callback)
               }
           }
       }
   }

可以看出该函数有两种情况:

  1. 刷新结果返回时出入数据;
  2. 刷新过程中更新UI

这两种情况也对应了performDiff()和postEvents()的调用,因此performDiff()的作用是计算新列表和老列表的区别并更新老列表,postEvents()是标志Adapter正在执行getItem()函数,此时Adapter正在渲染UI,因此不会走下面的更新UI的逻辑。
下面回到differBase.refresh()这段代码,我们看看PagingDataDiffer类内部是怎么定义refresh()方法的:

fun refresh() {
        receiver?.refresh()
    }

其中,receiver的类型为UiReceiver接口,其实现类是定义在PagerFetcher类中的内部类PagerUiReceiver,重写refresh()方法为:

PagerFetcher {
		private val refreshChannel = ConflatedBroadcastChannel<Boolean>()
		fun refresh() {
        		refreshChannel.offer(true)
       }
		PagerUiReceiver {
				override fun refresh() = this@PageFetcher.refresh()
		}
}

可以看到上述代码最终是向refreshChannel添加一个true,refreshChannel的类型为ConflatedBroadcastChannel最终父类是SendChannel,其offer()为:

/**
     * Sends the value to all subscribed receives and stores this value as the most recent state for
     * future subscribers. This implementation always returns `true`.
     * It throws exception if the channel [isClosedForSend] (see [close] for details).
     */
public override fun offer(element: E): Boolean {
		//*offerInternal(element)返回null才是正常情况*
        offerInternal(element)?.let { throw it.sendException }
        return true
    }
private fun offerInternal(element: E): Closed? {
        // If some other thread is updating the state in its offer operation we assume that our offer had linearized
        // before that offer (we lost) and that offer overwrote us and conflated our offer.
        if (!_updating.compareAndSet(0, 1)) return null
        try {
            _state.loop { state ->
                when (state) {
                    is Closed -> return state
                    //正常情况会走这个分支
                    is State<*> -> {
                        val update = State(element, (state as State<E>).subscribers)
                        if (_state.compareAndSet(state, update)) {
                            // Note: Using offerInternal here to ignore the case when this subscriber was
                            // already concurrently closed (assume the close had conflated our offer for this
                            // particular subscriber).
                            state.subscribers?.forEach { it.offerInternal(element) }
                            return null
                        }
                    }
                    else -> error("Invalid state $state")
                }
            }
        } finally {
            _updating.value = 0 // reset the updating flag to zero even when something goes wrong
        }
    }

当执行offer操作时,正常情况_state是State类型的,然后执行该分支代码,先是创建一个名为update的State对象,然后再将_state更新为该对象,若能成功更新则遍历state.subscribers。通过offer()方法的注释可以直达该方法的目的是:发送该值给所有的订阅者,并存储这些值的最新状态给未来的订阅者使用。因此下面看看订阅refreshChannel的地方:

    val flow: Flow<PagingData<Value>> = channelFlow {
        refreshChannel.asFlow()
            .onStart {
                @OptIn(ExperimentalPagingApi::class)
                emit(remoteMediatorAccessor?.initialize() == LAUNCH_INITIAL_REFRESH)
            }
            .scan(null) { previousGeneration: PageFetcherSnapshot<Key, Value>?,
                          triggerRemoteRefresh ->
                val pagingSource = pagingSourceFactory()

                // Ensure pagingSourceFactory produces a new instance of PagingSource.
                check(pagingSource !== previousGeneration?.pagingSource) {
                    """
                    An instance of PagingSource was re-used when Pager expected to create a new
                    instance. Ensure that the pagingSourceFactory passed to Pager always returns a
                    new instance of PagingSource.
                    """.trimIndent()
                }

                @OptIn(ExperimentalPagingApi::class)
                val initialKey = previousGeneration?.refreshKeyInfo()
                    ?.let { pagingSource.getRefreshKey(it) }
                    ?: initialKey

                // Hook up refresh signals from DataSource / PagingSource.
                pagingSource.registerInvalidatedCallback(::invalidate)
                previousGeneration?.pagingSource?.unregisterInvalidatedCallback(::invalidate)
                previousGeneration?.pagingSource?.invalidate() // Note: Invalidate is idempotent.
                previousGeneration?.close()

                PageFetcherSnapshot(
                    initialKey = initialKey,
                    pagingSource = pagingSource,
                    config = config,
                    retryFlow = retryChannel.asFlow(),
                    // Only trigger remote refresh on refresh signals that do not originate from
                    // initialization or PagingSource invalidation.
                    triggerRemoteRefresh = triggerRemoteRefresh,
                    remoteMediatorAccessor = remoteMediatorAccessor,
                    invalidate = this@PageFetcher::refresh
                )
            }
            .filterNotNull()
            .mapLatest { generation ->
                PagingData(generation.pageEventFlow, PagerUiReceiver(generation, retryChannel))
            }
            .collect { send(it) }
    }

首先看一下refreshChannel.asFlow()的实现:

@FlowPreview
public fun <T> BroadcastChannel<T>.asFlow(): Flow<T> = flow {
    emitAll(openSubscription())
}

BroadcastChannel类中的openSubscription()是一个虚方法,我们看一下在子类ConflatedBroadcastChannel类中的实现:

    public override fun openSubscription(): ReceiveChannel<E> {
        val subscriber = Subscriber(this)
        _state.loop { state ->
            when (state) {
                is Closed -> {
                    subscriber.close(state.closeCause)
                    return subscriber
                }
                is State<*> -> {
                    if (state.value !== UNDEFINED)
                        subscriber.offerInternal(state.value as E)
                    val update = State(state.value, addSubscriber((state as State<E>).subscribers, subscriber))
                    if (_state.compareAndSet(state, update))
                        return subscriber
                }
                else -> error("Invalid state $state")
            }
        }
    }

可以看到先初始化了一个类型为Subscriber的对象subscriber,Subscriber继承了ReceiveChannel和ConflatedChannel,然后构造一个类型为State的对象update,并将subscriber添加到State的subscribers列表中去。以上就是 refreshChannel.asFlow()这段代码的逻辑,继续看下面的代码,在onStart中因为remoteMediatorAccessor?.initialize()的初始值为LAUNCH_INITIAL_REFRESH,因此会发射一个true,接着在scan中会构造一个PageFetcherSnapshot对象交给下游处理,在mapLatest中会构造一个PagingData对象并将其发射出去。接下来我们看该流flow的接收的地方,该flow所处的类是DataFetch类,找到初始化DataFetch的地方:

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
}

从上面代码可以看到,Pager中的flow对象即是PagerFetch中的flow对象,通过注释可以知道:该流是类型为PagingData的冷流,每当调用[PagingSource.invalidate]使该流无效或者调用[AsyncPagingDataDiffer.refresh]或[PagingDataAdapter.refresh]时,该流就是发射一个新的对象。回顾一下,我们代码的起点正是PagingDataAdapter.refresh。

2. PagingDataAdapter.submitData()代码逻辑

Pager中的flow流一般会在app层的代码进行监控,如果数据源是网络,则会设计网络库的接口,下面展示获取网络的一种写法:

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

该数据仓库中的postsOfSubreddit()方法中即构造了一个Pager对象,同时还构造了一个PagingConfig对象用于保存用户设置的获取数据的参数和一个PageKeyedSubredditPagingSource对象用于表示数据源,最后将该Pager对象的flow返回给调用者。下面看一下PageKeyedSubredditPagingSource类的定义:

class PageKeyedSubredditPagingSource(
        private val redditApi: RedditApi,
        private val subredditName: String
) : PagingSource<String, RedditPost>() {
    override suspend fun load(params: LoadParams<String>): LoadResult<String, RedditPost> {
        return try {
            val data = redditApi.getTop(
                    subreddit = subredditName,
                    after = if (params is Append) params.key else null,
                    before = if (params is Prepend) params.key else null,
                    limit = params.loadSize
            ).data

            Page(
                    data = data.children.map { it.data },
                    prevKey = data.before,
                    nextKey = data.after
            )
        } catch (e: IOException) {
            LoadResult.Error(e)
        } catch (e: HttpException) {
            LoadResult.Error(e)
        }
    }
}

可以看到该类继承至PagingSource,并重写了load()方法,该方法中的逻辑实际就是去访问网络接口获取数据了。下面我们回到postsOfSubreddit()方法中,调用该方法的地方一般会在ViewModel中:

    @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
    val posts = flowOf(
        clearListCh.consumeAsFlow().map { PagingData.empty<RedditPost>() },
        savedStateHandle.getLiveData<String>(KEY_SUBREDDIT)
            .asFlow()
            .flatMapLatest {
                repository.postsOfSubreddit(it, 30)
            }
    ).flattenMerge(2)

可以看到最终组合成posts的流对象,我们看一下对posts流收集的地方,这一般在Activity初始化的时候:

lifecycleScope.launchWhenCreated {
            @OptIn(ExperimentalCoroutinesApi::class)
            model.posts.collectLatest {
                Log.d(TAG, "posts $it")
                adapter.submitData(it)
            }
        }

可以看到正是调用PagingDataAdapter的submitData()方法,下面正式分析该方法的逻辑。从开始的时序图中可知,最终会调用到PagingDataDiffer类中的collectFrom()方法:

        pagingData.flow.collect { event ->
            withContext(mainDispatcher) {
                if (event is PageEvent.Insert && event.loadType == REFRESH) {
                    val newPresenter = PagePresenter(event)
                    val transformedLastAccessedIndex = performDiff(
                        previousList = presenter,
                        newList = newPresenter,
                        newCombinedLoadStates = event.combinedLoadStates,
                        lastAccessedIndex = lastAccessedIndex
                    )
                    presenter = newPresenter

                    // Dispatch ListUpdate as soon as we are done diffing.
                    dataRefreshedListeners.forEach { listener ->
                        listener(event.pages.all { page -> page.data.isEmpty() })
                    }

                    // Transform the last loadAround index from the old list to the new list
                    // by passing it through the DiffResult, and pass it forward as a
                    // ViewportHint within the new list to the next generation of Pager.
                    // This ensures prefetch distance for the last ViewportHint from the old
                    // list is respected in the new list, even if invalidation interrupts
                    // the prepend / append load that would have fulfilled it in the old
                    // list.
                    transformedLastAccessedIndex?.let { newIndex ->
                        lastAccessedIndex = newIndex
                        receiver?.addHint(presenter.loadAround(newIndex))
                    }
                } else {
                    if (postEvents()) {
                        yield()
                    }

                    // Send event to presenter to be shown to the UI.
                    presenter.processEvent(event, callback)
                }
            }
        }
    }

collect{}代码中的逻辑在上一节中有分析,注意计算过程会用到App层自定义的diffCallback。在分析collect{}代码块中的逻辑前我们应该弄清楚这里的flow到底是什么对象,它是PagingData中的对象,那么这个PagingData是什么时候初始化的呢,答案是:

.mapLatest { generation ->
                PagingData(generation.pageEventFlow, PagerUiReceiver(generation, retryChannel))
            }

下面看一下pageEventFlow的定义:

    @OptIn(ExperimentalCoroutinesApi::class)
    val pageEventFlow: Flow<PageEvent<Value>> = cancelableChannelFlow(pageEventChannelFlowJob) {
        check(pageEventChCollected.compareAndSet(false, true)) {
            "cannot collect twice from pager"
        }

        // Start collection on pageEventCh, which the rest of this class uses to send PageEvents
        // to this flow.
        launch { pageEventCh.consumeAsFlow().collect { send(it) } }

        // Wrap collection behind a RendezvousChannel to prevent the RetryChannel from buffering
        // retry signals.
        val retryChannel = Channel<Unit>(Channel.RENDEZVOUS)
        launch { retryFlow.collect { retryChannel.offer(it) } }

        // Start collection on retry signals.
        launch {
            retryChannel.consumeAsFlow()
                .collect {
                    val loadStates = stateLock.withLock { state.loadStates }
                    loadStates.forEach { loadType, fromRemote, loadState ->
                        if (loadState !is Error) return@forEach

                        retryLoadError(
                            loadType = loadType,
                            fromRemote = fromRemote,
                            viewportHint = when {
                                // ViewportHint is only used when retrying source PREPEND / APPEND.
                                (fromRemote || loadType == REFRESH) -> null
                                else -> state.failedHintsByLoadType[loadType]
                            }
                        )

                        // If retrying REFRESH from PagingSource succeeds, start collection on
                        // ViewportHints for PREPEND / APPEND loads.
                        if (!fromRemote && loadType == REFRESH) {
                            val newRefreshState = stateLock.withLock {
                                state.loadStates.get(REFRESH, false)
                            }

                            if (newRefreshState !is Error) {
                                startConsumingHints()
                            }
                        }
                    }
                }
        }

        // Trigger RemoteMediator initialization blockingly.
        if (triggerRemoteRefresh) {
            remoteMediatorAccessor?.run {
                val pagingState = stateLock.withLock { state.currentPagingState(null) }
                doBoundaryCall(this@cancelableChannelFlow, REFRESH, pagingState)
            }
        }

        // Setup finished, start the initial load even if RemoteMediator throws an error.
        doInitialLoad(this, state)

        // Only start collection on ViewportHints if the initial load succeeded.
        if (stateLock.withLock { state.loadStates.get(REFRESH, false) } !is Error) {
            startConsumingHints()
        }
    }

通过注释可知,pageEventCh字段就是主要用来产生PageEvent的,pageEventCh是一个BUFFERED类型的Channel对象,如果pageEventCh中一旦有新值就会发射一个PageEvent到流pageEventFlow里面。retryFlow是一个Flow对象,它在初始化PageFetcherSnapshot()通过PagerFetcher中的retryChannel.asFlow()赋值得到。当retryChannel有值时会触发retryLoadError(),这里我们先不分析其内部逻辑,直接跳到doInitialLoad()方法。从注释可以看到:到这里即初始化完成,并开始初始加载。

    private suspend fun doInitialLoad(scope: CoroutineScope, state: PagerState<Key, Value>) {
        stateLock.withLock { state.setLoading(REFRESH, false) }

        val params = loadParams(REFRESH, initialKey)
        when (val result = pagingSource.load(params)) {
            is Page<Key, Value> -> {
                val insertApplied = stateLock.withLock { state.insert(0, REFRESH, result) }

                // Update loadStates which are sent along with this load's Insert PageEvent.
                stateLock.withLock {
                    state.loadStates.set(REFRESH, false, NotLoading.Incomplete)
                    if (result.prevKey == null) {
                        state.loadStates.set(
                            type = PREPEND,
                            remote = false,
                            state = when (remoteMediatorAccessor) {
                                null -> NotLoading.Complete
                                else -> NotLoading.Incomplete
                            }
                        )
                    }
                    if (result.nextKey == null) {
                        state.loadStates.set(
                            type = APPEND,
                            remote = false,
                            state = when (remoteMediatorAccessor) {
                                null -> NotLoading.Complete
                                else -> NotLoading.Incomplete
                            }
                        )
                    }
                }

                // Send insert event after load state updates, so that endOfPaginationReached is
                // correctly reflected in the insert event. Note that we only send the event if the
                // insert was successfully applied in the case of cancellation due to page dropping.
                if (insertApplied) {
                    stateLock.withLock {
                        with(state) {
                            pageEventCh.send(result.toPageEvent(REFRESH, config.enablePlaceholders))
                        }
                    }
                }

                // Launch any RemoteMediator boundary calls after applying initial insert.
                if (remoteMediatorAccessor != null) {
                    if (result.prevKey == null || result.nextKey == null) {
                        val pagingState = stateLock.withLock { state.currentPagingState(lastHint) }

                        if (result.prevKey == null) {
                            remoteMediatorAccessor.doBoundaryCall(scope, PREPEND, pagingState)
                        }

                        if (result.nextKey == null) {
                            remoteMediatorAccessor.doBoundaryCall(scope, APPEND, pagingState)
                        }
                    }
                }
            }
            is LoadResult.Error -> stateLock.withLock {
                val loadState = Error(result.throwable)
                if (state.loadStates.set(REFRESH, false, loadState)) {
                    pageEventCh.send(LoadStateUpdate(REFRESH, false, loadState))
                }
            }
        }
    }

首先会调用pagingSource.load()去获取数据,pagingSource是在构造PageFetcherSnapshot对象时传入的,在这里即是我们在App层构造的PageKeyedSubredditPagingSource对象,即会调用PageKeyedSubredditPagingSource用户重写的load()方法向网络接口获取数据。获取成功后知道result是Page类型的,因此调用state.insert()执行数据插入,并更新state.loadStates的信息。

加载更多(预加载)

一般在使用RecyclerView的时候,需要根据用户滑动时判断还剩多少数据未展示来控制是否触发预加载逻辑,下面我们看看Paging是怎么实现这个功能的,先从PagingDataAdapter的getItem()方法看起:

protected fun getItem(position: Int) = differ.getItem(position)

这里的differ是一个AsyncPagingDataDiffer对象:

fun getItem(index: Int): T? {
        try {
            inGetItem = true
            return differBase[index]
        } finally {
            inGetItem = false
        }
    }

接着看differBase的get()方法,differBase是一个PagingDataDiffer对象:

operator fun get(index: Int): T? {
        lastAccessedIndex = index
        receiver?.addHint(presenter.loadAround(index))
        return presenter.get(index)
    }

这里的receiver是一个UiReceiver对象,在前文中的colectFrom()方法中我们可以看到receiver的初始化为:receiver = pagingData.receiver。下面看一下addHint()方法:

inner class PagerUiReceiver<Key : Any, Value : Any> constructor(
        private val pageFetcherSnapshot: PageFetcherSnapshot<Key, Value>,
        private val retryChannel: SendChannel<Unit>
    ) : UiReceiver {
        override fun addHint(hint: ViewportHint) = pageFetcherSnapshot.addHint(hint)

        override fun retry() {
            retryChannel.offer(Unit)
        }

        override fun refresh() = this@PageFetcher.refresh()
    }

这里的pageFetcherSnapshot是一个PageFetcherSnapshot对象,前面分析过它的初始化,继续:

fun addHint(hint: ViewportHint) {
        lastHint = hint
        @OptIn(ExperimentalCoroutinesApi::class)
        hintChannel.offer(hint)
    }

这里和上文刷新数据的套路相似,可以看到hintChannel是一个BroadcastChannel对象,只要该channel中有新值,它会广播给所有的订阅者,下面看一下订阅hintChannel的地方:

    @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
    private fun CoroutineScope.startConsumingHints() {
        // Pseudo-tiling via invalidation on jumps.
        if (config.jumpThreshold != COUNT_UNDEFINED) {
            launch {
                hintChannel.asFlow()
                    .collect { hint ->
                        stateLock.withLock {
                            with(state) {
                                hint.withCoercedHint { _, _, hintOffset ->
                                    if (hintOffset.absoluteValue >= config.jumpThreshold) {
                                        invalidate()
                                    }
                                }
                            }
                        }
                    }
            }
        }

        launch {
            state.consumeAppendGenerationIdAsFlow()
                .transformLatest<Int, GenerationalViewportHint> { generationId ->
                    // Reset state to Idle and setup a new flow for consuming incoming load hints.
                    // Subsequent generationIds are normally triggered by cancellation.
                    stateLock.withLock {
                        // Skip this generationId of loads if there is no more to load in this
                        // direction. In the case of the terminal page getting dropped, a new
                        // generationId will be sent after load state is updated to Idle.
                        if (state.loadStates.get(APPEND, false) == NotLoading.Complete) {
                            return@transformLatest
                        } else if (state.loadStates.get(APPEND, false) !is Error) {
                            state.loadStates.set(APPEND, false, NotLoading.Incomplete)
                        }
                    }

					//将hintChannel转换成flow,会触发下游的收集。重点关注这里
                    @OptIn(FlowPreview::class)
                    val generationalHints = hintChannel.asFlow()
                        // Prevent infinite loop when competing prepend / append cancel each other
                        .drop(if (generationId == 0) 0 else 1)
                        .map { hint -> GenerationalViewportHint(generationId, hint) }
                    emitAll(generationalHints)
                }
                .scan(GenerationalViewportHint.APPEND_INITIAL_VALUE) { acc, it ->
                    if (acc.hint > it.hint) acc else it
                }
                .filter { it != GenerationalViewportHint.APPEND_INITIAL_VALUE }
                .conflate()
                .collect { generationalHint ->
                    doLoad(this, state, APPEND, generationalHint)
                }
        }
    }

可以看到上游hintChannel有值时,会构造一个GenerationalViewportHint对象,下游会调用doLoad()方法:

private suspend fun doLoad(
        scope: CoroutineScope,
        state: PagerState<Key, Value>,
        loadType: LoadType,
        generationalHint: GenerationalViewportHint
    ) {
    var loadKey: Key? = stateLock.withLock {
            with(state) {
                generationalHint.hint.withCoercedHint { indexInPage, pageIndex, hintOffset ->
                //重点关注这里
                    nextLoadKeyOrNull(
                        loadType,
                        generationalHint.generationId,
                        indexInPage,
                        pageIndex,
                        hintOffset,
                        generationalHint.hint.fromRetry
                    )?.also { setLoading(loadType, false) }
                }
            }
        }
        loop@ while (loadKey != null) {
            val params = loadParams(loadType, loadKey)
            val result: LoadResult<Key, Value> = pagingSource.load(params)
            when (result) {
           	 	//省略部分代码
            }
            
                state.dropInfo(dropType, generationalHint.hint, config.prefetchDistance)
                    ?.let { info ->
                        state.drop(dropType, info.pageCount, info.placeholdersRemaining)
                        pageEventCh.send(Drop(dropType, info.pageCount, info.placeholdersRemaining))
                    }

                with(state) {
                    loadKey = generationalHint.hint
                        .withCoercedHint { indexInPage, pageIndex, hintOffset ->
                            state.nextLoadKeyOrNull(
                                loadType,
                                generationalHint.generationId,
                                indexInPage,
                                pageIndex,
                                hintOffset,
                                generationalHint.hint.fromRetry
                            )
                        }
                }
            }
        }
    }

这里代码很长,主要是执行一个循环,通过计算一个类型为Key的loadKey,判断其是否为null来决定是否触发真正的加载更多操作。下面看一下nextLoadKeyOrNull()方法的实现:

    private fun PagerState<Key, Value>.nextLoadKeyOrNull(
        loadType: LoadType,
        generationId: Int,
        indexInPage: Int,
        pageIndex: Int,
        hintOffset: Int,
        fromRetry: Boolean
    ): Key? = when (loadType) {
        PREPEND -> nextPrependKey(
            generationId,
            pageIndex,
            indexInPage,
            config.prefetchDistance + hintOffset.absoluteValue,
            fromRetry
        )
        APPEND -> nextAppendKey(
            generationId,
            pageIndex,
            indexInPage,
            config.prefetchDistance + hintOffset.absoluteValue,
            fromRetry
        )
        REFRESH -> throw IllegalArgumentException("Just use initialKey directly")
    }

只看APPEND的逻辑:

private fun PagerState<Key, Value>.nextAppendKey(
        loadId: Int,
        pageIndex: Int,
        indexInPage: Int,
        prefetchDistance: Int,
        fromRetry: Boolean
    ): Key? {
        if (loadId != appendLoadId) return null
        // Skip load if in error state, unless retrying.
        if (loadStates.get(APPEND, false) is Error && !fromRetry) return null

        val itemsIncludingPage = (pageIndex until pages.size).sumBy { pages[it].data.size }
        val shouldLoad = indexInPage + 1 + prefetchDistance > itemsIncludingPage
        return if (shouldLoad) pages.last().nextKey else null
    }

可以看到当loadStates是Error类型是跳过加载更多,同时当前页可见最后一个item的index + 预加载距离 > 已经获取的所有item数量时,shouldLoad为真,才会触发加载更多操作。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值