MutableLoadStateCollection.kt

/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.paging

import androidx.paging.LoadState.Error
import androidx.paging.LoadState.Loading
import androidx.paging.LoadState.NotLoading

/**
 * Helper to construct [CombinedLoadStates] that accounts for previous state to set the convenience
 * properties correctly.
 */
internal class MutableLoadStateCollection {
    private var refresh: LoadState = NotLoading.Incomplete
    private var prepend: LoadState = NotLoading.Incomplete
    private var append: LoadState = NotLoading.Incomplete
    private var source: LoadStates = LoadStates.IDLE
    private var mediator: LoadStates? = null

    fun snapshot() = CombinedLoadStates(
        refresh = refresh,
        prepend = prepend,
        append = append,
        source = source,
        mediator = mediator,
    )

    fun set(combinedLoadStates: CombinedLoadStates) {
        refresh = combinedLoadStates.refresh
        prepend = combinedLoadStates.prepend
        append = combinedLoadStates.append
        source = combinedLoadStates.source
        mediator = combinedLoadStates.mediator
    }

    fun set(sourceLoadStates: LoadStates, remoteLoadStates: LoadStates?) {
        source = sourceLoadStates
        mediator = remoteLoadStates
        updateHelperStates()
    }

    fun set(type: LoadType, remote: Boolean, state: LoadState): Boolean {
        val didChange = if (remote) {
            val lastMediator = mediator
            mediator = (mediator ?: LoadStates.IDLE).modifyState(type, state)
            mediator != lastMediator
        } else {
            val lastSource = source
            source = source.modifyState(type, state)
            source != lastSource
        }

        updateHelperStates()
        return didChange
    }

    fun get(type: LoadType, remote: Boolean): LoadState? {
        return (if (remote) mediator else source)?.get(type)
    }

    private fun updateHelperStates() {
        refresh = computeHelperState(
            previousState = refresh,
            sourceRefreshState = source.refresh,
            sourceState = source.refresh,
            remoteState = mediator?.refresh
        )
        prepend = computeHelperState(
            previousState = prepend,
            sourceRefreshState = source.refresh,
            sourceState = source.prepend,
            remoteState = mediator?.prepend
        )
        append = computeHelperState(
            previousState = append,
            sourceRefreshState = source.refresh,
            sourceState = source.append,
            remoteState = mediator?.append
        )
    }

    /**
     * Computes the next value for the convenience helpers in [CombinedLoadStates], which
     * generally defers to remote state, but waits for both source and remote states to become
     * [NotLoading] before moving to that state. This provides a reasonable default for the common
     * use-case where you generally want to wait for both RemoteMediator to return and for the
     * update to get applied before signaling to UI that a network fetch has "finished".
     */
    private fun computeHelperState(
        previousState: LoadState,
        sourceRefreshState: LoadState,
        sourceState: LoadState,
        remoteState: LoadState?
    ): LoadState {
        if (remoteState == null) return sourceState

        return when (previousState) {
            is Loading -> when {
                sourceRefreshState is NotLoading && remoteState is NotLoading -> remoteState
                remoteState is Error -> remoteState
                else -> previousState
            }
            else -> remoteState
        }
    }

    internal inline fun forEach(op: (LoadType, Boolean, LoadState) -> Unit) {
        source.forEach { type, state ->
            op(type, false, state)
        }
        mediator?.forEach { type, state ->
            op(type, true, state)
        }
    }

    internal fun terminates(loadType: LoadType): Boolean {
        return get(loadType, false)!!.endOfPaginationReached &&
            get(loadType, true)?.endOfPaginationReached != false
    }
}