| /* |
| * Copyright 2018 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.recyclerview.widget; |
| |
| import android.os.Handler; |
| import android.os.Looper; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.CopyOnWriteArrayList; |
| import java.util.concurrent.Executor; |
| |
| /** |
| * Helper for computing the difference between two lists via {@link DiffUtil} on a background |
| * thread. |
| * <p> |
| * It can be connected to a |
| * {@link RecyclerView.Adapter RecyclerView.Adapter}, and will signal the |
| * adapter of changes between sumbitted lists. |
| * <p> |
| * For simplicity, the {@link ListAdapter} wrapper class can often be used instead of the |
| * AsyncListDiffer directly. This AsyncListDiffer can be used for complex cases, where overriding an |
| * adapter base class to support asynchronous List diffing isn't convenient. |
| * <p> |
| * The AsyncListDiffer can consume the values from a LiveData of <code>List</code> and present the |
| * data simply for an adapter. It computes differences in list contents via {@link DiffUtil} on a |
| * background thread as new <code>List</code>s are received. |
| * <p> |
| * Use {@link #getCurrentList()} to access the current List, and present its data objects. Diff |
| * results will be dispatched to the ListUpdateCallback immediately before the current list is |
| * updated. If you're dispatching list updates directly to an Adapter, this means the Adapter can |
| * safely access list items and total size via {@link #getCurrentList()}. |
| * <p> |
| * A complete usage pattern with Room would look like this: |
| * <pre> |
| * {@literal @}Dao |
| * interface UserDao { |
| * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC") |
| * public abstract LiveData<List<User>> usersByLastName(); |
| * } |
| * |
| * class MyViewModel extends ViewModel { |
| * public final LiveData<List<User>> usersList; |
| * public MyViewModel(UserDao userDao) { |
| * usersList = userDao.usersByLastName(); |
| * } |
| * } |
| * |
| * class MyActivity extends AppCompatActivity { |
| * {@literal @}Override |
| * public void onCreate(Bundle savedState) { |
| * super.onCreate(savedState); |
| * MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class); |
| * RecyclerView recyclerView = findViewById(R.id.user_list); |
| * UserAdapter adapter = new UserAdapter(); |
| * viewModel.usersList.observe(this, list -> adapter.submitList(list)); |
| * recyclerView.setAdapter(adapter); |
| * } |
| * } |
| * |
| * class UserAdapter extends RecyclerView.Adapter<UserViewHolder> { |
| * private final AsyncListDiffer<User> mDiffer = new AsyncListDiffer(this, DIFF_CALLBACK); |
| * {@literal @}Override |
| * public int getItemCount() { |
| * return mDiffer.getCurrentList().size(); |
| * } |
| * public void submitList(List<User> list) { |
| * mDiffer.submitList(list); |
| * } |
| * {@literal @}Override |
| * public void onBindViewHolder(UserViewHolder holder, int position) { |
| * User user = mDiffer.getCurrentList().get(position); |
| * holder.bindTo(user); |
| * } |
| * public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK |
| * = new DiffUtil.ItemCallback<User>() { |
| * {@literal @}Override |
| * public boolean areItemsTheSame( |
| * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { |
| * // User properties may have changed if reloaded from the DB, but ID is fixed |
| * return oldUser.getId() == newUser.getId(); |
| * } |
| * {@literal @}Override |
| * public boolean areContentsTheSame( |
| * {@literal @}NonNull User oldUser, {@literal @}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); |
| * } |
| * } |
| * }</pre> |
| * |
| * @param <T> Type of the lists this AsyncListDiffer will receive. |
| * |
| * @see DiffUtil |
| * @see AdapterListUpdateCallback |
| */ |
| public class AsyncListDiffer<T> { |
| private final ListUpdateCallback mUpdateCallback; |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| final AsyncDifferConfig<T> mConfig; |
| Executor mMainThreadExecutor; |
| |
| private static class MainThreadExecutor implements Executor { |
| final Handler mHandler = new Handler(Looper.getMainLooper()); |
| MainThreadExecutor() {} |
| @Override |
| public void execute(@NonNull Runnable command) { |
| mHandler.post(command); |
| } |
| } |
| |
| // TODO: use MainThreadExecutor from supportlib once one exists |
| private static final Executor sMainThreadExecutor = new MainThreadExecutor(); |
| |
| /** |
| * Listener for when the current List is updated. |
| * |
| * @param <T> Type of items in List |
| */ |
| public interface ListListener<T> { |
| /** |
| * Called after the current List has been updated. |
| * |
| * @param previousList The previous list. |
| * @param currentList The new current list. |
| */ |
| void onCurrentListChanged(@NonNull List<T> previousList, @NonNull List<T> currentList); |
| } |
| |
| private final List<ListListener<T>> mListeners = new CopyOnWriteArrayList<>(); |
| |
| /** |
| * Convenience for |
| * {@code AsyncListDiffer(new AdapterListUpdateCallback(adapter), |
| * new AsyncDifferConfig.Builder().setDiffCallback(diffCallback).build());} |
| * |
| * @param adapter Adapter to dispatch position updates to. |
| * @param diffCallback ItemCallback that compares items to dispatch appropriate animations when |
| * |
| * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter) |
| */ |
| public AsyncListDiffer(@NonNull RecyclerView.Adapter adapter, |
| @NonNull DiffUtil.ItemCallback<T> diffCallback) { |
| this(new AdapterListUpdateCallback(adapter), |
| new AsyncDifferConfig.Builder<>(diffCallback).build()); |
| } |
| |
| /** |
| * Create a AsyncListDiffer with the provided config, and ListUpdateCallback to dispatch |
| * updates to. |
| * |
| * @param listUpdateCallback Callback to dispatch updates to. |
| * @param config Config to define background work Executor, and DiffUtil.ItemCallback for |
| * computing List diffs. |
| * |
| * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter) |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public AsyncListDiffer(@NonNull ListUpdateCallback listUpdateCallback, |
| @NonNull AsyncDifferConfig<T> config) { |
| mUpdateCallback = listUpdateCallback; |
| mConfig = config; |
| if (config.getMainThreadExecutor() != null) { |
| mMainThreadExecutor = config.getMainThreadExecutor(); |
| } else { |
| mMainThreadExecutor = sMainThreadExecutor; |
| } |
| } |
| |
| @Nullable |
| private List<T> mList; |
| |
| /** |
| * Non-null, unmodifiable version of mList. |
| * <p> |
| * Collections.emptyList when mList is null, wrapped by Collections.unmodifiableList otherwise |
| */ |
| @NonNull |
| private List<T> mReadOnlyList = Collections.emptyList(); |
| |
| // Max generation of currently scheduled runnable |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| int mMaxScheduledGeneration; |
| |
| /** |
| * Get the current List - any diffing to present this list has already been computed and |
| * dispatched via the ListUpdateCallback. |
| * <p> |
| * If a <code>null</code> List, or no List has been submitted, an empty list will be returned. |
| * <p> |
| * The returned list may not be mutated - mutations to content must be done through |
| * {@link #submitList(List)}. |
| * |
| * @return current List. |
| */ |
| @NonNull |
| public List<T> getCurrentList() { |
| return mReadOnlyList; |
| } |
| |
| /** |
| * Pass a new List to the AdapterHelper. Adapter updates will be computed on a background |
| * thread. |
| * <p> |
| * If a List 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 {@link ListUpdateCallback}), |
| * and the new List will be swapped in. |
| * |
| * @param newList The new List. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public void submitList(@Nullable final List<T> newList) { |
| submitList(newList, null); |
| } |
| |
| /** |
| * Pass a new List to the AdapterHelper. Adapter updates will be computed on a background |
| * thread. |
| * <p> |
| * If a List 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 {@link ListUpdateCallback}), |
| * and the new List will be swapped in. |
| * <p> |
| * The commit callback can be used to know when the List is committed, but note that it |
| * may not be executed. If List B is submitted immediately after List A, and is |
| * committed directly, the callback associated with List A will not be run. |
| * |
| * @param newList The new List. |
| * @param commitCallback Optional runnable that is executed when the List is committed, if |
| * it is committed. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public void submitList(@Nullable final List<T> newList, |
| @Nullable final Runnable commitCallback) { |
| // incrementing generation means any currently-running diffs are discarded when they finish |
| final int runGeneration = ++mMaxScheduledGeneration; |
| |
| if (newList == mList) { |
| // nothing to do (Note - still had to inc generation, since may have ongoing work) |
| if (commitCallback != null) { |
| commitCallback.run(); |
| } |
| return; |
| } |
| |
| final List<T> previousList = mReadOnlyList; |
| |
| // fast simple remove all |
| if (newList == null) { |
| //noinspection ConstantConditions |
| int countRemoved = mList.size(); |
| mList = null; |
| mReadOnlyList = Collections.emptyList(); |
| // notify last, after list is updated |
| mUpdateCallback.onRemoved(0, countRemoved); |
| onCurrentListChanged(previousList, commitCallback); |
| return; |
| } |
| |
| // fast simple first insert |
| if (mList == null) { |
| mList = newList; |
| mReadOnlyList = Collections.unmodifiableList(newList); |
| // notify last, after list is updated |
| mUpdateCallback.onInserted(0, newList.size()); |
| onCurrentListChanged(previousList, commitCallback); |
| return; |
| } |
| |
| final List<T> oldList = mList; |
| mConfig.getBackgroundThreadExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { |
| @Override |
| public int getOldListSize() { |
| return oldList.size(); |
| } |
| |
| @Override |
| public int getNewListSize() { |
| return newList.size(); |
| } |
| |
| @Override |
| public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { |
| T oldItem = oldList.get(oldItemPosition); |
| T newItem = newList.get(newItemPosition); |
| if (oldItem != null && newItem != null) { |
| return mConfig.getDiffCallback().areItemsTheSame(oldItem, newItem); |
| } |
| // If both items are null we consider them the same. |
| return oldItem == null && newItem == null; |
| } |
| |
| @Override |
| public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { |
| T oldItem = oldList.get(oldItemPosition); |
| T newItem = newList.get(newItemPosition); |
| if (oldItem != null && newItem != null) { |
| return mConfig.getDiffCallback().areContentsTheSame(oldItem, newItem); |
| } |
| if (oldItem == null && newItem == null) { |
| return true; |
| } |
| // There is an implementation bug if we reach this point. Per the docs, this |
| // method should only be invoked when areItemsTheSame returns true. That |
| // only occurs when both items are non-null or both are null and both of |
| // those cases are handled above. |
| throw new AssertionError(); |
| } |
| |
| @Nullable |
| @Override |
| public Object getChangePayload(int oldItemPosition, int newItemPosition) { |
| T oldItem = oldList.get(oldItemPosition); |
| T newItem = newList.get(newItemPosition); |
| if (oldItem != null && newItem != null) { |
| return mConfig.getDiffCallback().getChangePayload(oldItem, newItem); |
| } |
| // There is an implementation bug if we reach this point. Per the docs, this |
| // method should only be invoked when areItemsTheSame returns true AND |
| // areContentsTheSame returns false. That only occurs when both items are |
| // non-null which is the only case handled above. |
| throw new AssertionError(); |
| } |
| }); |
| |
| mMainThreadExecutor.execute(new Runnable() { |
| @Override |
| public void run() { |
| if (mMaxScheduledGeneration == runGeneration) { |
| latchList(newList, result, commitCallback); |
| } |
| } |
| }); |
| } |
| }); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void latchList( |
| @NonNull List<T> newList, |
| @NonNull DiffUtil.DiffResult diffResult, |
| @Nullable Runnable commitCallback) { |
| final List<T> previousList = mReadOnlyList; |
| mList = newList; |
| // notify last, after list is updated |
| mReadOnlyList = Collections.unmodifiableList(newList); |
| diffResult.dispatchUpdatesTo(mUpdateCallback); |
| onCurrentListChanged(previousList, commitCallback); |
| } |
| |
| private void onCurrentListChanged(@NonNull List<T> previousList, |
| @Nullable Runnable commitCallback) { |
| // current list is always mReadOnlyList |
| for (ListListener<T> listener : mListeners) { |
| listener.onCurrentListChanged(previousList, mReadOnlyList); |
| } |
| if (commitCallback != null) { |
| commitCallback.run(); |
| } |
| } |
| |
| /** |
| * Add a ListListener to receive updates when the current List changes. |
| * |
| * @param listener Listener to receive updates. |
| * |
| * @see #getCurrentList() |
| * @see #removeListListener(ListListener) |
| */ |
| public void addListListener(@NonNull ListListener<T> listener) { |
| mListeners.add(listener); |
| } |
| |
| /** |
| * Remove a previously registered ListListener. |
| * |
| * @param listener Previously registered listener. |
| * @see #getCurrentList() |
| * @see #addListListener(ListListener) |
| */ |
| public void removeListListener(@NonNull ListListener<T> listener) { |
| mListeners.remove(listener); |
| } |
| } |