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&lt;List&lt;User>> usersByLastName();
57  * }
58  *
59  * class MyViewModel extends ViewModel {
60  *     public final LiveData&lt;List&lt;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&lt;UserViewHolder> {
79  *     private final AsyncListDiffer&lt;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&lt;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&lt;User> DIFF_CALLBACK
93  *             = new DiffUtil.ItemCallback&lt;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