| /* |
| * Copyright 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 android.support.v7.recyclerview.extensions; |
| |
| import android.support.annotation.NonNull; |
| import android.support.v7.util.AdapterListUpdateCallback; |
| import android.support.v7.util.DiffUtil; |
| import android.support.v7.util.ListUpdateCallback; |
| import android.support.v7.widget.RecyclerView; |
| |
| import java.util.List; |
| |
| /** |
| * Helper object for displaying a List in |
| * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, which signals the |
| * adapter of changes when the List is changed by computing changes with DiffUtil in the |
| * background. |
| * <p> |
| * For simplicity, the {@link ListAdapter} wrapper class can often be used instead of the |
| * helper directly. This helper class is exposed for complex cases, and where overriding an adapter |
| * base class to support List diffing isn't convenient. |
| * <p> |
| * The ListAdapterHelper 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> |
| * It provides a simple list-like API with {@link #getItem(int)} and {@link #getItemCount()} for an |
| * adapter to acquire and present data objects. |
| * <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 = ViewModelProviders.of(this).get(MyViewModel.class); |
| * RecyclerView recyclerView = findViewById(R.id.user_list); |
| * UserAdapter<User> adapter = new UserAdapter(); |
| * viewModel.usersList.observe(this, list -> adapter.setList(list)); |
| * recyclerView.setAdapter(adapter); |
| * } |
| * } |
| * |
| * class UserAdapter extends RecyclerView.Adapter<UserViewHolder> { |
| * private final ListAdapterHelper<User> mHelper = |
| * new ListAdapterHelper(this, DIFF_CALLBACK); |
| * {@literal @}Override |
| * public int getItemCount() { |
| * return mHelper.getItemCount(); |
| * } |
| * public void setList(List<User> list) { |
| * mHelper.setList(list); |
| * } |
| * {@literal @}Override |
| * public void onBindViewHolder(UserViewHolder holder, int position) { |
| * User user = mHelper.getItem(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 helper will receive. |
| */ |
| public class ListAdapterHelper<T> { |
| private final ListUpdateCallback mUpdateCallback; |
| private final ListAdapterConfig<T> mConfig; |
| |
| /** |
| * Convenience for |
| * {@code PagedListAdapterHelper(new AdapterListUpdateCallback(adapter), |
| * new ListAdapterConfig.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 ListAdapterHelper(@NonNull RecyclerView.Adapter adapter, |
| @NonNull DiffUtil.ItemCallback<T> diffCallback) { |
| mUpdateCallback = new AdapterListUpdateCallback(adapter); |
| mConfig = new ListAdapterConfig.Builder<>(diffCallback).build(); |
| } |
| |
| /** |
| * Create a ListAdapterHelper 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 ListAdapterHelper(@NonNull ListUpdateCallback listUpdateCallback, |
| @NonNull ListAdapterConfig<T> config) { |
| mUpdateCallback = listUpdateCallback; |
| mConfig = config; |
| } |
| |
| private List<T> mList; |
| |
| // Max generation of currently scheduled runnable |
| private int mMaxScheduledGeneration; |
| |
| |
| /** |
| * Get the item from the current List at the specified index. |
| * |
| * @param index Index of item to get, must be >= 0, and < {@link #getItemCount()}. |
| * @return The item at the specified List position. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public T getItem(int index) { |
| if (mList == null) { |
| throw new IndexOutOfBoundsException("Item count is zero, getItem() call is invalid"); |
| } |
| |
| return mList.get(index); |
| } |
| |
| /** |
| * Get the number of items currently presented by this AdapterHelper. This value can be directly |
| * returned to {@link android.support.v7.widget.RecyclerView.Adapter#getItemCount() |
| * RecyclerView.Adapter.getItemCount()}. |
| * |
| * @return Number of items being presented. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public int getItemCount() { |
| return mList == null ? 0 : mList.size(); |
| } |
| |
| /** |
| * 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 setList(final List<T> newList) { |
| if (newList == mList) { |
| // nothing to do |
| return; |
| } |
| |
| // incrementing generation means any currently-running diffs are discarded when they finish |
| final int runGeneration = ++mMaxScheduledGeneration; |
| |
| if (newList == null) { |
| mUpdateCallback.onRemoved(0, mList.size()); |
| mList = null; |
| return; |
| } |
| |
| if (mList == null) { |
| // fast simple first insert |
| mUpdateCallback.onInserted(0, newList.size()); |
| mList = newList; |
| 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) { |
| return mConfig.getDiffCallback().areItemsTheSame( |
| oldList.get(oldItemPosition), newList.get(newItemPosition)); |
| } |
| |
| @Override |
| public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { |
| return mConfig.getDiffCallback().areContentsTheSame( |
| oldList.get(oldItemPosition), newList.get(newItemPosition)); |
| } |
| }); |
| |
| mConfig.getMainThreadExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| if (mMaxScheduledGeneration == runGeneration) { |
| latchList(newList, result); |
| } |
| } |
| }); |
| } |
| }); |
| } |
| |
| private void latchList(List<T> newList, DiffUtil.DiffResult diffResult) { |
| diffResult.dispatchUpdatesTo(mUpdateCallback); |
| mList = newList; |
| } |
| } |