1/*
2 * Copyright (C) 2017 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
17package androidx.paging;
18
19import androidx.annotation.NonNull;
20import androidx.annotation.Nullable;
21import androidx.arch.core.executor.ArchTaskExecutor;
22import androidx.lifecycle.LiveData;
23import androidx.recyclerview.widget.AdapterListUpdateCallback;
24import androidx.recyclerview.widget.AsyncDifferConfig;
25import androidx.recyclerview.widget.DiffUtil;
26import androidx.recyclerview.widget.ListUpdateCallback;
27import androidx.recyclerview.widget.RecyclerView;
28
29import java.util.concurrent.Executor;
30
31/**
32 * Helper object for mapping a {@link PagedList} into a
33 * {@link androidx.recyclerview.widget.RecyclerView.Adapter RecyclerView.Adapter}.
34 * <p>
35 * For simplicity, the {@link PagedListAdapter} wrapper class can often be used instead of the
36 * differ directly. This diff class is exposed for complex cases, and where overriding an adapter
37 * base class to support paging isn't convenient.
38 * <p>
39 * When consuming a {@link LiveData} of PagedList, you can observe updates and dispatch them
40 * directly to {@link #submitList(PagedList)}. The AsyncPagedListDiffer then can present this
41 * updating data set simply for an adapter. It listens to PagedList loading callbacks, and uses
42 * DiffUtil on a background thread to compute updates as new PagedLists are received.
43 * <p>
44 * It provides a simple list-like API with {@link #getItem(int)} and {@link #getItemCount()} for an
45 * adapter to acquire and present data objects.
46 * <p>
47 * A complete usage pattern with Room would look like this:
48 * <pre>
49 * {@literal @}Dao
50 * interface UserDao {
51 *     {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
52 *     public abstract DataSource.Factory&lt;Integer, User> usersByLastName();
53 * }
54 *
55 * class MyViewModel extends ViewModel {
56 *     public final LiveData&lt;PagedList&lt;User>> usersList;
57 *     public MyViewModel(UserDao userDao) {
58 *         usersList = new LivePagedListBuilder&lt;>(
59 *                 userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
60 *     }
61 * }
62 *
63 * class MyActivity extends AppCompatActivity {
64 *     {@literal @}Override
65 *     public void onCreate(Bundle savedState) {
66 *         super.onCreate(savedState);
67 *         MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
68 *         RecyclerView recyclerView = findViewById(R.id.user_list);
69 *         final UserAdapter adapter = new UserAdapter();
70 *         viewModel.usersList.observe(this, pagedList -> adapter.submitList(pagedList));
71 *         recyclerView.setAdapter(adapter);
72 *     }
73 * }
74 *
75 * class UserAdapter extends RecyclerView.Adapter&lt;UserViewHolder> {
76 *     private final AsyncPagedListDiffer&lt;User> mDiffer
77 *             = new AsyncPagedListDiffer(this, DIFF_CALLBACK);
78 *     {@literal @}Override
79 *     public int getItemCount() {
80 *         return mDiffer.getItemCount();
81 *     }
82 *     public void submitList(PagedList&lt;User> pagedList) {
83 *         mDiffer.submitList(pagedList);
84 *     }
85 *     {@literal @}Override
86 *     public void onBindViewHolder(UserViewHolder holder, int position) {
87 *         User user = mDiffer.getItem(position);
88 *         if (user != null) {
89 *             holder.bindTo(user);
90 *         } else {
91 *             // Null defines a placeholder item - AsyncPagedListDiffer will automatically
92 *             // invalidate this row when the actual object is loaded from the database
93 *             holder.clear();
94 *         }
95 *     }
96 *     public static final DiffUtil.ItemCallback&lt;User> DIFF_CALLBACK =
97 *             new DiffUtil.ItemCallback&lt;User>() {
98 *          {@literal @}Override
99 *          public boolean areItemsTheSame(
100 *                  {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
101 *              // User properties may have changed if reloaded from the DB, but ID is fixed
102 *              return oldUser.getId() == newUser.getId();
103 *          }
104 *          {@literal @}Override
105 *          public boolean areContentsTheSame(
106 *                  {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
107 *              // NOTE: if you use equals, your object must properly override Object#equals()
108 *              // Incorrectly returning false here will result in too many animations.
109 *              return oldUser.equals(newUser);
110 *          }
111 *      }
112 * }</pre>
113 *
114 * @param <T> Type of the PagedLists this differ will receive.
115 */
116public class AsyncPagedListDiffer<T> {
117    // updateCallback notifications must only be notified *after* new data and item count are stored
118    // this ensures Adapter#notifyItemRangeInserted etc are accessing the new data
119    private final ListUpdateCallback mUpdateCallback;
120    private final AsyncDifferConfig<T> mConfig;
121
122    @SuppressWarnings("RestrictedApi")
123    Executor mMainThreadExecutor = ArchTaskExecutor.getMainThreadExecutor();
124
125    // TODO: REAL API
126    interface PagedListListener<T> {
127        void onCurrentListChanged(@Nullable PagedList<T> currentList);
128    }
129
130    @Nullable
131    PagedListListener<T> mListener;
132
133    private boolean mIsContiguous;
134
135    private PagedList<T> mPagedList;
136    private PagedList<T> mSnapshot;
137
138    // Max generation of currently scheduled runnable
139    private int mMaxScheduledGeneration;
140
141    /**
142     * Convenience for {@code AsyncPagedListDiffer(new AdapterListUpdateCallback(adapter),
143     * new AsyncDifferConfig.Builder<T>(diffCallback).build();}
144     *
145     * @param adapter Adapter that will receive update signals.
146     * @param diffCallback The {@link DiffUtil.ItemCallback DiffUtil.ItemCallback} instance to
147     * compare items in the list.
148     */
149    @SuppressWarnings("WeakerAccess")
150    public AsyncPagedListDiffer(@NonNull RecyclerView.Adapter adapter,
151            @NonNull DiffUtil.ItemCallback<T> diffCallback) {
152        mUpdateCallback = new AdapterListUpdateCallback(adapter);
153        mConfig = new AsyncDifferConfig.Builder<>(diffCallback).build();
154    }
155
156    @SuppressWarnings("WeakerAccess")
157    public AsyncPagedListDiffer(@NonNull ListUpdateCallback listUpdateCallback,
158            @NonNull AsyncDifferConfig<T> config) {
159        mUpdateCallback = listUpdateCallback;
160        mConfig = config;
161    }
162
163    private PagedList.Callback mPagedListCallback = new PagedList.Callback() {
164        @Override
165        public void onInserted(int position, int count) {
166            mUpdateCallback.onInserted(position, count);
167        }
168
169        @Override
170        public void onRemoved(int position, int count) {
171            mUpdateCallback.onRemoved(position, count);
172        }
173
174        @Override
175        public void onChanged(int position, int count) {
176            // NOTE: pass a null payload to convey null -> item
177            mUpdateCallback.onChanged(position, count, null);
178        }
179    };
180
181    /**
182     * Get the item from the current PagedList at the specified index.
183     * <p>
184     * Note that this operates on both loaded items and null padding within the PagedList.
185     *
186     * @param index Index of item to get, must be >= 0, and &lt; {@link #getItemCount()}.
187     * @return The item, or null, if a null placeholder is at the specified position.
188     */
189    @SuppressWarnings("WeakerAccess")
190    @Nullable
191    public T getItem(int index) {
192        if (mPagedList == null) {
193            if (mSnapshot == null) {
194                throw new IndexOutOfBoundsException(
195                        "Item count is zero, getItem() call is invalid");
196            } else {
197                return mSnapshot.get(index);
198            }
199        }
200
201        mPagedList.loadAround(index);
202        return mPagedList.get(index);
203    }
204
205    /**
206     * Get the number of items currently presented by this Differ. This value can be directly
207     * returned to {@link RecyclerView.Adapter#getItemCount()}.
208     *
209     * @return Number of items being presented.
210     */
211    @SuppressWarnings("WeakerAccess")
212    public int getItemCount() {
213        if (mPagedList != null) {
214            return mPagedList.size();
215        }
216
217        return mSnapshot == null ? 0 : mSnapshot.size();
218    }
219
220    /**
221     * Pass a new PagedList to the differ.
222     * <p>
223     * If a PagedList is already present, a diff will be computed asynchronously on a background
224     * thread. When the diff is computed, it will be applied (dispatched to the
225     * {@link ListUpdateCallback}), and the new PagedList will be swapped in as the
226     * {@link #getCurrentList() current list}.
227     *
228     * @param pagedList The new PagedList.
229     */
230    @SuppressWarnings("ReferenceEquality")
231    public void submitList(final PagedList<T> pagedList) {
232        if (pagedList != null) {
233            if (mPagedList == null && mSnapshot == null) {
234                mIsContiguous = pagedList.isContiguous();
235            } else {
236                if (pagedList.isContiguous() != mIsContiguous) {
237                    throw new IllegalArgumentException("AsyncPagedListDiffer cannot handle both"
238                            + " contiguous and non-contiguous lists.");
239                }
240            }
241        }
242
243        if (pagedList == mPagedList) {
244            // nothing to do
245            return;
246        }
247
248        // incrementing generation means any currently-running diffs are discarded when they finish
249        final int runGeneration = ++mMaxScheduledGeneration;
250
251        if (pagedList == null) {
252            int removedCount = getItemCount();
253            if (mPagedList != null) {
254                mPagedList.removeWeakCallback(mPagedListCallback);
255                mPagedList = null;
256            } else if (mSnapshot != null) {
257                mSnapshot = null;
258            }
259            // dispatch update callback after updating mPagedList/mSnapshot
260            mUpdateCallback.onRemoved(0, removedCount);
261            if (mListener != null) {
262                mListener.onCurrentListChanged(null);
263            }
264            return;
265        }
266
267        if (mPagedList == null && mSnapshot == null) {
268            // fast simple first insert
269            mPagedList = pagedList;
270            pagedList.addWeakCallback(null, mPagedListCallback);
271
272            // dispatch update callback after updating mPagedList/mSnapshot
273            mUpdateCallback.onInserted(0, pagedList.size());
274
275            if (mListener != null) {
276                mListener.onCurrentListChanged(pagedList);
277            }
278            return;
279        }
280
281        if (mPagedList != null) {
282            // first update scheduled on this list, so capture mPages as a snapshot, removing
283            // callbacks so we don't have resolve updates against a moving target
284            mPagedList.removeWeakCallback(mPagedListCallback);
285            mSnapshot = (PagedList<T>) mPagedList.snapshot();
286            mPagedList = null;
287        }
288
289        if (mSnapshot == null || mPagedList != null) {
290            throw new IllegalStateException("must be in snapshot state to diff");
291        }
292
293        final PagedList<T> oldSnapshot = mSnapshot;
294        final PagedList<T> newSnapshot = (PagedList<T>) pagedList.snapshot();
295        mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
296            @Override
297            public void run() {
298                final DiffUtil.DiffResult result;
299                result = PagedStorageDiffHelper.computeDiff(
300                        oldSnapshot.mStorage,
301                        newSnapshot.mStorage,
302                        mConfig.getDiffCallback());
303
304                mMainThreadExecutor.execute(new Runnable() {
305                    @Override
306                    public void run() {
307                        if (mMaxScheduledGeneration == runGeneration) {
308                            latchPagedList(pagedList, newSnapshot, result);
309                        }
310                    }
311                });
312            }
313        });
314    }
315
316    private void latchPagedList(
317            PagedList<T> newList, PagedList<T> diffSnapshot,
318            DiffUtil.DiffResult diffResult) {
319        if (mSnapshot == null || mPagedList != null) {
320            throw new IllegalStateException("must be in snapshot state to apply diff");
321        }
322
323        PagedList<T> previousSnapshot = mSnapshot;
324        mPagedList = newList;
325        mSnapshot = null;
326
327        // dispatch update callback after updating mPagedList/mSnapshot
328        PagedStorageDiffHelper.dispatchDiff(mUpdateCallback,
329                previousSnapshot.mStorage, newList.mStorage, diffResult);
330
331        newList.addWeakCallback(diffSnapshot, mPagedListCallback);
332        if (mListener != null) {
333            mListener.onCurrentListChanged(mPagedList);
334        }
335    }
336
337    /**
338     * Returns the PagedList currently being displayed by the differ.
339     * <p>
340     * This is not necessarily the most recent list passed to {@link #submitList(PagedList)},
341     * because a diff is computed asynchronously between the new list and the current list before
342     * updating the currentList value. May be null if no PagedList is being presented.
343     *
344     * @return The list currently being displayed, may be null.
345     */
346    @SuppressWarnings("WeakerAccess")
347    @Nullable
348    public PagedList<T> getCurrentList() {
349        if (mSnapshot != null) {
350            return mSnapshot;
351        }
352        return mPagedList;
353    }
354}
355