blob: 661eac18b7810d6382c3a4a0efdd584f69fbc849 [file] [log] [blame]
/*
* Copyright 2019 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.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import androidx.paging.LoadType.END
import androidx.paging.LoadType.REFRESH
import androidx.paging.LoadType.START
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicReference
open class AsyncPagingDataDiffer<T : Any>(
private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
private val workerDispatcher: CoroutineDispatcher = Dispatchers.Default,
private val diffCallback: DiffUtil.ItemCallback<T>,
private val updateCallback: ListUpdateCallback
) {
internal val callback = object : PresenterCallback {
override fun onInserted(position: Int, count: Int) {
updateCallback.onInserted(position, count)
}
override fun onRemoved(position: Int, count: Int) =
updateCallback.onRemoved(position, count)
override fun onChanged(position: Int, count: Int) {
// NOTE: pass a null payload to convey null -> item, or item -> null
updateCallback.onChanged(position, count, null)
}
override fun onStateUpdate(loadType: LoadType, loadState: LoadState) {
when (loadType) {
REFRESH -> {
if (loadState != loadStates[REFRESH]) {
loadStates[REFRESH] = loadState
dispatchLoadState(REFRESH, loadState)
}
}
START -> {
if (loadState != loadStates[START]) {
loadStates[START] = loadState
dispatchLoadState(START, loadState)
}
}
END -> {
if (loadState != loadStates[END]) {
loadStates[END] = loadState
dispatchLoadState(END, loadState)
}
}
}
}
private fun dispatchLoadState(type: LoadType, state: LoadState) {
loadStateListeners.forEach { it(type, state) }
}
}
/** True if we're currently executing [getItem] */
internal var inGetItem: Boolean = false
private val differBase = object : PagingDataDiffer<T>(mainDispatcher) {
override suspend fun performDiff(
previousList: NullPaddedList<T>,
newList: NullPaddedList<T>,
newLoadStates: Map<LoadType, LoadState>,
lastAccessedIndex: Int
): Int? {
return withContext(mainDispatcher) {
when {
previousList.size == 0 -> {
// fast path for no items -> some items
callback.onInserted(0, newList.size)
newLoadStates.entries.forEach { callback.onStateUpdate(it.key, it.value) }
return@withContext null
}
newList.size == 0 -> {
// fast path for some items -> no items
callback.onRemoved(0, previousList.size)
newLoadStates.entries.forEach { callback.onStateUpdate(it.key, it.value) }
return@withContext null
}
else -> { // full diff
val diffResult = withContext(workerDispatcher) {
previousList.computeDiff(newList, diffCallback)
}
previousList.dispatchDiff(updateCallback, newList, diffResult)
newLoadStates.entries.forEach { callback.onStateUpdate(it.key, it.value) }
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
}
}
private val job = AtomicReference<Job?>(null)
suspend fun presentData(pagingData: PagingData<T>) {
try {
job.get()?.cancelAndJoin()
} finally {
differBase.collectFrom(pagingData, callback)
}
}
fun submitData(lifecycle: Lifecycle, pagingData: PagingData<T>) {
val newJob = lifecycle.coroutineScope.launch(start = CoroutineStart.LAZY) {
differBase.collectFrom(pagingData, callback)
}
job.getAndSet(newJob)?.cancel()
newJob.start()
}
fun retry() {
differBase.retry()
}
fun refresh() {
differBase.refresh()
}
/**
* Get the item from the current PagedList at the specified index.
*
* Note that this operates on both loaded items and null padding within the PagedList.
*
* @param index Index of item to get, must be >= 0, and < [itemCount]
* @return The item, or null, if a null placeholder is at the specified position.
*/
open fun getItem(index: Int): T? {
try {
inGetItem = true
return differBase[index]
} finally {
inGetItem = false
}
}
/**
* Get the number of items currently presented by this Differ. This value can be directly
* returned to [androidx.recyclerview.widget.RecyclerView.Adapter.getItemCount].
*
* @return Number of items being presented.
*/
open val itemCount: Int
get() = differBase.size
internal val loadStateListeners: MutableList<(LoadType, LoadState) -> Unit> =
CopyOnWriteArrayList()
internal val loadStates = mutableMapOf<LoadType, LoadState>(
REFRESH to LoadState.Idle,
START to LoadState.Idle,
END to LoadState.Idle
)
/**
* Add a listener to observe the loading state.
*
* As new [PagingData] generations are submitted and displayed, the listener will be notified to
* reflect current [LoadType.REFRESH], [LoadType.START], and [LoadType.END] states.
*
* @param listener to receive [LoadState] updates.
*
* @see removeLoadStateListener
*/
open fun addLoadStateListener(listener: (LoadType, LoadState) -> Unit) {
// Note: Important to add the listener first before sending off events, in case the
// callback triggers removal, which could lead to a leak if the listener is added
// afterwards.
loadStateListeners.add(listener)
listener(REFRESH, loadStates[REFRESH]!!)
if (loadStateListeners.contains(listener)) listener(START, loadStates[START]!!)
if (loadStateListeners.contains(listener)) listener(END, loadStates[END]!!)
}
/**
* Remove a previously registered load state listener.
*
* @param listener Previously registered listener.
* @see addLoadStateListener
*/
open fun removeLoadStateListener(listener: (LoadType, LoadState) -> Unit) {
loadStateListeners.remove(listener)
}
}