1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.recyclerview.widget; 18 19 import android.os.Handler; 20 import android.os.Looper; 21 22 import org.jspecify.annotations.NonNull; 23 import org.jspecify.annotations.Nullable; 24 25 import java.util.Collections; 26 import java.util.List; 27 import java.util.concurrent.CopyOnWriteArrayList; 28 import java.util.concurrent.Executor; 29 30 /** 31 * Helper for computing the difference between two lists via {@link DiffUtil} on a background 32 * thread. 33 * <p> 34 * It can be connected to a 35 * {@link RecyclerView.Adapter RecyclerView.Adapter}, and will signal the 36 * adapter of changes between sumbitted lists. 37 * <p> 38 * For simplicity, the {@link ListAdapter} wrapper class can often be used instead of the 39 * AsyncListDiffer directly. This AsyncListDiffer can be used for complex cases, where overriding an 40 * adapter base class to support asynchronous List diffing isn't convenient. 41 * <p> 42 * The AsyncListDiffer can consume the values from a LiveData of <code>List</code> and present the 43 * data simply for an adapter. It computes differences in list contents via {@link DiffUtil} on a 44 * background thread as new <code>List</code>s are received. 45 * <p> 46 * Use {@link #getCurrentList()} to access the current List, and present its data objects. Diff 47 * results will be dispatched to the ListUpdateCallback immediately before the current list is 48 * updated. If you're dispatching list updates directly to an Adapter, this means the Adapter can 49 * safely access list items and total size via {@link #getCurrentList()}. 50 * <p> 51 * A complete usage pattern with Room would look like this: 52 * <pre> 53 * {@literal @}Dao 54 * interface UserDao { 55 * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC") 56 * public abstract LiveData<List<User>> usersByLastName(); 57 * } 58 * 59 * class MyViewModel extends ViewModel { 60 * public final LiveData<List<User>> usersList; 61 * public MyViewModel(UserDao userDao) { 62 * usersList = userDao.usersByLastName(); 63 * } 64 * } 65 * 66 * class MyActivity extends AppCompatActivity { 67 * {@literal @}Override 68 * public void onCreate(Bundle savedState) { 69 * super.onCreate(savedState); 70 * MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class); 71 * RecyclerView recyclerView = findViewById(R.id.user_list); 72 * UserAdapter adapter = new UserAdapter(); 73 * viewModel.usersList.observe(this, list -> adapter.submitList(list)); 74 * recyclerView.setAdapter(adapter); 75 * } 76 * } 77 * 78 * class UserAdapter extends RecyclerView.Adapter<UserViewHolder> { 79 * private final AsyncListDiffer<User> mDiffer = new AsyncListDiffer(this, DIFF_CALLBACK); 80 * {@literal @}Override 81 * public int getItemCount() { 82 * return mDiffer.getCurrentList().size(); 83 * } 84 * public void submitList(List<User> list) { 85 * mDiffer.submitList(list); 86 * } 87 * {@literal @}Override 88 * public void onBindViewHolder(UserViewHolder holder, int position) { 89 * User user = mDiffer.getCurrentList().get(position); 90 * holder.bindTo(user); 91 * } 92 * public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK 93 * = new DiffUtil.ItemCallback<User>() { 94 * {@literal @}Override 95 * public boolean areItemsTheSame( 96 * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { 97 * // User properties may have changed if reloaded from the DB, but ID is fixed 98 * return oldUser.getId() == newUser.getId(); 99 * } 100 * {@literal @}Override 101 * public boolean areContentsTheSame( 102 * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { 103 * // NOTE: if you use equals, your object must properly override Object#equals() 104 * // Incorrectly returning false here will result in too many animations. 105 * return oldUser.equals(newUser); 106 * } 107 * } 108 * }</pre> 109 * 110 * @param <T> Type of the lists this AsyncListDiffer will receive. 111 * 112 * @see DiffUtil 113 * @see AdapterListUpdateCallback 114 */ 115 public class AsyncListDiffer<T> { 116 private final ListUpdateCallback mUpdateCallback; 117 @SuppressWarnings("WeakerAccess") /* synthetic access */ 118 final AsyncDifferConfig<T> mConfig; 119 Executor mMainThreadExecutor; 120 121 private static class MainThreadExecutor implements Executor { 122 final Handler mHandler = new Handler(Looper.getMainLooper()); MainThreadExecutor()123 MainThreadExecutor() {} 124 @Override execute(@onNull Runnable command)125 public void execute(@NonNull Runnable command) { 126 mHandler.post(command); 127 } 128 } 129 130 // TODO: use MainThreadExecutor from supportlib once one exists 131 private static final Executor sMainThreadExecutor = new MainThreadExecutor(); 132 133 /** 134 * Listener for when the current List is updated. 135 * 136 * @param <T> Type of items in List 137 */ 138 public interface ListListener<T> { 139 /** 140 * Called after the current List has been updated. 141 * 142 * @param previousList The previous list. 143 * @param currentList The new current list. 144 */ onCurrentListChanged(@onNull List<T> previousList, @NonNull List<T> currentList)145 void onCurrentListChanged(@NonNull List<T> previousList, @NonNull List<T> currentList); 146 } 147 148 private final List<ListListener<T>> mListeners = new CopyOnWriteArrayList<>(); 149 150 /** 151 * Convenience for 152 * {@code AsyncListDiffer(new AdapterListUpdateCallback(adapter), 153 * new AsyncDifferConfig.Builder().setDiffCallback(diffCallback).build());} 154 * 155 * @param adapter Adapter to dispatch position updates to. 156 * @param diffCallback ItemCallback that compares items to dispatch appropriate animations when 157 * 158 * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter) 159 */ AsyncListDiffer(RecyclerView.@onNull Adapter adapter, DiffUtil.@NonNull ItemCallback<T> diffCallback)160 public AsyncListDiffer(RecyclerView.@NonNull Adapter adapter, 161 DiffUtil.@NonNull ItemCallback<T> diffCallback) { 162 this(new AdapterListUpdateCallback(adapter), 163 new AsyncDifferConfig.Builder<>(diffCallback).build()); 164 } 165 166 /** 167 * Create a AsyncListDiffer with the provided config, and ListUpdateCallback to dispatch 168 * updates to. 169 * 170 * @param listUpdateCallback Callback to dispatch updates to. 171 * @param config Config to define background work Executor, and DiffUtil.ItemCallback for 172 * computing List diffs. 173 * 174 * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter) 175 */ 176 @SuppressWarnings("WeakerAccess") AsyncListDiffer(@onNull ListUpdateCallback listUpdateCallback, @NonNull AsyncDifferConfig<T> config)177 public AsyncListDiffer(@NonNull ListUpdateCallback listUpdateCallback, 178 @NonNull AsyncDifferConfig<T> config) { 179 mUpdateCallback = listUpdateCallback; 180 mConfig = config; 181 if (config.getMainThreadExecutor() != null) { 182 mMainThreadExecutor = config.getMainThreadExecutor(); 183 } else { 184 mMainThreadExecutor = sMainThreadExecutor; 185 } 186 } 187 188 private @Nullable List<T> mList; 189 190 /** 191 * Non-null, unmodifiable version of mList. 192 * <p> 193 * Collections.emptyList when mList is null, wrapped by Collections.unmodifiableList otherwise 194 */ 195 private @NonNull List<T> mReadOnlyList = Collections.emptyList(); 196 197 // Max generation of currently scheduled runnable 198 @SuppressWarnings("WeakerAccess") /* synthetic access */ 199 int mMaxScheduledGeneration; 200 201 /** 202 * Get the current List - any diffing to present this list has already been computed and 203 * dispatched via the ListUpdateCallback. 204 * <p> 205 * If a <code>null</code> List, or no List has been submitted, an empty list will be returned. 206 * <p> 207 * The returned list may not be mutated - mutations to content must be done through 208 * {@link #submitList(List)}. 209 * 210 * @return current List. 211 */ getCurrentList()212 public @NonNull List<T> getCurrentList() { 213 return mReadOnlyList; 214 } 215 216 /** 217 * Pass a new List to the AdapterHelper. Adapter updates will be computed on a background 218 * thread. 219 * <p> 220 * If a List is already present, a diff will be computed asynchronously on a background thread. 221 * When the diff is computed, it will be applied (dispatched to the {@link ListUpdateCallback}), 222 * and the new List will be swapped in. 223 * 224 * @param newList The new List. 225 */ 226 @SuppressWarnings("WeakerAccess") submitList(final @Nullable List<T> newList)227 public void submitList(final @Nullable List<T> newList) { 228 submitList(newList, null); 229 } 230 231 /** 232 * Pass a new List to the AdapterHelper. Adapter updates will be computed on a background 233 * thread. 234 * <p> 235 * If a List is already present, a diff will be computed asynchronously on a background thread. 236 * When the diff is computed, it will be applied (dispatched to the {@link ListUpdateCallback}), 237 * and the new List will be swapped in. 238 * <p> 239 * The commit callback can be used to know when the List is committed, but note that it 240 * may not be executed. If List B is submitted immediately after List A, and is 241 * committed directly, the callback associated with List A will not be run. 242 * 243 * @param newList The new List. 244 * @param commitCallback Optional runnable that is executed when the List is committed, if 245 * it is committed. 246 */ 247 @SuppressWarnings("WeakerAccess") submitList(final @Nullable List<T> newList, final @Nullable Runnable commitCallback)248 public void submitList(final @Nullable List<T> newList, 249 final @Nullable Runnable commitCallback) { 250 // incrementing generation means any currently-running diffs are discarded when they finish 251 final int runGeneration = ++mMaxScheduledGeneration; 252 253 if (newList == mList) { 254 // nothing to do (Note - still had to inc generation, since may have ongoing work) 255 if (commitCallback != null) { 256 commitCallback.run(); 257 } 258 return; 259 } 260 261 final List<T> previousList = mReadOnlyList; 262 263 // fast simple remove all 264 if (newList == null) { 265 //noinspection ConstantConditions 266 int countRemoved = mList.size(); 267 mList = null; 268 mReadOnlyList = Collections.emptyList(); 269 // notify last, after list is updated 270 mUpdateCallback.onRemoved(0, countRemoved); 271 onCurrentListChanged(previousList, commitCallback); 272 return; 273 } 274 275 // fast simple first insert 276 if (mList == null) { 277 mList = newList; 278 mReadOnlyList = Collections.unmodifiableList(newList); 279 // notify last, after list is updated 280 mUpdateCallback.onInserted(0, newList.size()); 281 onCurrentListChanged(previousList, commitCallback); 282 return; 283 } 284 285 final List<T> oldList = mList; 286 mConfig.getBackgroundThreadExecutor().execute(new Runnable() { 287 @Override 288 public void run() { 289 final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { 290 @Override 291 public int getOldListSize() { 292 return oldList.size(); 293 } 294 295 @Override 296 public int getNewListSize() { 297 return newList.size(); 298 } 299 300 @Override 301 public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { 302 T oldItem = oldList.get(oldItemPosition); 303 T newItem = newList.get(newItemPosition); 304 if (oldItem != null && newItem != null) { 305 return mConfig.getDiffCallback().areItemsTheSame(oldItem, newItem); 306 } 307 // If both items are null we consider them the same. 308 return oldItem == null && newItem == null; 309 } 310 311 @Override 312 public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { 313 T oldItem = oldList.get(oldItemPosition); 314 T newItem = newList.get(newItemPosition); 315 if (oldItem != null && newItem != null) { 316 return mConfig.getDiffCallback().areContentsTheSame(oldItem, newItem); 317 } 318 if (oldItem == null && newItem == null) { 319 return true; 320 } 321 // There is an implementation bug if we reach this point. Per the docs, this 322 // method should only be invoked when areItemsTheSame returns true. That 323 // only occurs when both items are non-null or both are null and both of 324 // those cases are handled above. 325 throw new AssertionError(); 326 } 327 328 @Override 329 public @Nullable Object getChangePayload(int oldItemPosition, 330 int newItemPosition) { 331 T oldItem = oldList.get(oldItemPosition); 332 T newItem = newList.get(newItemPosition); 333 if (oldItem != null && newItem != null) { 334 return mConfig.getDiffCallback().getChangePayload(oldItem, newItem); 335 } 336 // There is an implementation bug if we reach this point. Per the docs, this 337 // method should only be invoked when areItemsTheSame returns true AND 338 // areContentsTheSame returns false. That only occurs when both items are 339 // non-null which is the only case handled above. 340 throw new AssertionError(); 341 } 342 }); 343 344 mMainThreadExecutor.execute(new Runnable() { 345 @Override 346 public void run() { 347 if (mMaxScheduledGeneration == runGeneration) { 348 latchList(newList, result, commitCallback); 349 } 350 } 351 }); 352 } 353 }); 354 } 355 356 @SuppressWarnings("WeakerAccess") /* synthetic access */ latchList( @onNull List<T> newList, DiffUtil.@NonNull DiffResult diffResult, @Nullable Runnable commitCallback)357 void latchList( 358 @NonNull List<T> newList, 359 DiffUtil.@NonNull DiffResult diffResult, 360 @Nullable Runnable commitCallback) { 361 final List<T> previousList = mReadOnlyList; 362 mList = newList; 363 // notify last, after list is updated 364 mReadOnlyList = Collections.unmodifiableList(newList); 365 diffResult.dispatchUpdatesTo(mUpdateCallback); 366 onCurrentListChanged(previousList, commitCallback); 367 } 368 onCurrentListChanged(@onNull List<T> previousList, @Nullable Runnable commitCallback)369 private void onCurrentListChanged(@NonNull List<T> previousList, 370 @Nullable Runnable commitCallback) { 371 // current list is always mReadOnlyList 372 for (ListListener<T> listener : mListeners) { 373 listener.onCurrentListChanged(previousList, mReadOnlyList); 374 } 375 if (commitCallback != null) { 376 commitCallback.run(); 377 } 378 } 379 380 /** 381 * Add a ListListener to receive updates when the current List changes. 382 * 383 * @param listener Listener to receive updates. 384 * 385 * @see #getCurrentList() 386 * @see #removeListListener(ListListener) 387 */ addListListener(@onNull ListListener<T> listener)388 public void addListListener(@NonNull ListListener<T> listener) { 389 mListeners.add(listener); 390 } 391 392 /** 393 * Remove a previously registered ListListener. 394 * 395 * @param listener Previously registered listener. 396 * @see #getCurrentList() 397 * @see #addListListener(ListListener) 398 */ removeListListener(@onNull ListListener<T> listener)399 public void removeListListener(@NonNull ListListener<T> listener) { 400 mListeners.remove(listener); 401 } 402 } 403