| /* |
| * Copyright (C) 2017 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.annotation.VisibleForTesting |
| import androidx.arch.core.executor.ArchTaskExecutor |
| import androidx.lifecycle.LiveData |
| import androidx.recyclerview.widget.AdapterListUpdateCallback |
| import androidx.recyclerview.widget.AsyncDifferConfig |
| import androidx.recyclerview.widget.DiffUtil |
| import androidx.recyclerview.widget.ListUpdateCallback |
| import androidx.recyclerview.widget.RecyclerView |
| import java.util.concurrent.CopyOnWriteArrayList |
| |
| /** |
| * Helper object for mapping a [androidx.paging.PagedList] into a |
| * [RecyclerView.Adapter][androidx.recyclerview.widget.RecyclerView.Adapter]. |
| * |
| * For simplicity, the [PagedListAdapter] wrapper class can often be used instead of the differ |
| * directly. This diff class is exposed for complex cases, and where overriding an adapter base |
| * class to support paging isn't convenient. |
| * |
| * When consuming a [LiveData] of PagedList, you can observe updates and dispatch them directly to |
| * [submitList]. The AsyncPagedListDiffer then can present this updating data set simply for an |
| * adapter. It listens to PagedList loading callbacks, and uses DiffUtil on a background thread to |
| * compute updates as new PagedLists are received. |
| * |
| * It provides a simple list-like API with [getItem] and [itemCount] for an adapter to acquire |
| * and present data objects. |
| * |
| * A complete usage pattern with Room would look like this: |
| * ``` |
| * @Dao |
| * interface UserDao { |
| * @Query("SELECT * FROM user ORDER BY lastName ASC") |
| * public abstract DataSource.Factory<Integer, User> usersByLastName(); |
| * } |
| * |
| * class MyViewModel extends ViewModel { |
| * public final LiveData<PagedList<User>> usersList; |
| * public MyViewModel(UserDao userDao) { |
| * usersList = new LivePagedListBuilder<>( |
| * userDao.usersByLastName(), /* page size */ 20).build(); |
| * } |
| * } |
| * |
| * class MyActivity extends AppCompatActivity { |
| * @Override |
| * public void onCreate(Bundle savedState) { |
| * super.onCreate(savedState); |
| * MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class); |
| * RecyclerView recyclerView = findViewById(R.id.user_list); |
| * final UserAdapter adapter = new UserAdapter(); |
| * viewModel.usersList.observe(this, pagedList -> adapter.submitList(pagedList)); |
| * recyclerView.setAdapter(adapter); |
| * } |
| * } |
| * |
| * class UserAdapter extends RecyclerView.Adapter<UserViewHolder> { |
| * private final AsyncPagedListDiffer<User> differ = |
| * new AsyncPagedListDiffer(this, DIFF_CALLBACK); |
| * @Override |
| * public int getItemCount() { |
| * return differ.getItemCount(); |
| * } |
| * public void submitList(PagedList<User> pagedList) { |
| * differ.submitList(pagedList); |
| * } |
| * @Override |
| * public void onBindViewHolder(UserViewHolder holder, int position) { |
| * User user = differ.getItem(position); |
| * if (user != null) { |
| * holder.bindTo(user); |
| * } else { |
| * // Null defines a placeholder item - AsyncPagedListDiffer will automatically |
| * // invalidate this row when the actual object is loaded from the database |
| * holder.clear(); |
| * } |
| * } |
| * public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK = |
| * new DiffUtil.ItemCallback<User>() { |
| * @Override |
| * public boolean areItemsTheSame( |
| * @NonNull User oldUser, @NonNull User newUser) { |
| * // User properties may have changed if reloaded from the DB, but ID is fixed |
| * return oldUser.getId() == newUser.getId(); |
| * } |
| * @Override |
| * public boolean areContentsTheSame(@NonNull User oldUser, @NonNull User newUser) { |
| * // NOTE: if you use equals, your object must properly override Object#equals() |
| * // Incorrectly returning false here will result in too many animations. |
| * return oldUser.equals(newUser); |
| * } |
| * } |
| * } |
| * ``` |
| * |
| * @param T Type of the PagedLists this differ will receive. |
| */ |
| @Deprecated( |
| message = "AsyncPagedListDiffer is deprecated and has been replaced by AsyncPagingDataDiffer", |
| replaceWith = ReplaceWith( |
| "AsyncPagingDataDiffer<T>", |
| "androidx.paging.AsyncPagingDataDiffer" |
| ) |
| ) |
| open class AsyncPagedListDiffer<T : Any> { |
| /** |
| * updateCallback notifications must only be notified *after* new data and item count are stored |
| * this ensures Adapter#notifyItemRangeInserted etc are accessing the new data |
| */ |
| internal lateinit var updateCallback: ListUpdateCallback |
| |
| @Suppress("MemberVisibilityCanBePrivate") // synthetic access |
| internal val config: AsyncDifferConfig<T> |
| |
| internal var mainThreadExecutor = ArchTaskExecutor.getMainThreadExecutor() |
| |
| @Suppress("DEPRECATION") |
| @VisibleForTesting |
| internal val listeners = CopyOnWriteArrayList<PagedListListener<T>>() |
| @Suppress("DEPRECATION") |
| private var pagedList: PagedList<T>? = null |
| @Suppress("DEPRECATION") |
| private var snapshot: PagedList<T>? = null |
| |
| /** |
| * Max generation of currently scheduled runnable |
| */ |
| @Suppress("MemberVisibilityCanBePrivate") // synthetic access |
| internal var maxScheduledGeneration: Int = 0 |
| |
| @Suppress("DEPRECATION") |
| private val loadStateManager = object : PagedList.LoadStateManager() { |
| override fun onStateChanged(type: LoadType, state: LoadState) { |
| // Don't need to post - PagedList will already have done that |
| loadStateListeners.forEach { it(type, state) } |
| } |
| } |
| |
| private val loadStateListener = loadStateManager::onStateChanged |
| |
| internal val loadStateListeners: MutableList<(LoadType, LoadState) -> Unit> = |
| CopyOnWriteArrayList() |
| |
| @Suppress("DEPRECATION") |
| private val pagedListCallback = object : PagedList.Callback() { |
| 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 |
| updateCallback.onChanged(position, count, null) |
| } |
| } |
| |
| /** |
| * Get the number of items currently presented by this Differ. This value can be directly |
| * returned to [RecyclerView.Adapter.getItemCount]. |
| * |
| * @return Number of items being presented. |
| */ |
| open val itemCount: Int |
| get() = currentList?.size ?: 0 |
| |
| /** |
| * Returns the PagedList currently being displayed by the differ. |
| * |
| * This is not necessarily the most recent list passed to [submitList], because a diff is |
| * computed asynchronously between the new list and the current list before updating the |
| * currentList value. May be `null` if no [PagedList] is being presented. |
| * |
| * @return The list currently being displayed, may be `null`. |
| */ |
| @Suppress("DEPRECATION") |
| open val currentList: PagedList<T>? |
| get() = snapshot ?: pagedList |
| |
| /** |
| * Listener for when the current [PagedList] is updated. |
| * |
| * @param T Type of items in [PagedList] |
| */ |
| @Deprecated("PagedList is deprecated and has been replaced by PagingData") |
| interface PagedListListener<T : Any> { |
| /** |
| * Called after the current PagedList has been updated. |
| * |
| * @param previousList The previous list, may be null. |
| * @param currentList The new current list, may be null. |
| */ |
| fun onCurrentListChanged( |
| @Suppress("DEPRECATION") previousList: PagedList<T>?, |
| @Suppress("DEPRECATION") currentList: PagedList<T>? |
| ) |
| } |
| |
| /** |
| * Wrapper for [PagedList] listener, which reports when the current [PagedList] is updated. |
| * |
| * @param T Type of items in [PagedList] |
| */ |
| @Suppress("DEPRECATION") |
| private class OnCurrentListChangedWrapper<T : Any>( |
| val callback: (PagedList<T>?, PagedList<T>?) -> Unit |
| ) : PagedListListener<T> { |
| override fun onCurrentListChanged(previousList: PagedList<T>?, currentList: PagedList<T>?) { |
| callback(previousList, currentList) |
| } |
| } |
| |
| /** |
| * Convenience for |
| * |
| * ``` |
| * AsyncPagedListDiffer( |
| * AdapterListUpdateCallback(adapter), |
| * AsyncDifferConfig.Builder<T>(diffCallback).build() |
| * ) |
| * ``` |
| * |
| * @param adapter Adapter that will receive update signals. |
| * @param diffCallback The [DiffUtil.ItemCallback] instance to compare items in the list. |
| */ |
| @Deprecated( |
| message = "PagedList is deprecated and has been replaced by PagingData", |
| replaceWith = ReplaceWith( |
| """AsyncPagingDataDiffer( |
| Dispatchers.Main, |
| Dispatchers.IO, |
| diffCallback, |
| listUpdateCallback |
| )""", |
| "androidx.paging.AsyncPagingDataDiffer", |
| "kotlinx.coroutines.Dispatchers" |
| ) |
| ) |
| constructor(adapter: RecyclerView.Adapter<*>, diffCallback: DiffUtil.ItemCallback<T>) { |
| updateCallback = AdapterListUpdateCallback(adapter) |
| config = AsyncDifferConfig.Builder(diffCallback).build() |
| } |
| |
| @Deprecated( |
| message = "PagedList is deprecated and has been replaced by PagingData", |
| replaceWith = ReplaceWith( |
| """AsyncPagingDataDiffer( |
| Dispatchers.Main, |
| Dispatchers.IO, |
| config.diffCallback, |
| listUpdateCallback |
| )""", |
| "androidx.paging.AsyncPagingDataDiffer", |
| "kotlinx.coroutines.Dispatchers" |
| ) |
| ) |
| constructor(listUpdateCallback: ListUpdateCallback, config: AsyncDifferConfig<T>) { |
| updateCallback = listUpdateCallback |
| this.config = config |
| } |
| |
| /** |
| * 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 < `getItemCount`. |
| * @return The item, or null, if a null placeholder is at the specified position. |
| */ |
| open fun getItem(index: Int): T? { |
| val snapshot = this.snapshot |
| val pagedList = this.pagedList |
| |
| return when { |
| snapshot != null -> snapshot[index] |
| pagedList != null -> { |
| pagedList.loadAround(index) |
| pagedList[index] |
| } |
| else -> throw IndexOutOfBoundsException("Item count is zero, getItem() call is invalid") |
| } |
| } |
| |
| /** |
| * Pass a new [PagedList] to the differ. |
| * |
| * If a PagedList is already present, a diff will be computed asynchronously on a background |
| * thread. When the diff is computed, it will be applied (dispatched to the |
| * [ListUpdateCallback]), and the new PagedList will be swapped in as the [currentList]. |
| * |
| * @param pagedList The new PagedList. |
| */ |
| open fun submitList(@Suppress("DEPRECATION") pagedList: PagedList<T>?) = |
| submitList(pagedList, null) |
| |
| /** |
| * Pass a new PagedList to the differ. |
| * |
| * If a PagedList is already present, a diff will be computed asynchronously on a background |
| * thread. When the diff is computed, it will be applied (dispatched to the |
| * [ListUpdateCallback]), and the new PagedList will be swapped in as the |
| * [current list][currentList]. |
| * |
| * The commit callback can be used to know when the PagedList is committed, but note that it |
| * may not be executed. If PagedList B is submitted immediately after PagedList A, and is |
| * committed directly, the callback associated with PagedList A will not be run. |
| * |
| * @param pagedList The new [PagedList]. |
| * @param commitCallback Optional runnable that is executed when the PagedList is committed, if |
| * it is committed. |
| */ |
| open fun submitList( |
| @Suppress("DEPRECATION") pagedList: PagedList<T>?, |
| commitCallback: Runnable? |
| ) { |
| // incrementing generation means any currently-running diffs are discarded when they finish |
| val runGeneration = ++maxScheduledGeneration |
| |
| if (pagedList === this.pagedList) { |
| // nothing to do (Note - still had to inc generation, since may have ongoing work) |
| commitCallback?.run() |
| return |
| } |
| |
| val previous = currentList |
| |
| if (pagedList == null) { |
| val removedCount = itemCount |
| val currentPagedList = this.pagedList |
| if (currentPagedList != null) { |
| currentPagedList.removeWeakCallback(pagedListCallback) |
| currentPagedList.removeWeakLoadStateListener(loadStateListener) |
| this.pagedList = null |
| } else if (snapshot != null) { |
| snapshot = null |
| } |
| // dispatch update callback after updating pagedList/snapshot |
| updateCallback.onRemoved(0, removedCount) |
| onCurrentListChanged(previous, null, commitCallback) |
| return |
| } |
| |
| if (currentList == null) { |
| // fast simple first insert |
| this.pagedList = pagedList |
| pagedList.addWeakLoadStateListener(loadStateListener) |
| pagedList.addWeakCallback(pagedListCallback) |
| |
| // dispatch update callback after updating pagedList/snapshot |
| updateCallback.onInserted(0, pagedList.size) |
| |
| onCurrentListChanged(null, pagedList, commitCallback) |
| return |
| } |
| |
| this.pagedList?.let { |
| // first update scheduled on this list, so capture mPages as a snapshot, removing |
| // callbacks so we don't have resolve updates against a moving target |
| it.removeWeakCallback(pagedListCallback) |
| it.removeWeakLoadStateListener(loadStateListener) |
| |
| @Suppress("DEPRECATION") |
| snapshot = it.snapshot() as PagedList<T> |
| this.pagedList = null |
| } |
| |
| val oldSnapshot = snapshot |
| if (oldSnapshot == null || this.pagedList != null) { |
| throw IllegalStateException("must be in snapshot state to diff") |
| } |
| |
| @Suppress("DEPRECATION") |
| val newSnapshot = pagedList.snapshot() as PagedList<T> |
| val recordingCallback = RecordingCallback() |
| pagedList.addWeakCallback(recordingCallback) |
| config.backgroundThreadExecutor.execute { |
| val result = oldSnapshot.getNullPaddedList().computeDiff( |
| newSnapshot.getNullPaddedList(), |
| config.diffCallback |
| ) |
| |
| mainThreadExecutor.execute { |
| if (maxScheduledGeneration == runGeneration) { |
| latchPagedList( |
| pagedList, |
| newSnapshot, |
| result, |
| recordingCallback, |
| oldSnapshot.lastLoad(), |
| commitCallback |
| ) |
| } |
| } |
| } |
| } |
| |
| @Suppress("MemberVisibilityCanBePrivate") // synthetic access |
| internal fun latchPagedList( |
| @Suppress("DEPRECATION") newList: PagedList<T>, |
| @Suppress("DEPRECATION") diffSnapshot: PagedList<T>, |
| diffResult: DiffUtil.DiffResult, |
| recordingCallback: RecordingCallback, |
| lastAccessIndex: Int, |
| commitCallback: Runnable? |
| ) { |
| val previousSnapshot = snapshot |
| if (previousSnapshot == null || pagedList != null) { |
| throw IllegalStateException("must be in snapshot state to apply diff") |
| } |
| |
| pagedList = newList |
| newList.addWeakLoadStateListener(loadStateListener) |
| snapshot = null |
| |
| // dispatch updates to UI from previousSnapshot -> newSnapshot |
| previousSnapshot.getNullPaddedList().dispatchDiff( |
| updateCallback, |
| previousSnapshot.getNullPaddedList(), |
| diffResult |
| ) |
| |
| // dispatch updates to UI from newSnapshot -> currentList |
| // after this, the callback will be up to date with current pagedList... |
| recordingCallback.dispatchRecordingTo(pagedListCallback) |
| |
| // ...and can simply start observing further updates |
| newList.addWeakCallback(pagedListCallback) |
| |
| if (!newList.isEmpty()) { |
| // Transform the last loadAround() index from the old list to the new list by passing it |
| // through the DiffResult. This ensures the lastKey of a positional PagedList is carried |
| // to new list even if no in-viewport item changes (AsyncPagedListDiffer#get not called) |
| // Note: we don't take into account loads between new list snapshot and new list, but |
| // this is only a problem in rare cases when placeholders are disabled, and a load |
| // starts (for some reason) and finishes before diff completes. |
| val newPosition = previousSnapshot.getNullPaddedList().transformAnchorIndex( |
| diffResult, |
| diffSnapshot.getNullPaddedList(), |
| lastAccessIndex |
| ) |
| |
| // Trigger load in new list at this position, clamped to list bounds. |
| // This is a load, not just an update of last load position, since the new list may be |
| // incomplete. If new list is subset of old list, but doesn't fill the viewport, this |
| // will likely trigger a load of new data. |
| newList.loadAround(newPosition.coerceIn(0, newList.size - 1)) |
| } |
| |
| onCurrentListChanged(previousSnapshot, pagedList, commitCallback) |
| } |
| |
| private fun onCurrentListChanged( |
| @Suppress("DEPRECATION") previousList: PagedList<T>?, |
| @Suppress("DEPRECATION") currentList: PagedList<T>?, |
| commitCallback: Runnable? |
| ) { |
| listeners.forEach { it.onCurrentListChanged(previousList, currentList) } |
| commitCallback?.run() |
| } |
| |
| /** |
| * Add a [PagedListListener] to receive updates when the current [PagedList] changes. |
| * |
| * @param listener Listener to receive updates. |
| * |
| * @see currentList |
| * @see removePagedListListener |
| */ |
| open fun addPagedListListener(@Suppress("DEPRECATION") listener: PagedListListener<T>) { |
| listeners.add(listener) |
| } |
| |
| /** |
| * Add a callback to receive updates when the current [PagedList] changes. |
| * |
| * @param callback to receive updates. |
| * |
| * @see currentList |
| * @see removePagedListListener |
| */ |
| fun addPagedListListener( |
| @Suppress("DEPRECATION") callback: (PagedList<T>?, PagedList<T>?) -> Unit |
| ) { |
| listeners.add(OnCurrentListChangedWrapper(callback)) |
| } |
| |
| /** |
| * Remove a previously registered [PagedListListener]. |
| * |
| * @param listener Previously registered listener. |
| * |
| * @see currentList |
| * @see addPagedListListener |
| */ |
| open fun removePagedListListener(@Suppress("DEPRECATION") listener: PagedListListener<T>) { |
| listeners.remove(listener) |
| } |
| |
| /** |
| * Remove a previously registered callback via [addPagedListListener]. |
| * |
| * @param callback Previously registered callback. |
| * |
| * @see currentList |
| * @see addPagedListListener |
| */ |
| fun removePagedListListener( |
| @Suppress("DEPRECATION") callback: (PagedList<T>?, PagedList<T>?) -> Unit |
| ) { |
| listeners.removeAll { it is OnCurrentListChangedWrapper<T> && it.callback === callback } |
| } |
| |
| /** |
| * Add a [LoadState] listener to observe the loading state of the current [PagedList]. |
| * |
| * As new PagedLists are submitted and displayed, the listener will be notified to reflect |
| * current [LoadType.REFRESH], [LoadType.START], and [LoadType.END] states. |
| * |
| * @param listener [LoadState] listener to receive updates. |
| * |
| * @see removeLoadStateListener |
| */ |
| open fun addLoadStateListener(listener: (LoadType, LoadState) -> Unit) { |
| val pagedList = this.pagedList |
| if (pagedList != null) { |
| pagedList.addWeakLoadStateListener(listener) |
| } else { |
| loadStateManager.dispatchCurrentLoadState(listener) |
| } |
| loadStateListeners.add(listener) |
| } |
| |
| /** |
| * Remove a previously registered [LoadState] listener. |
| * |
| * @param listener Previously registered listener. |
| * |
| * @see currentList |
| * @see addPagedListListener |
| */ |
| open fun removeLoadStateListener(listener: (LoadType, LoadState) -> Unit) { |
| loadStateListeners.remove(listener) |
| pagedList?.removeWeakLoadStateListener(listener) |
| } |
| } |