From 5785890083100e06d4f7aaf640885a2eee415853 Mon Sep 17 00:00:00 2001 From: M66B Date: Wed, 22 Jul 2020 08:39:30 +0200 Subject: [PATCH] Revert "Revert "Compile paging inline"" This reverts commit 5ff77cacd8fe348171ccde37430d7120b3f9490b. --- app/build.gradle | 3 +- .../androidx/paging/AsyncPagedListDiffer.java | 447 +++++++ .../androidx/paging/ContiguousDataSource.java | 63 + .../androidx/paging/ContiguousPagedList.java | 412 ++++++ .../main/java/androidx/paging/DataSource.java | 408 ++++++ .../androidx/paging/ItemKeyedDataSource.java | 375 ++++++ .../java/androidx/paging/ListDataSource.java | 51 + .../androidx/paging/LivePagedListBuilder.java | 212 +++ .../androidx/paging/PageKeyedDataSource.java | 445 +++++++ .../main/java/androidx/paging/PageResult.java | 105 ++ .../main/java/androidx/paging/PagedList.java | 1174 +++++++++++++++++ .../androidx/paging/PagedListAdapter.java | 244 ++++ .../java/androidx/paging/PagedStorage.java | 649 +++++++++ .../paging/PagedStorageDiffHelper.java | 231 ++++ .../androidx/paging/PositionalDataSource.java | 576 ++++++++ .../androidx/paging/SnapshotPagedList.java | 74 ++ .../java/androidx/paging/TiledDataSource.java | 86 ++ .../java/androidx/paging/TiledPagedList.java | 230 ++++ .../paging/WrapperItemKeyedDataSource.java | 116 ++ .../paging/WrapperPageKeyedDataSource.java | 96 ++ .../paging/WrapperPositionalDataSource.java | 81 ++ 21 files changed, 6077 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/androidx/paging/AsyncPagedListDiffer.java create mode 100644 app/src/main/java/androidx/paging/ContiguousDataSource.java create mode 100644 app/src/main/java/androidx/paging/ContiguousPagedList.java create mode 100644 app/src/main/java/androidx/paging/DataSource.java create mode 100644 app/src/main/java/androidx/paging/ItemKeyedDataSource.java create mode 100644 app/src/main/java/androidx/paging/ListDataSource.java create mode 100644 app/src/main/java/androidx/paging/LivePagedListBuilder.java create mode 100644 app/src/main/java/androidx/paging/PageKeyedDataSource.java create mode 100644 app/src/main/java/androidx/paging/PageResult.java create mode 100644 app/src/main/java/androidx/paging/PagedList.java create mode 100644 app/src/main/java/androidx/paging/PagedListAdapter.java create mode 100644 app/src/main/java/androidx/paging/PagedStorage.java create mode 100644 app/src/main/java/androidx/paging/PagedStorageDiffHelper.java create mode 100644 app/src/main/java/androidx/paging/PositionalDataSource.java create mode 100644 app/src/main/java/androidx/paging/SnapshotPagedList.java create mode 100644 app/src/main/java/androidx/paging/TiledDataSource.java create mode 100644 app/src/main/java/androidx/paging/TiledPagedList.java create mode 100644 app/src/main/java/androidx/paging/WrapperItemKeyedDataSource.java create mode 100644 app/src/main/java/androidx/paging/WrapperPageKeyedDataSource.java create mode 100644 app/src/main/java/androidx/paging/WrapperPositionalDataSource.java diff --git a/app/build.gradle b/app/build.gradle index ff29277c82..b0cd82b332 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -204,6 +204,7 @@ configurations.all { // lifecycle-livedata: ComputableLiveData, MediatorLiveData, Transformations // lifecycle-livedata-core: LiveData, MutableLiveData, Observer + // paging-runtime: AsyncPagedListDiffer, LivePagedListBuilder, PagedListAdapter, PagedStorageDiffHelper } dependencies { @@ -303,7 +304,7 @@ dependencies { // https://mvnrepository.com/artifact/androidx.paging/paging-runtime // https://developer.android.com/jetpack/androidx/releases/paging - implementation "androidx.paging:paging-runtime:$paging_version" + //implementation "androidx.paging:paging-runtime:$paging_version" // https://mvnrepository.com/artifact/androidx.preference/preference implementation "androidx.preference:preference:$preference_version" diff --git a/app/src/main/java/androidx/paging/AsyncPagedListDiffer.java b/app/src/main/java/androidx/paging/AsyncPagedListDiffer.java new file mode 100644 index 0000000000..6640dc0c8c --- /dev/null +++ b/app/src/main/java/androidx/paging/AsyncPagedListDiffer.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 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 androidx.paging; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.arch.core.executor.ArchTaskExecutor; +import androidx.lifecycle.LiveData; +import androidx.recyclerview.widget.AdapterListUpdateCallback; +import androidx.recyclerview.widget.AsyncDifferConfig; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListUpdateCallback; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; + +/** + * Helper object for mapping a {@link PagedList} into a + * {@link androidx.recyclerview.widget.RecyclerView.Adapter RecyclerView.Adapter}. + *

+ * For simplicity, the {@link PagedListAdapter} wrapper class can often be used instead of the + * differ directly. This diff class is exposed for complex cases, and where overriding an adapter + * base class to support paging isn't convenient. + *

+ * When consuming a {@link LiveData} of PagedList, you can observe updates and dispatch them + * directly to {@link #submitList(PagedList)}. The AsyncPagedListDiffer then can present this + * updating data set simply for an adapter. It listens to PagedList loading callbacks, and uses + * DiffUtil on a background thread to compute updates as new PagedLists are received. + *

+ * It provides a simple list-like API with {@link #getItem(int)} and {@link #getItemCount()} for an + * adapter to acquire and present data objects. + *

+ * A complete usage pattern with Room would look like this: + *

+ * {@literal @}Dao
+ * interface UserDao {
+ *     {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
+ *     public abstract DataSource.Factory<Integer, User> usersByLastName();
+ * }
+ *
+ * class MyViewModel extends ViewModel {
+ *     public final LiveData<PagedList<User>> usersList;
+ *     public MyViewModel(UserDao userDao) {
+ *         usersList = new LivePagedListBuilder<>(
+ *                 userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
+ *     }
+ * }
+ *
+ * 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);
+ *         final UserAdapter adapter = new UserAdapter();
+ *         viewModel.usersList.observe(this, pagedList -> adapter.submitList(pagedList));
+ *         recyclerView.setAdapter(adapter);
+ *     }
+ * }
+ *
+ * class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
+ *     private final AsyncPagedListDiffer<User> mDiffer
+ *             = new AsyncPagedListDiffer(this, DIFF_CALLBACK);
+ *     {@literal @}Override
+ *     public int getItemCount() {
+ *         return mDiffer.getItemCount();
+ *     }
+ *     public void submitList(PagedList<User> pagedList) {
+ *         mDiffer.submitList(pagedList);
+ *     }
+ *     {@literal @}Override
+ *     public void onBindViewHolder(UserViewHolder holder, int position) {
+ *         User user = mDiffer.getItem(position);
+ *         if (user != null) {
+ *             holder.bindTo(user);
+ *         } else {
+ *             // Null defines a placeholder item - AsyncPagedListDiffer will automatically
+ *             // invalidate this row when the actual object is loaded from the database
+ *             holder.clear();
+ *         }
+ *     }
+ *     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);
+ *          }
+ *      }
+ * }
+ * + * @param Type of the PagedLists this differ will receive. + */ +public class AsyncPagedListDiffer { + // updateCallback notifications must only be notified *after* new data and item count are stored + // this ensures Adapter#notifyItemRangeInserted etc are accessing the new data + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final ListUpdateCallback mUpdateCallback; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final AsyncDifferConfig mConfig; + + @SuppressWarnings("RestrictedApi") + Executor mMainThreadExecutor = ArchTaskExecutor.getMainThreadExecutor(); + + /** + * Listener for when the current PagedList is updated. + * + * @param Type of items in PagedList + */ + public interface PagedListListener { + /** + * Called after the current PagedList has been updated. + * + * @param previousList The previous list, may be null. + * @param currentList The new current list, may be null. + */ + void onCurrentListChanged( + @Nullable PagedList previousList, @Nullable PagedList currentList); + } + + private final List> mListeners = new CopyOnWriteArrayList<>(); + + private boolean mIsContiguous; + + private PagedList mPagedList; + private PagedList mSnapshot; + + // Max generation of currently scheduled runnable + @SuppressWarnings("WeakerAccess") /* synthetic access */ + int mMaxScheduledGeneration; + + /** + * Convenience for {@code AsyncPagedListDiffer(new AdapterListUpdateCallback(adapter), + * new AsyncDifferConfig.Builder(diffCallback).build();} + * + * @param adapter Adapter that will receive update signals. + * @param diffCallback The {@link DiffUtil.ItemCallback DiffUtil.ItemCallback} instance to + * compare items in the list. + */ + @SuppressWarnings("WeakerAccess") + public AsyncPagedListDiffer(@NonNull RecyclerView.Adapter adapter, + @NonNull DiffUtil.ItemCallback diffCallback) { + mUpdateCallback = new AdapterListUpdateCallback(adapter); + mConfig = new AsyncDifferConfig.Builder<>(diffCallback).build(); + } + + @SuppressWarnings("WeakerAccess") + public AsyncPagedListDiffer(@NonNull ListUpdateCallback listUpdateCallback, + @NonNull AsyncDifferConfig config) { + mUpdateCallback = listUpdateCallback; + mConfig = config; + } + + private PagedList.Callback mPagedListCallback = new PagedList.Callback() { + @Override + public void onInserted(int position, int count) { + mUpdateCallback.onInserted(position, count); + } + + @Override + public void onRemoved(int position, int count) { + mUpdateCallback.onRemoved(position, count); + } + + @Override + public void onChanged(int position, int count) { + // NOTE: pass a null payload to convey null -> item + mUpdateCallback.onChanged(position, count, null); + } + }; + + /** + * Get the item from the current PagedList at the specified index. + *

+ * Note that this operates on both loaded items and null padding within the PagedList. + * + * @param index Index of item to get, must be >= 0, and < {@link #getItemCount()}. + * @return The item, or null, if a null placeholder is at the specified position. + */ + @SuppressWarnings("WeakerAccess") + @Nullable + public T getItem(int index) { + if (mPagedList == null) { + if (mSnapshot == null) { + throw new IndexOutOfBoundsException( + "Item count is zero, getItem() call is invalid"); + } else { + return mSnapshot.get(index); + } + } + + mPagedList.loadAround(index); + return mPagedList.get(index); + } + + /** + * Get the number of items currently presented by this Differ. This value can be directly + * returned to {@link RecyclerView.Adapter#getItemCount()}. + * + * @return Number of items being presented. + */ + @SuppressWarnings("WeakerAccess") + public int getItemCount() { + if (mPagedList != null) { + return mPagedList.size(); + } + + return mSnapshot == null ? 0 : mSnapshot.size(); + } + + /** + * Pass a new PagedList to the differ. + *

+ * If a PagedList 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 PagedList will be swapped in as the + * {@link #getCurrentList() current list}. + * + * @param pagedList The new PagedList. + */ + public void submitList(@Nullable final PagedList pagedList) { + submitList(pagedList, null); + } + + /** + * Pass a new PagedList to the differ. + *

+ * If a PagedList 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 PagedList will be swapped in as the + * {@link #getCurrentList() current list}. + *

+ * The commit callback can be used to know when the PagedList is committed, but note that it + * may not be executed. If PagedList B is submitted immediately after PagedList A, and is + * committed directly, the callback associated with PagedList A will not be run. + * + * @param pagedList The new PagedList. + * @param commitCallback Optional runnable that is executed when the PagedList is committed, if + * it is committed. + */ + @SuppressWarnings("ReferenceEquality") + public void submitList(@Nullable final PagedList pagedList, + @Nullable final Runnable commitCallback) { + if (pagedList != null) { + if (mPagedList == null && mSnapshot == null) { + mIsContiguous = pagedList.isContiguous(); + } else { + if (pagedList.isContiguous() != mIsContiguous) { + throw new IllegalArgumentException("AsyncPagedListDiffer cannot handle both" + + " contiguous and non-contiguous lists."); + } + } + } + + // incrementing generation means any currently-running diffs are discarded when they finish + final int runGeneration = ++mMaxScheduledGeneration; + + if (pagedList == mPagedList) { + // nothing to do (Note - still had to inc generation, since may have ongoing work) + if (commitCallback != null) { + commitCallback.run(); + } + return; + } + + final PagedList previous = (mSnapshot != null) ? mSnapshot : mPagedList; + + if (pagedList == null) { + int removedCount = getItemCount(); + if (mPagedList != null) { + mPagedList.removeWeakCallback(mPagedListCallback); + mPagedList = null; + } else if (mSnapshot != null) { + mSnapshot = null; + } + // dispatch update callback after updating mPagedList/mSnapshot + mUpdateCallback.onRemoved(0, removedCount); + onCurrentListChanged(previous, null, commitCallback); + return; + } + + if (mPagedList == null && mSnapshot == null) { + // fast simple first insert + mPagedList = pagedList; + pagedList.addWeakCallback(null, mPagedListCallback); + + // dispatch update callback after updating mPagedList/mSnapshot + mUpdateCallback.onInserted(0, pagedList.size()); + + onCurrentListChanged(null, pagedList, commitCallback); + return; + } + + if (mPagedList != null) { + // first update scheduled on this list, so capture mPages as a snapshot, removing + // callbacks so we don't have resolve updates against a moving target + mPagedList.removeWeakCallback(mPagedListCallback); + mSnapshot = (PagedList) mPagedList.snapshot(); + mPagedList = null; + } + + if (mSnapshot == null || mPagedList != null) { + throw new IllegalStateException("must be in snapshot state to diff"); + } + + final PagedList oldSnapshot = mSnapshot; + final PagedList newSnapshot = (PagedList) pagedList.snapshot(); + mConfig.getBackgroundThreadExecutor().execute(new Runnable() { + @Override + public void run() { + final DiffUtil.DiffResult result; + result = PagedStorageDiffHelper.computeDiff( + oldSnapshot.mStorage, + newSnapshot.mStorage, + mConfig.getDiffCallback()); + + mMainThreadExecutor.execute(new Runnable() { + @Override + public void run() { + if (mMaxScheduledGeneration == runGeneration) { + latchPagedList(pagedList, newSnapshot, result, + oldSnapshot.mLastLoad, commitCallback); + } + } + }); + } + }); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void latchPagedList( + @NonNull PagedList newList, + @NonNull PagedList diffSnapshot, + @NonNull DiffUtil.DiffResult diffResult, + int lastAccessIndex, + @Nullable Runnable commitCallback) { + if (mSnapshot == null || mPagedList != null) { + throw new IllegalStateException("must be in snapshot state to apply diff"); + } + + PagedList previousSnapshot = mSnapshot; + mPagedList = newList; + mSnapshot = null; + + // dispatch update callback after updating mPagedList/mSnapshot + PagedStorageDiffHelper.dispatchDiff(mUpdateCallback, + previousSnapshot.mStorage, newList.mStorage, diffResult); + + newList.addWeakCallback(diffSnapshot, mPagedListCallback); + + if (!mPagedList.isEmpty()) { + // Transform the last loadAround() index from the old list to the new list by passing it + // through the DiffResult. This ensures the lastKey of a positional PagedList is carried + // to new list even if no in-viewport item changes (AsyncPagedListDiffer#get not called) + // Note: we don't take into account loads between new list snapshot and new list, but + // this is only a problem in rare cases when placeholders are disabled, and a load + // starts (for some reason) and finishes before diff completes. + int newPosition = PagedStorageDiffHelper.transformAnchorIndex( + diffResult, previousSnapshot.mStorage, diffSnapshot.mStorage, lastAccessIndex); + + // Trigger load in new list at this position, clamped to list bounds. + // This is a load, not just an update of last load position, since the new list may be + // incomplete. If new list is subset of old list, but doesn't fill the viewport, this + // will likely trigger a load of new data. + mPagedList.loadAround(Math.max(0, Math.min(mPagedList.size() - 1, newPosition))); + } + + onCurrentListChanged(previousSnapshot, mPagedList, commitCallback); + } + + private void onCurrentListChanged( + @Nullable PagedList previousList, + @Nullable PagedList currentList, + @Nullable Runnable commitCallback) { + for (PagedListListener listener : mListeners) { + listener.onCurrentListChanged(previousList, currentList); + } + if (commitCallback != null) { + commitCallback.run(); + } + } + + /** + * Add a PagedListListener to receive updates when the current PagedList changes. + * + * @param listener Listener to receive updates. + * + * @see #getCurrentList() + * @see #removePagedListListener(PagedListListener) + */ + public void addPagedListListener(@NonNull PagedListListener listener) { + mListeners.add(listener); + } + + /** + * Remove a previously registered PagedListListener. + * + * @param listener Previously registered listener. + * @see #getCurrentList() + * @see #addPagedListListener(PagedListListener) + */ + public void removePagedListListener(@NonNull PagedListListener listener) { + mListeners.remove(listener); + } + + /** + * Returns the PagedList currently being displayed by the differ. + *

+ * This is not necessarily the most recent list passed to {@link #submitList(PagedList)}, + * because a diff is computed asynchronously between the new list and the current list before + * updating the currentList value. May be null if no PagedList is being presented. + * + * @return The list currently being displayed, may be null. + */ + @SuppressWarnings("WeakerAccess") + @Nullable + public PagedList getCurrentList() { + if (mSnapshot != null) { + return mSnapshot; + } + return mPagedList; + } +} diff --git a/app/src/main/java/androidx/paging/ContiguousDataSource.java b/app/src/main/java/androidx/paging/ContiguousDataSource.java new file mode 100644 index 0000000000..5cde0af09a --- /dev/null +++ b/app/src/main/java/androidx/paging/ContiguousDataSource.java @@ -0,0 +1,63 @@ +/* + * 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.paging; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.concurrent.Executor; + +abstract class ContiguousDataSource extends DataSource { + @Override + boolean isContiguous() { + return true; + } + + abstract void dispatchLoadInitial( + @Nullable Key key, + int initialLoadSize, + int pageSize, + boolean enablePlaceholders, + @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver); + + abstract void dispatchLoadAfter( + int currentEndIndex, + @NonNull Value currentEndItem, + int pageSize, + @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver); + + abstract void dispatchLoadBefore( + int currentBeginIndex, + @NonNull Value currentBeginItem, + int pageSize, + @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver); + + /** + * Get the key from either the position, or item, or null if position/item invalid. + *

+ * Position may not match passed item's position - if trying to query the key from a position + * that isn't yet loaded, a fallback item (last loaded item accessed) will be passed. + */ + abstract Key getKey(int position, Value item); + + boolean supportsPageDropping() { + return true; + } +} diff --git a/app/src/main/java/androidx/paging/ContiguousPagedList.java b/app/src/main/java/androidx/paging/ContiguousPagedList.java new file mode 100644 index 0000000000..99060671d8 --- /dev/null +++ b/app/src/main/java/androidx/paging/ContiguousPagedList.java @@ -0,0 +1,412 @@ +/* + * 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.paging; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import androidx.annotation.AnyThread; +import androidx.annotation.IntDef; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.util.List; +import java.util.concurrent.Executor; + +class ContiguousPagedList extends PagedList implements PagedStorage.Callback { + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final ContiguousDataSource mDataSource; + + @Retention(SOURCE) + @IntDef({READY_TO_FETCH, FETCHING, DONE_FETCHING}) + @interface FetchState {} + + private static final int READY_TO_FETCH = 0; + private static final int FETCHING = 1; + private static final int DONE_FETCHING = 2; + + @FetchState + @SuppressWarnings("WeakerAccess") /* synthetic access */ + int mPrependWorkerState = READY_TO_FETCH; + @FetchState + @SuppressWarnings("WeakerAccess") /* synthetic access */ + int mAppendWorkerState = READY_TO_FETCH; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + int mPrependItemsRequested = 0; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + int mAppendItemsRequested = 0; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + boolean mReplacePagesWithNulls = false; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final boolean mShouldTrim; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + PageResult.Receiver mReceiver = new PageResult.Receiver() { + // Creation thread for initial synchronous load, otherwise main thread + // Safe to access main thread only state - no other thread has reference during construction + @AnyThread + @Override + public void onPageResult(@PageResult.ResultType int resultType, + @NonNull PageResult pageResult) { + if (pageResult.isInvalid()) { + detach(); + return; + } + + if (isDetached()) { + // No op, have detached + return; + } + + List page = pageResult.page; + if (resultType == PageResult.INIT) { + mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls, + pageResult.positionOffset, ContiguousPagedList.this); + if (mLastLoad == LAST_LOAD_UNSPECIFIED) { + // Because the ContiguousPagedList wasn't initialized with a last load position, + // initialize it to the middle of the initial load + mLastLoad = + pageResult.leadingNulls + pageResult.positionOffset + page.size() / 2; + } + } else { + // if we end up trimming, we trim from side that's furthest from most recent access + boolean trimFromFront = mLastLoad > mStorage.getMiddleOfLoadedRange(); + + // is the new page big enough to warrant pre-trimming (i.e. dropping) it? + boolean skipNewPage = mShouldTrim + && mStorage.shouldPreTrimNewPage( + mConfig.maxSize, mRequiredRemainder, page.size()); + + if (resultType == PageResult.APPEND) { + if (skipNewPage && !trimFromFront) { + // don't append this data, drop it + mAppendItemsRequested = 0; + mAppendWorkerState = READY_TO_FETCH; + } else { + mStorage.appendPage(page, ContiguousPagedList.this); + } + } else if (resultType == PageResult.PREPEND) { + if (skipNewPage && trimFromFront) { + // don't append this data, drop it + mPrependItemsRequested = 0; + mPrependWorkerState = READY_TO_FETCH; + } else { + mStorage.prependPage(page, ContiguousPagedList.this); + } + } else { + throw new IllegalArgumentException("unexpected resultType " + resultType); + } + + if (mShouldTrim) { + if (trimFromFront) { + if (mPrependWorkerState != FETCHING) { + if (mStorage.trimFromFront( + mReplacePagesWithNulls, + mConfig.maxSize, + mRequiredRemainder, + ContiguousPagedList.this)) { + // trimmed from front, ensure we can fetch in that dir + mPrependWorkerState = READY_TO_FETCH; + } + } + } else { + if (mAppendWorkerState != FETCHING) { + if (mStorage.trimFromEnd( + mReplacePagesWithNulls, + mConfig.maxSize, + mRequiredRemainder, + ContiguousPagedList.this)) { + mAppendWorkerState = READY_TO_FETCH; + } + } + } + } + } + + if (mBoundaryCallback != null) { + boolean deferEmpty = mStorage.size() == 0; + boolean deferBegin = !deferEmpty + && resultType == PageResult.PREPEND + && pageResult.page.size() == 0; + boolean deferEnd = !deferEmpty + && resultType == PageResult.APPEND + && pageResult.page.size() == 0; + deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd); + } + } + }; + + static final int LAST_LOAD_UNSPECIFIED = -1; + + ContiguousPagedList( + @NonNull ContiguousDataSource dataSource, + @NonNull Executor mainThreadExecutor, + @NonNull Executor backgroundThreadExecutor, + @Nullable BoundaryCallback boundaryCallback, + @NonNull Config config, + final @Nullable K key, + int lastLoad) { + super(new PagedStorage(), mainThreadExecutor, backgroundThreadExecutor, + boundaryCallback, config); + mDataSource = dataSource; + mLastLoad = lastLoad; + + if (mDataSource.isInvalid()) { + detach(); + } else { + mDataSource.dispatchLoadInitial(key, + mConfig.initialLoadSizeHint, + mConfig.pageSize, + mConfig.enablePlaceholders, + mMainThreadExecutor, + mReceiver); + } + mShouldTrim = mDataSource.supportsPageDropping() + && mConfig.maxSize != Config.MAX_SIZE_UNBOUNDED; + } + + @MainThread + @Override + void dispatchUpdatesSinceSnapshot( + @NonNull PagedList pagedListSnapshot, @NonNull Callback callback) { + final PagedStorage snapshot = pagedListSnapshot.mStorage; + + final int newlyAppended = mStorage.getNumberAppended() - snapshot.getNumberAppended(); + final int newlyPrepended = mStorage.getNumberPrepended() - snapshot.getNumberPrepended(); + + final int previousTrailing = snapshot.getTrailingNullCount(); + final int previousLeading = snapshot.getLeadingNullCount(); + + // Validate that the snapshot looks like a previous version of this list - if it's not, + // we can't be sure we'll dispatch callbacks safely + if (snapshot.isEmpty() + || newlyAppended < 0 + || newlyPrepended < 0 + || mStorage.getTrailingNullCount() != Math.max(previousTrailing - newlyAppended, 0) + || mStorage.getLeadingNullCount() != Math.max(previousLeading - newlyPrepended, 0) + || (mStorage.getStorageCount() + != snapshot.getStorageCount() + newlyAppended + newlyPrepended)) { + throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear" + + " to be a snapshot of this PagedList"); + } + + if (newlyAppended != 0) { + final int changedCount = Math.min(previousTrailing, newlyAppended); + final int addedCount = newlyAppended - changedCount; + + final int endPosition = snapshot.getLeadingNullCount() + snapshot.getStorageCount(); + if (changedCount != 0) { + callback.onChanged(endPosition, changedCount); + } + if (addedCount != 0) { + callback.onInserted(endPosition + changedCount, addedCount); + } + } + if (newlyPrepended != 0) { + final int changedCount = Math.min(previousLeading, newlyPrepended); + final int addedCount = newlyPrepended - changedCount; + + if (changedCount != 0) { + callback.onChanged(previousLeading, changedCount); + } + if (addedCount != 0) { + callback.onInserted(0, addedCount); + } + } + } + + static int getPrependItemsRequested(int prefetchDistance, int index, int leadingNulls) { + return prefetchDistance - (index - leadingNulls); + } + + static int getAppendItemsRequested( + int prefetchDistance, int index, int itemsBeforeTrailingNulls) { + return index + prefetchDistance + 1 - itemsBeforeTrailingNulls; + } + + @MainThread + @Override + protected void loadAroundInternal(int index) { + int prependItems = getPrependItemsRequested(mConfig.prefetchDistance, index, + mStorage.getLeadingNullCount()); + int appendItems = getAppendItemsRequested(mConfig.prefetchDistance, index, + mStorage.getLeadingNullCount() + mStorage.getStorageCount()); + + mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested); + if (mPrependItemsRequested > 0) { + schedulePrepend(); + } + + mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested); + if (mAppendItemsRequested > 0) { + scheduleAppend(); + } + } + + @MainThread + private void schedulePrepend() { + if (mPrependWorkerState != READY_TO_FETCH) { + return; + } + mPrependWorkerState = FETCHING; + + final int position = mStorage.getLeadingNullCount() + mStorage.getPositionOffset(); + + // safe to access first item here - mStorage can't be empty if we're prepending + final V item = mStorage.getFirstLoadedItem(); + mBackgroundThreadExecutor.execute(new Runnable() { + @Override + public void run() { + if (isDetached()) { + return; + } + if (mDataSource.isInvalid()) { + detach(); + } else { + mDataSource.dispatchLoadBefore(position, item, mConfig.pageSize, + mMainThreadExecutor, mReceiver); + } + } + }); + } + + @MainThread + private void scheduleAppend() { + if (mAppendWorkerState != READY_TO_FETCH) { + return; + } + mAppendWorkerState = FETCHING; + + final int position = mStorage.getLeadingNullCount() + + mStorage.getStorageCount() - 1 + mStorage.getPositionOffset(); + + // safe to access first item here - mStorage can't be empty if we're appending + final V item = mStorage.getLastLoadedItem(); + mBackgroundThreadExecutor.execute(new Runnable() { + @Override + public void run() { + if (isDetached()) { + return; + } + if (mDataSource.isInvalid()) { + detach(); + } else { + mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize, + mMainThreadExecutor, mReceiver); + } + } + }); + } + + @Override + boolean isContiguous() { + return true; + } + + @NonNull + @Override + public DataSource getDataSource() { + return mDataSource; + } + + @Nullable + @Override + public Object getLastKey() { + return mDataSource.getKey(mLastLoad, mLastItem); + } + + @MainThread + @Override + public void onInitialized(int count) { + notifyInserted(0, count); + // simple heuristic to decide if, when dropping pages, we should replace with placeholders + mReplacePagesWithNulls = + mStorage.getLeadingNullCount() > 0 || mStorage.getTrailingNullCount() > 0; + } + + @MainThread + @Override + public void onPagePrepended(int leadingNulls, int changedCount, int addedCount) { + // consider whether to post more work, now that a page is fully prepended + mPrependItemsRequested = mPrependItemsRequested - changedCount - addedCount; + mPrependWorkerState = READY_TO_FETCH; + if (mPrependItemsRequested > 0) { + // not done prepending, keep going + schedulePrepend(); + } + + // finally dispatch callbacks, after prepend may have already been scheduled + notifyChanged(leadingNulls, changedCount); + notifyInserted(0, addedCount); + + offsetAccessIndices(addedCount); + } + + @MainThread + @Override + public void onEmptyPrepend() { + mPrependWorkerState = DONE_FETCHING; + } + + @MainThread + @Override + public void onPageAppended(int endPosition, int changedCount, int addedCount) { + // consider whether to post more work, now that a page is fully appended + mAppendItemsRequested = mAppendItemsRequested - changedCount - addedCount; + mAppendWorkerState = READY_TO_FETCH; + if (mAppendItemsRequested > 0) { + // not done appending, keep going + scheduleAppend(); + } + + // finally dispatch callbacks, after append may have already been scheduled + notifyChanged(endPosition, changedCount); + notifyInserted(endPosition + changedCount, addedCount); + } + + @MainThread + @Override + public void onEmptyAppend() { + mAppendWorkerState = DONE_FETCHING; + } + + @MainThread + @Override + public void onPagePlaceholderInserted(int pageIndex) { + throw new IllegalStateException("Tiled callback on ContiguousPagedList"); + } + + @MainThread + @Override + public void onPageInserted(int start, int count) { + throw new IllegalStateException("Tiled callback on ContiguousPagedList"); + } + + @Override + public void onPagesRemoved(int startOfDrops, int count) { + notifyRemoved(startOfDrops, count); + } + + @Override + public void onPagesSwappedToPlaceholder(int startOfDrops, int count) { + notifyChanged(startOfDrops, count); + } +} diff --git a/app/src/main/java/androidx/paging/DataSource.java b/app/src/main/java/androidx/paging/DataSource.java new file mode 100644 index 0000000000..60f3cfe51f --- /dev/null +++ b/app/src/main/java/androidx/paging/DataSource.java @@ -0,0 +1,408 @@ +/* + * 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.paging; + +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import androidx.arch.core.util.Function; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Base class for loading pages of snapshot data into a {@link PagedList}. + *

+ * DataSource is queried to load pages of content into a {@link PagedList}. A PagedList can grow as + * it loads more data, but the data loaded cannot be updated. If the underlying data set is + * modified, a new PagedList / DataSource pair must be created to represent the new data. + *

Loading Pages

+ * PagedList queries data from its DataSource in response to loading hints. {@link PagedListAdapter} + * calls {@link PagedList#loadAround(int)} to load content as the user scrolls in a RecyclerView. + *

+ * To control how and when a PagedList queries data from its DataSource, see + * {@link PagedList.Config}. The Config object defines things like load sizes and prefetch distance. + *

Updating Paged Data

+ * A PagedList / DataSource pair are a snapshot of the data set. A new pair of + * PagedList / DataSource must be created if an update occurs, such as a reorder, insert, delete, or + * content update occurs. A DataSource must detect that it cannot continue loading its + * snapshot (for instance, when Database query notices a table being invalidated), and call + * {@link #invalidate()}. Then a new PagedList / DataSource pair would be created to load data from + * the new state of the Database query. + *

+ * To page in data that doesn't update, you can create a single DataSource, and pass it to a single + * PagedList. For example, loading from network when the network's paging API doesn't provide + * updates. + *

+ * To page in data from a source that does provide updates, you can create a + * {@link DataSource.Factory}, where each DataSource created is invalidated when an update to the + * data set occurs that makes the current snapshot invalid. For example, when paging a query from + * the Database, and the table being queried inserts or removes items. You can also use a + * DataSource.Factory to provide multiple versions of network-paged lists. If reloading all content + * (e.g. in response to an action like swipe-to-refresh) is required to get a new version of data, + * you can connect an explicit refresh signal to call {@link #invalidate()} on the current + * DataSource. + *

+ * If you have more granular update signals, such as a network API signaling an update to a single + * item in the list, it's recommended to load data from network into memory. Then present that + * data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory + * copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the + * snapshot can be created. + *

Implementing a DataSource

+ * To implement, extend one of the subclasses: {@link PageKeyedDataSource}, + * {@link ItemKeyedDataSource}, or {@link PositionalDataSource}. + *

+ * Use {@link PageKeyedDataSource} if pages you load embed keys for loading adjacent pages. For + * example a network response that returns some items, and a next/previous page links. + *

+ * Use {@link ItemKeyedDataSource} if you need to use data from item {@code N-1} to load item + * {@code N}. For example, if requesting the backend for the next comments in the list + * requires the ID or timestamp of the most recent loaded comment, or if querying the next users + * from a name-sorted database query requires the name and unique ID of the previous. + *

+ * Use {@link PositionalDataSource} if you can load pages of a requested size at arbitrary + * positions, and provide a fixed item count. PositionalDataSource supports querying pages at + * arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that + * PositionalDataSource is required to respect page size for efficient tiling. If you want to + * override page size (e.g. when network page size constraints are only known at runtime), use one + * of the other DataSource classes. + *

+ * Because a {@code null} item indicates a placeholder in {@link PagedList}, DataSource may not + * return {@code null} items in lists that it loads. This is so that users of the PagedList + * can differentiate unloaded placeholder items from content that has been paged in. + * + * @param Unique identifier for item loaded from DataSource. Often an integer to represent + * position in data set. Note - this is distinct from e.g. Room's {@code @PrimaryKey}. + * @param Value type loaded by the DataSource. + */ +@SuppressWarnings("unused") // suppress warning to remove Key/Value, needed for subclass type safety +public abstract class DataSource { + /** + * Factory for DataSources. + *

+ * Data-loading systems of an application or library can implement this interface to allow + * {@code LiveData}s to be created. For example, Room can provide a + * DataSource.Factory for a given SQL query: + * + *

+     * {@literal @}Dao
+     * interface UserDao {
+     *    {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
+     *    public abstract DataSource.Factory<Integer, User> usersByLastName();
+     * }
+     * 
+ * In the above sample, {@code Integer} is used because it is the {@code Key} type of + * PositionalDataSource. Currently, Room uses the {@code LIMIT}/{@code OFFSET} SQL keywords to + * page a large query with a PositionalDataSource. + * + * @param Key identifying items in DataSource. + * @param Type of items in the list loaded by the DataSources. + */ + public abstract static class Factory { + /** + * Create a DataSource. + *

+ * The DataSource should invalidate itself if the snapshot is no longer valid. If a + * DataSource becomes invalid, the only way to query more data is to create a new DataSource + * from the Factory. + *

+ * {@link LivePagedListBuilder} for example will construct a new PagedList and DataSource + * when the current DataSource is invalidated, and pass the new PagedList through the + * {@code LiveData} to observers. + * + * @return the new DataSource. + */ + @NonNull + public abstract DataSource create(); + + /** + * Applies the given function to each value emitted by DataSources produced by this Factory. + *

+ * Same as {@link #mapByPage(Function)}, but operates on individual items. + * + * @param function Function that runs on each loaded item, returning items of a potentially + * new type. + * @param Type of items produced by the new DataSource, from the passed function. + * + * @return A new DataSource.Factory, which transforms items using the given function. + * + * @see #mapByPage(Function) + * @see DataSource#map(Function) + * @see DataSource#mapByPage(Function) + */ + @NonNull + public DataSource.Factory map( + @NonNull Function function) { + return mapByPage(createListFunction(function)); + } + + /** + * Applies the given function to each value emitted by DataSources produced by this Factory. + *

+ * Same as {@link #map(Function)}, but allows for batch conversions. + * + * @param function Function that runs on each loaded page, returning items of a potentially + * new type. + * @param Type of items produced by the new DataSource, from the passed function. + * + * @return A new DataSource.Factory, which transforms items using the given function. + * + * @see #map(Function) + * @see DataSource#map(Function) + * @see DataSource#mapByPage(Function) + */ + @NonNull + public DataSource.Factory mapByPage( + @NonNull final Function, List> function) { + return new Factory() { + @Override + public DataSource create() { + return Factory.this.create().mapByPage(function); + } + }; + } + } + + @NonNull + static Function, List> createListFunction( + final @NonNull Function innerFunc) { + return new Function, List>() { + @Override + public List apply(@NonNull List source) { + List out = new ArrayList<>(source.size()); + for (int i = 0; i < source.size(); i++) { + out.add(innerFunc.apply(source.get(i))); + } + return out; + } + }; + } + + static List convert(Function, List> function, List source) { + List dest = function.apply(source); + if (dest.size() != source.size()) { + throw new IllegalStateException("Invalid Function " + function + + " changed return size. This is not supported."); + } + return dest; + } + + // Since we currently rely on implementation details of two implementations, + // prevent external subclassing, except through exposed subclasses + DataSource() { + } + + /** + * Applies the given function to each value emitted by the DataSource. + *

+ * Same as {@link #map(Function)}, but allows for batch conversions. + * + * @param function Function that runs on each loaded page, returning items of a potentially + * new type. + * @param Type of items produced by the new DataSource, from the passed function. + * + * @return A new DataSource, which transforms items using the given function. + * + * @see #map(Function) + * @see DataSource.Factory#map(Function) + * @see DataSource.Factory#mapByPage(Function) + */ + @NonNull + public abstract DataSource mapByPage( + @NonNull Function, List> function); + + /** + * Applies the given function to each value emitted by the DataSource. + *

+ * Same as {@link #mapByPage(Function)}, but operates on individual items. + * + * @param function Function that runs on each loaded item, returning items of a potentially + * new type. + * @param Type of items produced by the new DataSource, from the passed function. + * + * @return A new DataSource, which transforms items using the given function. + * + * @see #mapByPage(Function) + * @see DataSource.Factory#map(Function) + * @see DataSource.Factory#mapByPage(Function) + */ + @NonNull + public abstract DataSource map( + @NonNull Function function); + + /** + * Returns true if the data source guaranteed to produce a contiguous set of items, + * never producing gaps. + */ + abstract boolean isContiguous(); + + static class LoadCallbackHelper { + static void validateInitialLoadParams(@NonNull List data, int position, int totalCount) { + if (position < 0) { + throw new IllegalArgumentException("Position must be non-negative"); + } + if (data.size() + position > totalCount) { + throw new IllegalArgumentException( + "List size + position too large, last item in list beyond totalCount."); + } + if (data.size() == 0 && totalCount > 0) { + throw new IllegalArgumentException( + "Initial result cannot be empty if items are present in data set."); + } + } + + @PageResult.ResultType + final int mResultType; + private final DataSource mDataSource; + final PageResult.Receiver mReceiver; + + // mSignalLock protects mPostExecutor, and mHasSignalled + private final Object mSignalLock = new Object(); + private Executor mPostExecutor = null; + private boolean mHasSignalled = false; + + LoadCallbackHelper(@NonNull DataSource dataSource, @PageResult.ResultType int resultType, + @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) { + mDataSource = dataSource; + mResultType = resultType; + mPostExecutor = mainThreadExecutor; + mReceiver = receiver; + } + + void setPostExecutor(Executor postExecutor) { + synchronized (mSignalLock) { + mPostExecutor = postExecutor; + } + } + + /** + * Call before verifying args, or dispatching actul results + * + * @return true if DataSource was invalid, and invalid result dispatched + */ + boolean dispatchInvalidResultIfInvalid() { + if (mDataSource.isInvalid()) { + dispatchResultToReceiver(PageResult.getInvalidResult()); + return true; + } + return false; + } + + void dispatchResultToReceiver(final @NonNull PageResult result) { + Executor executor; + synchronized (mSignalLock) { + if (mHasSignalled) { + throw new IllegalStateException( + "callback.onResult already called, cannot call again."); + } + mHasSignalled = true; + executor = mPostExecutor; + } + + if (executor != null) { + executor.execute(new Runnable() { + @Override + public void run() { + mReceiver.onPageResult(mResultType, result); + } + }); + } else { + mReceiver.onPageResult(mResultType, result); + } + } + } + + /** + * Invalidation callback for DataSource. + *

+ * Used to signal when a DataSource a data source has become invalid, and that a new data source + * is needed to continue loading data. + */ + public interface InvalidatedCallback { + /** + * Called when the data backing the list has become invalid. This callback is typically used + * to signal that a new data source is needed. + *

+ * This callback will be invoked on the thread that calls {@link #invalidate()}. It is valid + * for the data source to invalidate itself during its load methods, or for an outside + * source to invalidate it. + */ + @AnyThread + void onInvalidated(); + } + + private AtomicBoolean mInvalid = new AtomicBoolean(false); + + private CopyOnWriteArrayList mOnInvalidatedCallbacks = + new CopyOnWriteArrayList<>(); + + /** + * Add a callback to invoke when the DataSource is first invalidated. + *

+ * Once invalidated, a data source will not become valid again. + *

+ * A data source will only invoke its callbacks once - the first time {@link #invalidate()} + * is called, on that thread. + * + * @param onInvalidatedCallback The callback, will be invoked on thread that + * {@link #invalidate()} is called on. + */ + @AnyThread + @SuppressWarnings("WeakerAccess") + public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { + mOnInvalidatedCallbacks.add(onInvalidatedCallback); + } + + /** + * Remove a previously added invalidate callback. + * + * @param onInvalidatedCallback The previously added callback. + */ + @AnyThread + @SuppressWarnings("WeakerAccess") + public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { + mOnInvalidatedCallbacks.remove(onInvalidatedCallback); + } + + /** + * Signal the data source to stop loading, and notify its callback. + *

+ * If invalidate has already been called, this method does nothing. + */ + @AnyThread + public void invalidate() { + if (mInvalid.compareAndSet(false, true)) { + for (InvalidatedCallback callback : mOnInvalidatedCallbacks) { + callback.onInvalidated(); + } + } + } + + /** + * Returns true if the data source is invalid, and can no longer be queried for data. + * + * @return True if the data source is invalid, and can no longer return data. + */ + @WorkerThread + public boolean isInvalid() { + return mInvalid.get(); + } +} diff --git a/app/src/main/java/androidx/paging/ItemKeyedDataSource.java b/app/src/main/java/androidx/paging/ItemKeyedDataSource.java new file mode 100644 index 0000000000..2e89ba6764 --- /dev/null +++ b/app/src/main/java/androidx/paging/ItemKeyedDataSource.java @@ -0,0 +1,375 @@ +/* + * 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.paging; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.arch.core.util.Function; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Incremental data loader for paging keyed content, where loaded content uses previously loaded + * items as input to future loads. + *

+ * Implement a DataSource using ItemKeyedDataSource if you need to use data from item {@code N - 1} + * to load item {@code N}. This is common, for example, in sorted database queries where + * attributes of the item such just before the next query define how to execute it. + *

+ * The {@code InMemoryByItemRepository} in the + * PagingWithNetworkSample + * shows how to implement a network ItemKeyedDataSource using + * Retrofit, while + * handling swipe-to-refresh, network errors, and retry. + * + * @param Type of data used to query Value types out of the DataSource. + * @param Type of items being loaded by the DataSource. + */ +public abstract class ItemKeyedDataSource extends ContiguousDataSource { + + /** + * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}. + * + * @param Type of data used to query Value types out of the DataSource. + */ + @SuppressWarnings("WeakerAccess") + public static class LoadInitialParams { + /** + * Load items around this key, or at the beginning of the data set if {@code null} is + * passed. + *

+ * Note that this key is generally a hint, and may be ignored if you want to always load + * from the beginning. + */ + @Nullable + public final Key requestedInitialKey; + + /** + * Requested number of items to load. + *

+ * Note that this may be larger than available data. + */ + public final int requestedLoadSize; + + /** + * Defines whether placeholders are enabled, and whether the total count passed to + * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored. + */ + public final boolean placeholdersEnabled; + + + public LoadInitialParams(@Nullable Key requestedInitialKey, int requestedLoadSize, + boolean placeholdersEnabled) { + this.requestedInitialKey = requestedInitialKey; + this.requestedLoadSize = requestedLoadSize; + this.placeholdersEnabled = placeholdersEnabled; + } + } + + /** + * Holder object for inputs to {@link #loadBefore(LoadParams, LoadCallback)} + * and {@link #loadAfter(LoadParams, LoadCallback)}. + * + * @param Type of data used to query Value types out of the DataSource. + */ + @SuppressWarnings("WeakerAccess") + public static class LoadParams { + /** + * Load items before/after this key. + *

+ * Returned data must begin directly adjacent to this position. + */ + @NonNull + public final Key key; + /** + * Requested number of items to load. + *

+ * Returned page can be of this size, but it may be altered if that is easier, e.g. a + * network data source where the backend defines page size. + */ + public final int requestedLoadSize; + + public LoadParams(@NonNull Key key, int requestedLoadSize) { + this.key = key; + this.requestedLoadSize = requestedLoadSize; + } + } + + /** + * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} + * to return data and, optionally, position/count information. + *

+ * A callback can be called only once, and will throw if called again. + *

+ * If you can compute the number of items in the data set before and after the loaded range, + * call the three parameter {@link #onResult(List, int, int)} to pass that information. You + * can skip passing this information by calling the single parameter {@link #onResult(List)}, + * either if it's difficult to compute, or if {@link LoadInitialParams#placeholdersEnabled} is + * {@code false}, so the positioning information will be ignored. + *

+ * It is always valid for a DataSource loading method that takes a callback to stash the + * callback and call it later. This enables DataSources to be fully asynchronous, and to handle + * temporary, recoverable error states (such as a network error that can be retried). + * + * @param Type of items being loaded. + */ + public abstract static class LoadInitialCallback extends LoadCallback { + /** + * Called to pass initial load state from a DataSource. + *

+ * Call this method from your DataSource's {@code loadInitial} function to return data, + * and inform how many placeholders should be shown before and after. If counting is cheap + * to compute (for example, if a network load returns the information regardless), it's + * recommended to pass data back through this method. + *

+ * It is always valid to pass a different amount of data than what is requested. Pass an + * empty list if there is no more data to load. + * + * @param data List of items loaded from the DataSource. If this is empty, the DataSource + * is treated as empty, and no further loads will occur. + * @param position Position of the item at the front of the list. If there are {@code N} + * items before the items in data that can be loaded from this DataSource, + * pass {@code N}. + * @param totalCount Total number of items that may be returned from this DataSource. + * Includes the number in the initial {@code data} parameter + * as well as any items that can be loaded in front or behind of + * {@code data}. + */ + public abstract void onResult(@NonNull List data, int position, int totalCount); + } + + + /** + * Callback for ItemKeyedDataSource {@link #loadBefore(LoadParams, LoadCallback)} + * and {@link #loadAfter(LoadParams, LoadCallback)} to return data. + *

+ * A callback can be called only once, and will throw if called again. + *

+ * It is always valid for a DataSource loading method that takes a callback to stash the + * callback and call it later. This enables DataSources to be fully asynchronous, and to handle + * temporary, recoverable error states (such as a network error that can be retried). + * + * @param Type of items being loaded. + */ + public abstract static class LoadCallback { + /** + * Called to pass loaded data from a DataSource. + *

+ * Call this method from your ItemKeyedDataSource's + * {@link #loadBefore(LoadParams, LoadCallback)} and + * {@link #loadAfter(LoadParams, LoadCallback)} methods to return data. + *

+ * Call this from {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} to + * initialize without counting available data, or supporting placeholders. + *

+ * It is always valid to pass a different amount of data than what is requested. Pass an + * empty list if there is no more data to load. + * + * @param data List of items loaded from the ItemKeyedDataSource. + */ + public abstract void onResult(@NonNull List data); + } + + static class LoadInitialCallbackImpl extends LoadInitialCallback { + final LoadCallbackHelper mCallbackHelper; + private final boolean mCountingEnabled; + LoadInitialCallbackImpl(@NonNull ItemKeyedDataSource dataSource, boolean countingEnabled, + @NonNull PageResult.Receiver receiver) { + mCallbackHelper = new LoadCallbackHelper<>(dataSource, PageResult.INIT, null, receiver); + mCountingEnabled = countingEnabled; + } + + @Override + public void onResult(@NonNull List data, int position, int totalCount) { + if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) { + LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount); + + int trailingUnloadedCount = totalCount - position - data.size(); + if (mCountingEnabled) { + mCallbackHelper.dispatchResultToReceiver(new PageResult<>( + data, position, trailingUnloadedCount, 0)); + } else { + mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position)); + } + } + } + + @Override + public void onResult(@NonNull List data) { + if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) { + mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0)); + } + } + } + + static class LoadCallbackImpl extends LoadCallback { + final LoadCallbackHelper mCallbackHelper; + + LoadCallbackImpl(@NonNull ItemKeyedDataSource dataSource, @PageResult.ResultType int type, + @Nullable Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + mCallbackHelper = new LoadCallbackHelper<>( + dataSource, type, mainThreadExecutor, receiver); + } + + @Override + public void onResult(@NonNull List data) { + if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) { + mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0)); + } + } + } + + @Nullable + @Override + final Key getKey(int position, Value item) { + if (item == null) { + return null; + } + + return getKey(item); + } + + @Override + final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize, + boolean enablePlaceholders, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + LoadInitialCallbackImpl callback = + new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver); + loadInitial(new LoadInitialParams<>(key, initialLoadSize, enablePlaceholders), callback); + + // If initialLoad's callback is not called within the body, we force any following calls + // to post to the UI thread. This constructor may be run on a background thread, but + // after constructor, mutation must happen on UI thread. + callback.mCallbackHelper.setPostExecutor(mainThreadExecutor); + } + + @Override + final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, + int pageSize, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + loadAfter(new LoadParams<>(getKey(currentEndItem), pageSize), + new LoadCallbackImpl<>(this, PageResult.APPEND, mainThreadExecutor, receiver)); + } + + @Override + final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, + int pageSize, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + loadBefore(new LoadParams<>(getKey(currentBeginItem), pageSize), + new LoadCallbackImpl<>(this, PageResult.PREPEND, mainThreadExecutor, receiver)); + } + + /** + * Load initial data. + *

+ * This method is called first to initialize a PagedList with data. If it's possible to count + * the items that can be loaded by the DataSource, it's recommended to pass the loaded data to + * the callback via the three-parameter + * {@link LoadInitialCallback#onResult(List, int, int)}. This enables PagedLists + * presenting data from this source to display placeholders to represent unloaded items. + *

+ * {@link LoadInitialParams#requestedInitialKey} and {@link LoadInitialParams#requestedLoadSize} + * are hints, not requirements, so they may be altered or ignored. Note that ignoring the + * {@code requestedInitialKey} can prevent subsequent PagedList/DataSource pairs from + * initializing at the same location. If your data source never invalidates (for example, + * loading from the network without the network ever signalling that old data must be reloaded), + * it's fine to ignore the {@code initialLoadKey} and always start from the beginning of the + * data set. + * + * @param params Parameters for initial load, including initial key and requested size. + * @param callback Callback that receives initial load data. + */ + public abstract void loadInitial(@NonNull LoadInitialParams params, + @NonNull LoadInitialCallback callback); + + /** + * Load list data after the key specified in {@link LoadParams#key LoadParams.key}. + *

+ * It's valid to return a different list size than the page size if it's easier, e.g. if your + * backend defines page sizes. It is generally safer to increase the number loaded than reduce. + *

+ * Data may be passed synchronously during the loadAfter method, or deferred and called at a + * later time. Further loads going down will be blocked until the callback is called. + *

+ * If data cannot be loaded (for example, if the request is invalid, or the data would be stale + * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source, + * and prevent further loading. + * + * @param params Parameters for the load, including the key to load after, and requested size. + * @param callback Callback that receives loaded data. + */ + public abstract void loadAfter(@NonNull LoadParams params, + @NonNull LoadCallback callback); + + /** + * Load list data before the key specified in {@link LoadParams#key LoadParams.key}. + *

+ * It's valid to return a different list size than the page size if it's easier, e.g. if your + * backend defines page sizes. It is generally safer to increase the number loaded than reduce. + *

+ *

Note: Data returned will be prepended just before the key + * passed, so if you vary size, ensure that the last item is adjacent to the passed key. + *

+ * Data may be passed synchronously during the loadBefore method, or deferred and called at a + * later time. Further loads going up will be blocked until the callback is called. + *

+ * If data cannot be loaded (for example, if the request is invalid, or the data would be stale + * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source, + * and prevent further loading. + * + * @param params Parameters for the load, including the key to load before, and requested size. + * @param callback Callback that receives loaded data. + */ + public abstract void loadBefore(@NonNull LoadParams params, + @NonNull LoadCallback callback); + + /** + * Return a key associated with the given item. + *

+ * If your ItemKeyedDataSource is loading from a source that is sorted and loaded by a unique + * integer ID, you would return {@code item.getID()} here. This key can then be passed to + * {@link #loadBefore(LoadParams, LoadCallback)} or + * {@link #loadAfter(LoadParams, LoadCallback)} to load additional items adjacent to the item + * passed to this function. + *

+ * If your key is more complex, such as when you're sorting by name, then resolving collisions + * with integer ID, you'll need to return both. In such a case you would use a wrapper class, + * such as {@code Pair} or, in Kotlin, + * {@code data class Key(val name: String, val id: Int)} + * + * @param item Item to get the key from. + * @return Key associated with given item. + */ + @NonNull + public abstract Key getKey(@NonNull Value item); + + @NonNull + @Override + public final ItemKeyedDataSource mapByPage( + @NonNull Function, List> function) { + return new WrapperItemKeyedDataSource<>(this, function); + } + + @NonNull + @Override + public final ItemKeyedDataSource map( + @NonNull Function function) { + return mapByPage(createListFunction(function)); + } +} diff --git a/app/src/main/java/androidx/paging/ListDataSource.java b/app/src/main/java/androidx/paging/ListDataSource.java new file mode 100644 index 0000000000..481d5f00d7 --- /dev/null +++ b/app/src/main/java/androidx/paging/ListDataSource.java @@ -0,0 +1,51 @@ +/* + * 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.paging; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +class ListDataSource extends PositionalDataSource { + private final List mList; + + public ListDataSource(List list) { + mList = new ArrayList<>(list); + } + + @Override + public void loadInitial(@NonNull LoadInitialParams params, + @NonNull LoadInitialCallback callback) { + final int totalCount = mList.size(); + + final int position = computeInitialLoadPosition(params, totalCount); + final int loadSize = computeInitialLoadSize(params, position, totalCount); + + // for simplicity, we could return everything immediately, + // but we tile here since it's expected behavior + List sublist = mList.subList(position, position + loadSize); + callback.onResult(sublist, position, totalCount); + } + + @Override + public void loadRange(@NonNull LoadRangeParams params, + @NonNull LoadRangeCallback callback) { + callback.onResult(mList.subList(params.startPosition, + params.startPosition + params.loadSize)); + } +} diff --git a/app/src/main/java/androidx/paging/LivePagedListBuilder.java b/app/src/main/java/androidx/paging/LivePagedListBuilder.java new file mode 100644 index 0000000000..481619d6d9 --- /dev/null +++ b/app/src/main/java/androidx/paging/LivePagedListBuilder.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 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 androidx.paging; + +import android.annotation.SuppressLint; + +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.arch.core.executor.ArchTaskExecutor; +import androidx.lifecycle.ComputableLiveData; +import androidx.lifecycle.LiveData; + +import java.util.concurrent.Executor; + +/** + * Builder for {@code LiveData}, given a {@link DataSource.Factory} and a + * {@link PagedList.Config}. + *

+ * The required parameters are in the constructor, so you can simply construct and build, or + * optionally enable extra features (such as initial load key, or BoundaryCallback). + * + * @param Type of input valued used to load data from the DataSource. Must be integer if + * you're using PositionalDataSource. + * @param Item type being presented. + */ +public final class LivePagedListBuilder { + private Key mInitialLoadKey; + private PagedList.Config mConfig; + private DataSource.Factory mDataSourceFactory; + private PagedList.BoundaryCallback mBoundaryCallback; + @SuppressLint("RestrictedApi") + private Executor mFetchExecutor = ArchTaskExecutor.getIOThreadExecutor(); + + /** + * Creates a LivePagedListBuilder with required parameters. + * + * @param dataSourceFactory DataSource factory providing DataSource generations. + * @param config Paging configuration. + */ + public LivePagedListBuilder(@NonNull DataSource.Factory dataSourceFactory, + @NonNull PagedList.Config config) { + //noinspection ConstantConditions + if (config == null) { + throw new IllegalArgumentException("PagedList.Config must be provided"); + } + //noinspection ConstantConditions + if (dataSourceFactory == null) { + throw new IllegalArgumentException("DataSource.Factory must be provided"); + } + + mDataSourceFactory = dataSourceFactory; + mConfig = config; + } + + /** + * Creates a LivePagedListBuilder with required parameters. + *

+ * This method is a convenience for: + *

+     * LivePagedListBuilder(dataSourceFactory,
+     *         new PagedList.Config.Builder().setPageSize(pageSize).build())
+     * 
+ * + * @param dataSourceFactory DataSource.Factory providing DataSource generations. + * @param pageSize Size of pages to load. + */ + public LivePagedListBuilder(@NonNull DataSource.Factory dataSourceFactory, + int pageSize) { + this(dataSourceFactory, new PagedList.Config.Builder().setPageSize(pageSize).build()); + } + + /** + * First loading key passed to the first PagedList/DataSource. + *

+ * When a new PagedList/DataSource pair is created after the first, it acquires a load key from + * the previous generation so that data is loaded around the position already being observed. + * + * @param key Initial load key passed to the first PagedList/DataSource. + * @return this + */ + @NonNull + public LivePagedListBuilder setInitialLoadKey(@Nullable Key key) { + mInitialLoadKey = key; + return this; + } + + /** + * Sets a {@link PagedList.BoundaryCallback} on each PagedList created, typically used to load + * additional data from network when paging from local storage. + *

+ * Pass a BoundaryCallback to listen to when the PagedList runs out of data to load. If this + * method is not called, or {@code null} is passed, you will not be notified when each + * DataSource runs out of data to provide to its PagedList. + *

+ * If you are paging from a DataSource.Factory backed by local storage, you can set a + * BoundaryCallback to know when there is no more information to page from local storage. + * This is useful to page from the network when local storage is a cache of network data. + *

+ * Note that when using a BoundaryCallback with a {@code LiveData}, method calls + * on the callback may be dispatched multiple times - one for each PagedList/DataSource + * pair. If loading network data from a BoundaryCallback, you should prevent multiple + * dispatches of the same method from triggering multiple simultaneous network loads. + * + * @param boundaryCallback The boundary callback for listening to PagedList load state. + * @return this + */ + @SuppressWarnings("unused") + @NonNull + public LivePagedListBuilder setBoundaryCallback( + @Nullable PagedList.BoundaryCallback boundaryCallback) { + mBoundaryCallback = boundaryCallback; + return this; + } + + /** + * Sets executor used for background fetching of PagedLists, and the pages within. + *

+ * If not set, defaults to the Arch components I/O thread pool. + * + * @param fetchExecutor Executor for fetching data from DataSources. + * @return this + */ + @SuppressWarnings("unused") + @NonNull + public LivePagedListBuilder setFetchExecutor( + @NonNull Executor fetchExecutor) { + mFetchExecutor = fetchExecutor; + return this; + } + + /** + * Constructs the {@code LiveData}. + *

+ * No work (such as loading) is done immediately, the creation of the first PagedList is is + * deferred until the LiveData is observed. + * + * @return The LiveData of PagedLists + */ + @NonNull + @SuppressLint("RestrictedApi") + public LiveData> build() { + return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory, + ArchTaskExecutor.getMainThreadExecutor(), mFetchExecutor); + } + + @AnyThread + @NonNull + @SuppressLint("RestrictedApi") + private static LiveData> create( + @Nullable final Key initialLoadKey, + @NonNull final PagedList.Config config, + @Nullable final PagedList.BoundaryCallback boundaryCallback, + @NonNull final DataSource.Factory dataSourceFactory, + @NonNull final Executor notifyExecutor, + @NonNull final Executor fetchExecutor) { + return new ComputableLiveData>(fetchExecutor) { + @Nullable + private PagedList mList; + @Nullable + private DataSource mDataSource; + + private final DataSource.InvalidatedCallback mCallback = + new DataSource.InvalidatedCallback() { + @Override + public void onInvalidated() { + invalidate(); + } + }; + + @SuppressWarnings("unchecked") // for casting getLastKey to Key + @Override + protected PagedList compute() { + @Nullable Key initializeKey = initialLoadKey; + if (mList != null) { + initializeKey = (Key) mList.getLastKey(); + } + + do { + if (mDataSource != null) { + mDataSource.removeInvalidatedCallback(mCallback); + } + + mDataSource = dataSourceFactory.create(); + mDataSource.addInvalidatedCallback(mCallback); + + mList = new PagedList.Builder<>(mDataSource, config) + .setNotifyExecutor(notifyExecutor) + .setFetchExecutor(fetchExecutor) + .setBoundaryCallback(boundaryCallback) + .setInitialKey(initializeKey) + .build(); + } while (mList.isDetached()); + return mList; + } + }.getLiveData(); + } +} diff --git a/app/src/main/java/androidx/paging/PageKeyedDataSource.java b/app/src/main/java/androidx/paging/PageKeyedDataSource.java new file mode 100644 index 0000000000..076d791079 --- /dev/null +++ b/app/src/main/java/androidx/paging/PageKeyedDataSource.java @@ -0,0 +1,445 @@ +/* + * 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.paging; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.arch.core.util.Function; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Incremental data loader for page-keyed content, where requests return keys for next/previous + * pages. + *

+ * Implement a DataSource using PageKeyedDataSource if you need to use data from page {@code N - 1} + * to load page {@code N}. This is common, for example, in network APIs that include a next/previous + * link or key with each page load. + *

+ * The {@code InMemoryByPageRepository} in the + * PagingWithNetworkSample + * shows how to implement a network PageKeyedDataSource using + * Retrofit, while + * handling swipe-to-refresh, network errors, and retry. + * + * @param Type of data used to query Value types out of the DataSource. + * @param Type of items being loaded by the DataSource. + */ +public abstract class PageKeyedDataSource extends ContiguousDataSource { + private final Object mKeyLock = new Object(); + + @Nullable + @GuardedBy("mKeyLock") + private Key mNextKey = null; + + @Nullable + @GuardedBy("mKeyLock") + private Key mPreviousKey = null; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void initKeys(@Nullable Key previousKey, @Nullable Key nextKey) { + synchronized (mKeyLock) { + mPreviousKey = previousKey; + mNextKey = nextKey; + } + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void setPreviousKey(@Nullable Key previousKey) { + synchronized (mKeyLock) { + mPreviousKey = previousKey; + } + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void setNextKey(@Nullable Key nextKey) { + synchronized (mKeyLock) { + mNextKey = nextKey; + } + } + + private @Nullable Key getPreviousKey() { + synchronized (mKeyLock) { + return mPreviousKey; + } + } + + private @Nullable Key getNextKey() { + synchronized (mKeyLock) { + return mNextKey; + } + } + + /** + * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}. + * + * @param Type of data used to query pages. + */ + @SuppressWarnings("WeakerAccess") + public static class LoadInitialParams { + /** + * Requested number of items to load. + *

+ * Note that this may be larger than available data. + */ + public final int requestedLoadSize; + + /** + * Defines whether placeholders are enabled, and whether the total count passed to + * {@link LoadInitialCallback#onResult(List, int, int, Key, Key)} will be ignored. + */ + public final boolean placeholdersEnabled; + + + public LoadInitialParams(int requestedLoadSize, boolean placeholdersEnabled) { + this.requestedLoadSize = requestedLoadSize; + this.placeholdersEnabled = placeholdersEnabled; + } + } + + /** + * Holder object for inputs to {@link #loadBefore(LoadParams, LoadCallback)} and + * {@link #loadAfter(LoadParams, LoadCallback)}. + * + * @param Type of data used to query pages. + */ + @SuppressWarnings("WeakerAccess") + public static class LoadParams { + /** + * Load items before/after this key. + *

+ * Returned data must begin directly adjacent to this position. + */ + @NonNull + public final Key key; + + /** + * Requested number of items to load. + *

+ * Returned page can be of this size, but it may be altered if that is easier, e.g. a + * network data source where the backend defines page size. + */ + public final int requestedLoadSize; + + public LoadParams(@NonNull Key key, int requestedLoadSize) { + this.key = key; + this.requestedLoadSize = requestedLoadSize; + } + } + + /** + * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} + * to return data and, optionally, position/count information. + *

+ * A callback can be called only once, and will throw if called again. + *

+ * If you can compute the number of items in the data set before and after the loaded range, + * call the five parameter {@link #onResult(List, int, int, Object, Object)} to pass that + * information. You can skip passing this information by calling the three parameter + * {@link #onResult(List, Object, Object)}, either if it's difficult to compute, or if + * {@link LoadInitialParams#placeholdersEnabled} is {@code false}, so the positioning + * information will be ignored. + *

+ * It is always valid for a DataSource loading method that takes a callback to stash the + * callback and call it later. This enables DataSources to be fully asynchronous, and to handle + * temporary, recoverable error states (such as a network error that can be retried). + * + * @param Type of data used to query pages. + * @param Type of items being loaded. + */ + public abstract static class LoadInitialCallback { + /** + * Called to pass initial load state from a DataSource. + *

+ * Call this method from your DataSource's {@code loadInitial} function to return data, + * and inform how many placeholders should be shown before and after. If counting is cheap + * to compute (for example, if a network load returns the information regardless), it's + * recommended to pass data back through this method. + *

+ * It is always valid to pass a different amount of data than what is requested. Pass an + * empty list if there is no more data to load. + * + * @param data List of items loaded from the DataSource. If this is empty, the DataSource + * is treated as empty, and no further loads will occur. + * @param position Position of the item at the front of the list. If there are {@code N} + * items before the items in data that can be loaded from this DataSource, + * pass {@code N}. + * @param totalCount Total number of items that may be returned from this DataSource. + * Includes the number in the initial {@code data} parameter + * as well as any items that can be loaded in front or behind of + * {@code data}. + */ + public abstract void onResult(@NonNull List data, int position, int totalCount, + @Nullable Key previousPageKey, @Nullable Key nextPageKey); + + /** + * Called to pass loaded data from a DataSource. + *

+ * Call this from {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} to + * initialize without counting available data, or supporting placeholders. + *

+ * It is always valid to pass a different amount of data than what is requested. Pass an + * empty list if there is no more data to load. + * + * @param data List of items loaded from the PageKeyedDataSource. + * @param previousPageKey Key for page before the initial load result, or {@code null} if no + * more data can be loaded before. + * @param nextPageKey Key for page after the initial load result, or {@code null} if no + * more data can be loaded after. + */ + public abstract void onResult(@NonNull List data, @Nullable Key previousPageKey, + @Nullable Key nextPageKey); + } + + /** + * Callback for PageKeyedDataSource {@link #loadBefore(LoadParams, LoadCallback)} and + * {@link #loadAfter(LoadParams, LoadCallback)} to return data. + *

+ * A callback can be called only once, and will throw if called again. + *

+ * It is always valid for a DataSource loading method that takes a callback to stash the + * callback and call it later. This enables DataSources to be fully asynchronous, and to handle + * temporary, recoverable error states (such as a network error that can be retried). + * + * @param Type of data used to query pages. + * @param Type of items being loaded. + */ + public abstract static class LoadCallback { + + /** + * Called to pass loaded data from a DataSource. + *

+ * Call this method from your PageKeyedDataSource's + * {@link #loadBefore(LoadParams, LoadCallback)} and + * {@link #loadAfter(LoadParams, LoadCallback)} methods to return data. + *

+ * It is always valid to pass a different amount of data than what is requested. Pass an + * empty list if there is no more data to load. + *

+ * Pass the key for the subsequent page to load to adjacentPageKey. For example, if you've + * loaded a page in {@link #loadBefore(LoadParams, LoadCallback)}, pass the key for the + * previous page, or {@code null} if the loaded page is the first. If in + * {@link #loadAfter(LoadParams, LoadCallback)}, pass the key for the next page, or + * {@code null} if the loaded page is the last. + * + * @param data List of items loaded from the PageKeyedDataSource. + * @param adjacentPageKey Key for subsequent page load (previous page in {@link #loadBefore} + * / next page in {@link #loadAfter}), or {@code null} if there are + * no more pages to load in the current load direction. + */ + public abstract void onResult(@NonNull List data, @Nullable Key adjacentPageKey); + } + + static class LoadInitialCallbackImpl extends LoadInitialCallback { + final LoadCallbackHelper mCallbackHelper; + private final PageKeyedDataSource mDataSource; + private final boolean mCountingEnabled; + LoadInitialCallbackImpl(@NonNull PageKeyedDataSource dataSource, + boolean countingEnabled, @NonNull PageResult.Receiver receiver) { + mCallbackHelper = new LoadCallbackHelper<>( + dataSource, PageResult.INIT, null, receiver); + mDataSource = dataSource; + mCountingEnabled = countingEnabled; + } + + @Override + public void onResult(@NonNull List data, int position, int totalCount, + @Nullable Key previousPageKey, @Nullable Key nextPageKey) { + if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) { + LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount); + + // setup keys before dispatching data, so guaranteed to be ready + mDataSource.initKeys(previousPageKey, nextPageKey); + + int trailingUnloadedCount = totalCount - position - data.size(); + if (mCountingEnabled) { + mCallbackHelper.dispatchResultToReceiver(new PageResult<>( + data, position, trailingUnloadedCount, 0)); + } else { + mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position)); + } + } + } + + @Override + public void onResult(@NonNull List data, @Nullable Key previousPageKey, + @Nullable Key nextPageKey) { + if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) { + mDataSource.initKeys(previousPageKey, nextPageKey); + mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0)); + } + } + } + + static class LoadCallbackImpl extends LoadCallback { + final LoadCallbackHelper mCallbackHelper; + private final PageKeyedDataSource mDataSource; + LoadCallbackImpl(@NonNull PageKeyedDataSource dataSource, + @PageResult.ResultType int type, @Nullable Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + mCallbackHelper = new LoadCallbackHelper<>( + dataSource, type, mainThreadExecutor, receiver); + mDataSource = dataSource; + } + + @Override + public void onResult(@NonNull List data, @Nullable Key adjacentPageKey) { + if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) { + if (mCallbackHelper.mResultType == PageResult.APPEND) { + mDataSource.setNextKey(adjacentPageKey); + } else { + mDataSource.setPreviousKey(adjacentPageKey); + } + mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0)); + } + } + } + + @Nullable + @Override + final Key getKey(int position, Value item) { + // don't attempt to persist keys, since we currently don't pass them to initial load + return null; + } + + @Override + boolean supportsPageDropping() { + /* To support page dropping when PageKeyed, we'll need to: + * - Stash keys for every page we have loaded (can id by index relative to loadInitial) + * - Drop keys for any page not adjacent to loaded content + * - And either: + * - Allow impl to signal previous page key: onResult(data, nextPageKey, prevPageKey) + * - Re-trigger loadInitial, and break assumption it will only occur once. + */ + return false; + } + + @Override + final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize, + boolean enablePlaceholders, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + LoadInitialCallbackImpl callback = + new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver); + loadInitial(new LoadInitialParams(initialLoadSize, enablePlaceholders), callback); + + // If initialLoad's callback is not called within the body, we force any following calls + // to post to the UI thread. This constructor may be run on a background thread, but + // after constructor, mutation must happen on UI thread. + callback.mCallbackHelper.setPostExecutor(mainThreadExecutor); + } + + + @Override + final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, + int pageSize, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + @Nullable Key key = getNextKey(); + if (key != null) { + loadAfter(new LoadParams<>(key, pageSize), + new LoadCallbackImpl<>(this, PageResult.APPEND, mainThreadExecutor, receiver)); + } else { + receiver.onPageResult(PageResult.APPEND, PageResult.getEmptyResult()); + } + } + + @Override + final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, + int pageSize, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + @Nullable Key key = getPreviousKey(); + if (key != null) { + loadBefore(new LoadParams<>(key, pageSize), + new LoadCallbackImpl<>(this, PageResult.PREPEND, mainThreadExecutor, receiver)); + } else { + receiver.onPageResult(PageResult.PREPEND, PageResult.getEmptyResult()); + } + } + + /** + * Load initial data. + *

+ * This method is called first to initialize a PagedList with data. If it's possible to count + * the items that can be loaded by the DataSource, it's recommended to pass the loaded data to + * the callback via the three-parameter + * {@link LoadInitialCallback#onResult(List, int, int, Object, Object)}. This enables PagedLists + * presenting data from this source to display placeholders to represent unloaded items. + *

+ * {@link LoadInitialParams#requestedLoadSize} is a hint, not a requirement, so it may be may be + * altered or ignored. + * + * @param params Parameters for initial load, including requested load size. + * @param callback Callback that receives initial load data. + */ + public abstract void loadInitial(@NonNull LoadInitialParams params, + @NonNull LoadInitialCallback callback); + + /** + * Prepend page with the key specified by {@link LoadParams#key LoadParams.key}. + *

+ * It's valid to return a different list size than the page size if it's easier, e.g. if your + * backend defines page sizes. It is generally safer to increase the number loaded than reduce. + *

+ * Data may be passed synchronously during the load method, or deferred and called at a + * later time. Further loads going down will be blocked until the callback is called. + *

+ * If data cannot be loaded (for example, if the request is invalid, or the data would be stale + * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source, + * and prevent further loading. + * + * @param params Parameters for the load, including the key for the new page, and requested load + * size. + * @param callback Callback that receives loaded data. + */ + public abstract void loadBefore(@NonNull LoadParams params, + @NonNull LoadCallback callback); + + /** + * Append page with the key specified by {@link LoadParams#key LoadParams.key}. + *

+ * It's valid to return a different list size than the page size if it's easier, e.g. if your + * backend defines page sizes. It is generally safer to increase the number loaded than reduce. + *

+ * Data may be passed synchronously during the load method, or deferred and called at a + * later time. Further loads going down will be blocked until the callback is called. + *

+ * If data cannot be loaded (for example, if the request is invalid, or the data would be stale + * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source, + * and prevent further loading. + * + * @param params Parameters for the load, including the key for the new page, and requested load + * size. + * @param callback Callback that receives loaded data. + */ + public abstract void loadAfter(@NonNull LoadParams params, + @NonNull LoadCallback callback); + + @NonNull + @Override + public final PageKeyedDataSource mapByPage( + @NonNull Function, List> function) { + return new WrapperPageKeyedDataSource<>(this, function); + } + + @NonNull + @Override + public final PageKeyedDataSource map( + @NonNull Function function) { + return mapByPage(createListFunction(function)); + } +} diff --git a/app/src/main/java/androidx/paging/PageResult.java b/app/src/main/java/androidx/paging/PageResult.java new file mode 100644 index 0000000000..e654a73627 --- /dev/null +++ b/app/src/main/java/androidx/paging/PageResult.java @@ -0,0 +1,105 @@ +/* + * 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.paging; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import androidx.annotation.IntDef; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; + +import java.lang.annotation.Retention; +import java.util.Collections; +import java.util.List; + +class PageResult { + /** + * Single empty instance to avoid allocations. + *

+ * Note, distinct from {@link #INVALID_RESULT} because {@link #isInvalid()} checks instance. + */ + @SuppressWarnings("unchecked") + private static final PageResult EMPTY_RESULT = + new PageResult(Collections.emptyList(), 0); + + @SuppressWarnings("unchecked") + private static final PageResult INVALID_RESULT = + new PageResult(Collections.emptyList(), 0); + + @SuppressWarnings("unchecked") + static PageResult getEmptyResult() { + return EMPTY_RESULT; + } + + @SuppressWarnings("unchecked") + static PageResult getInvalidResult() { + return INVALID_RESULT; + } + + @Retention(SOURCE) + @IntDef({INIT, APPEND, PREPEND, TILE}) + @interface ResultType {} + + static final int INIT = 0; + + // contiguous results + static final int APPEND = 1; + static final int PREPEND = 2; + + // non-contiguous, tile result + static final int TILE = 3; + + @NonNull + public final List page; + @SuppressWarnings("WeakerAccess") + public final int leadingNulls; + @SuppressWarnings("WeakerAccess") + public final int trailingNulls; + @SuppressWarnings("WeakerAccess") + public final int positionOffset; + + PageResult(@NonNull List list, int leadingNulls, int trailingNulls, int positionOffset) { + this.page = list; + this.leadingNulls = leadingNulls; + this.trailingNulls = trailingNulls; + this.positionOffset = positionOffset; + } + + PageResult(@NonNull List list, int positionOffset) { + this.page = list; + this.leadingNulls = 0; + this.trailingNulls = 0; + this.positionOffset = positionOffset; + } + + @Override + public String toString() { + return "Result " + leadingNulls + + ", " + page + + ", " + trailingNulls + + ", offset " + positionOffset; + } + + public boolean isInvalid() { + return this == INVALID_RESULT; + } + + abstract static class Receiver { + @MainThread + public abstract void onPageResult(@ResultType int type, @NonNull PageResult pageResult); + } +} diff --git a/app/src/main/java/androidx/paging/PagedList.java b/app/src/main/java/androidx/paging/PagedList.java new file mode 100644 index 0000000000..1d4a184efd --- /dev/null +++ b/app/src/main/java/androidx/paging/PagedList.java @@ -0,0 +1,1174 @@ +/* + * 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.paging; + +import androidx.annotation.AnyThread; +import androidx.annotation.IntRange; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.WorkerThread; + +import java.lang.ref.WeakReference; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Lazy loading list that pages in immutable content from a {@link DataSource}. + *

+ * A PagedList is a {@link List} which loads its data in chunks (pages) from a {@link DataSource}. + * Items can be accessed with {@link #get(int)}, and further loading can be triggered with + * {@link #loadAround(int)}. To display a PagedList, see {@link PagedListAdapter}, which enables the + * binding of a PagedList to a {@link androidx.recyclerview.widget.RecyclerView}. + *

Loading Data

+ *

+ * All data in a PagedList is loaded from its {@link DataSource}. Creating a PagedList loads the + * first chunk of data from the DataSource immediately, and should for this reason be done on a + * background thread. The constructed PagedList may then be passed to and used on the UI thread. + * This is done to prevent passing a list with no loaded content to the UI thread, which should + * generally not be presented to the user. + *

+ * A PagedList initially presents this first partial load as its content, and expands over time as + * content is loaded in. When {@link #loadAround} is called, items will be loaded in near the passed + * list index. If placeholder {@code null}s are present in the list, they will be replaced as + * content is loaded. If not, newly loaded items will be inserted at the beginning or end of the + * list. + *

+ * PagedList can present data for an unbounded, infinite scrolling list, or a very large but + * countable list. Use {@link Config} to control how many items a PagedList loads, and when. + *

+ * If you use {@link LivePagedListBuilder} to get a + * {@link androidx.lifecycle.LiveData}<PagedList>, it will initialize PagedLists on a + * background thread for you. + *

Placeholders

+ *

+ * There are two ways that PagedList can represent its not-yet-loaded data - with or without + * {@code null} placeholders. + *

+ * With placeholders, the PagedList is always the full size of the data set. {@code get(N)} returns + * the {@code N}th item in the data set, or {@code null} if its not yet loaded. + *

+ * Without {@code null} placeholders, the PagedList is the sublist of data that has already been + * loaded. The size of the PagedList is the number of currently loaded items, and {@code get(N)} + * returns the {@code N}th loaded item. This is not necessarily the {@code N}th item in the + * data set. + *

+ * Placeholders have several benefits: + *

    + *
  • They express the full sized list to the presentation layer (often a + * {@link PagedListAdapter}), and so can support scrollbars (without jumping as pages are + * loaded or dropped) and fast-scrolling to any position, loaded or not. + *
  • They avoid the need for a loading spinner at the end of the loaded list, since the list + * is always full sized. + *
+ *

+ * They also have drawbacks: + *

    + *
  • Your Adapter needs to account for {@code null} items. This often means providing default + * values in data you bind to a {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}. + *
  • They don't work well if your item views are of different sizes, as this will prevent + * loading items from cross-fading nicely. + *
  • They require you to count your data set, which can be expensive or impossible, depending + * on your DataSource. + *
+ *

+ * Placeholders are enabled by default, but can be disabled in two ways. They are disabled if the + * DataSource does not count its data set in its initial load, or if {@code false} is passed to + * {@link Config.Builder#setEnablePlaceholders(boolean)} when building a {@link Config}. + *

Mutability and Snapshots

+ * A PagedList is mutable while loading, or ready to load from its DataSource. + * As loads succeed, a mutable PagedList will be updated via Runnables on the main thread. You can + * listen to these updates with a {@link Callback}. (Note that {@link PagedListAdapter} will listen + * to these to signal RecyclerView about the updates/changes). + *

+ * If a PagedList attempts to load from an invalid DataSource, it will {@link #detach()} + * from the DataSource, meaning that it will no longer attempt to load data. It will return true + * from {@link #isImmutable()}, and a new DataSource / PagedList pair must be created to load + * further data. See {@link DataSource} and {@link LivePagedListBuilder} for how new PagedLists are + * created to represent changed data. + *

+ * A PagedList snapshot is simply an immutable shallow copy of the current state of the PagedList as + * a {@code List}. It will reference the same inner items, and contain the same {@code null} + * placeholders, if present. + * + * @param The type of the entries in the list. + */ +public abstract class PagedList extends AbstractList { + + // Notes on threading: + // + // PagedList and its subclasses are passed and accessed on multiple threads, but are always + // owned by a single thread. During initialization, this is the creation thread, generally the + // fetchExecutor/fetchScheduler when using LiveData/RxJava. After initialization, the PagedList + // is owned by the main thread (or notifyScheduler, if overridden in RxJava). + // + // The only exception is detach()/isDetached(), which can be called from the fetch thread. + // However these methods simply wrap a atomic boolean, so are safe. + // + // The PageResult.Receiver that receives new data from the DataSource is always run on the + // owning thread. + + @NonNull + final Executor mMainThreadExecutor; + @NonNull + final Executor mBackgroundThreadExecutor; + @Nullable + final BoundaryCallback mBoundaryCallback; + @NonNull + final Config mConfig; + @NonNull + final PagedStorage mStorage; + + /** + * Last access location, in total position space (including offset). + *

+ * Used by positional data + * sources to initialize loading near viewport + */ + int mLastLoad = 0; + T mLastItem = null; + + final int mRequiredRemainder; + + // if set to true, mBoundaryCallback is non-null, and should + // be dispatched when nearby load has occurred + @SuppressWarnings("WeakerAccess") /* synthetic access */ + boolean mBoundaryCallbackBeginDeferred = false; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + boolean mBoundaryCallbackEndDeferred = false; + + // lowest and highest index accessed by loadAround. Used to + // decide when mBoundaryCallback should be dispatched + private int mLowestIndexAccessed = Integer.MAX_VALUE; + private int mHighestIndexAccessed = Integer.MIN_VALUE; + + private final AtomicBoolean mDetached = new AtomicBoolean(false); + + private final ArrayList> mCallbacks = new ArrayList<>(); + + PagedList(@NonNull PagedStorage storage, + @NonNull Executor mainThreadExecutor, + @NonNull Executor backgroundThreadExecutor, + @Nullable BoundaryCallback boundaryCallback, + @NonNull Config config) { + mStorage = storage; + mMainThreadExecutor = mainThreadExecutor; + mBackgroundThreadExecutor = backgroundThreadExecutor; + mBoundaryCallback = boundaryCallback; + mConfig = config; + mRequiredRemainder = mConfig.prefetchDistance * 2 + mConfig.pageSize; + } + + /** + * Create a PagedList which loads data from the provided data source on a background thread, + * posting updates to the main thread. + * + * + * @param dataSource DataSource providing data to the PagedList + * @param notifyExecutor Thread that will use and consume data from the PagedList. + * Generally, this is the UI/main thread. + * @param fetchExecutor Data loading will be done via this executor - + * should be a background thread. + * @param boundaryCallback Optional boundary callback to attach to the list. + * @param config PagedList Config, which defines how the PagedList will load data. + * @param Key type that indicates to the DataSource what data to load. + * @param Type of items to be held and loaded by the PagedList. + * + * @return Newly created PagedList, which will page in data from the DataSource as needed. + */ + @NonNull + @SuppressWarnings("WeakerAccess") /* synthetic access */ + static PagedList create(@NonNull DataSource dataSource, + @NonNull Executor notifyExecutor, + @NonNull Executor fetchExecutor, + @Nullable BoundaryCallback boundaryCallback, + @NonNull Config config, + @Nullable K key) { + if (dataSource.isContiguous() || !config.enablePlaceholders) { + int lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED; + if (!dataSource.isContiguous()) { + //noinspection unchecked + dataSource = (DataSource) ((PositionalDataSource) dataSource) + .wrapAsContiguousWithoutPlaceholders(); + if (key != null) { + lastLoad = (Integer) key; + } + } + ContiguousDataSource contigDataSource = (ContiguousDataSource) dataSource; + return new ContiguousPagedList<>(contigDataSource, + notifyExecutor, + fetchExecutor, + boundaryCallback, + config, + key, + lastLoad); + } else { + return new TiledPagedList<>((PositionalDataSource) dataSource, + notifyExecutor, + fetchExecutor, + boundaryCallback, + config, + (key != null) ? (Integer) key : 0); + } + } + + /** + * Builder class for PagedList. + *

+ * DataSource, Config, main thread and background executor must all be provided. + *

+ * A PagedList queries initial data from its DataSource during construction, to avoid empty + * PagedLists being presented to the UI when possible. It's preferred to present initial data, + * so that the UI doesn't show an empty list, or placeholders for a few frames, just before + * showing initial content. + *

+ * {@link LivePagedListBuilder} does this creation on a background thread automatically, if you + * want to receive a {@code LiveData>}. + * + * @param Type of key used to load data from the DataSource. + * @param Type of items held and loaded by the PagedList. + */ + @SuppressWarnings("WeakerAccess") + public static final class Builder { + private final DataSource mDataSource; + private final Config mConfig; + private Executor mNotifyExecutor; + private Executor mFetchExecutor; + private BoundaryCallback mBoundaryCallback; + private Key mInitialKey; + + /** + * Create a PagedList.Builder with the provided {@link DataSource} and {@link Config}. + * + * @param dataSource DataSource the PagedList will load from. + * @param config Config that defines how the PagedList loads data from its DataSource. + */ + public Builder(@NonNull DataSource dataSource, @NonNull Config config) { + //noinspection ConstantConditions + if (dataSource == null) { + throw new IllegalArgumentException("DataSource may not be null"); + } + //noinspection ConstantConditions + if (config == null) { + throw new IllegalArgumentException("Config may not be null"); + } + mDataSource = dataSource; + mConfig = config; + } + + /** + * Create a PagedList.Builder with the provided {@link DataSource} and page size. + *

+ * This method is a convenience for: + *

+         * PagedList.Builder(dataSource,
+         *         new PagedList.Config.Builder().setPageSize(pageSize).build());
+         * 
+ * + * @param dataSource DataSource the PagedList will load from. + * @param pageSize Config that defines how the PagedList loads data from its DataSource. + */ + public Builder(@NonNull DataSource dataSource, int pageSize) { + this(dataSource, new PagedList.Config.Builder().setPageSize(pageSize).build()); + } + /** + * The executor defining where page loading updates are dispatched. + * + * @param notifyExecutor Executor that receives PagedList updates, and where + * {@link Callback} calls are dispatched. Generally, this is the ui/main thread. + * @return this + */ + @NonNull + public Builder setNotifyExecutor(@NonNull Executor notifyExecutor) { + mNotifyExecutor = notifyExecutor; + return this; + } + + /** + * The executor used to fetch additional pages from the DataSource. + * + * Does not affect initial load, which will be done immediately on whichever thread the + * PagedList is created on. + * + * @param fetchExecutor Executor used to fetch from DataSources, generally a background + * thread pool for e.g. I/O or network loading. + * @return this + */ + @NonNull + public Builder setFetchExecutor(@NonNull Executor fetchExecutor) { + mFetchExecutor = fetchExecutor; + return this; + } + + /** + * The BoundaryCallback for out of data events. + *

+ * Pass a BoundaryCallback to listen to when the PagedList runs out of data to load. + * + * @param boundaryCallback BoundaryCallback for listening to out-of-data events. + * @return this + */ + @SuppressWarnings("unused") + @NonNull + public Builder setBoundaryCallback( + @Nullable BoundaryCallback boundaryCallback) { + mBoundaryCallback = boundaryCallback; + return this; + } + + /** + * Sets the initial key the DataSource should load around as part of initialization. + * + * @param initialKey Key the DataSource should load around as part of initialization. + * @return this + */ + @NonNull + public Builder setInitialKey(@Nullable Key initialKey) { + mInitialKey = initialKey; + return this; + } + + /** + * Creates a {@link PagedList} with the given parameters. + *

+ * This call will dispatch the {@link DataSource}'s loadInitial method immediately. If a + * DataSource posts all of its work (e.g. to a network thread), the PagedList will + * be immediately created as empty, and grow to its initial size when the initial load + * completes. + *

+ * If the DataSource implements its load synchronously, doing the load work immediately in + * the loadInitial method, the PagedList will block on that load before completing + * construction. In this case, use a background thread to create a PagedList. + *

+ * It's fine to create a PagedList with an async DataSource on the main thread, such as in + * the constructor of a ViewModel. An async network load won't block the initialLoad + * function. For a synchronous DataSource such as one created from a Room database, a + * {@code LiveData} can be safely constructed with {@link LivePagedListBuilder} + * on the main thread, since actual construction work is deferred, and done on a background + * thread. + *

+ * While build() will always return a PagedList, it's important to note that the PagedList + * initial load may fail to acquire data from the DataSource. This can happen for example if + * the DataSource is invalidated during its initial load. If this happens, the PagedList + * will be immediately {@link PagedList#isDetached() detached}, and you can retry + * construction (including setting a new DataSource). + * + * @return The newly constructed PagedList + */ + @WorkerThread + @NonNull + public PagedList build() { + // TODO: define defaults, once they can be used in module without android dependency + if (mNotifyExecutor == null) { + throw new IllegalArgumentException("MainThreadExecutor required"); + } + if (mFetchExecutor == null) { + throw new IllegalArgumentException("BackgroundThreadExecutor required"); + } + + //noinspection unchecked + return PagedList.create( + mDataSource, + mNotifyExecutor, + mFetchExecutor, + mBoundaryCallback, + mConfig, + mInitialKey); + } + } + + /** + * Get the item in the list of loaded items at the provided index. + * + * @param index Index in the loaded item list. Must be >= 0, and < {@link #size()} + * @return The item at the passed index, or null if a null placeholder is at the specified + * position. + * + * @see #size() + */ + @Override + @Nullable + public T get(int index) { + T item = mStorage.get(index); + if (item != null) { + mLastItem = item; + } + return item; + } + + /** + * Load adjacent items to passed index. + * + * @param index Index at which to load. + */ + public void loadAround(int index) { + if (index < 0 || index >= size()) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size()); + } + + mLastLoad = index + getPositionOffset(); + loadAroundInternal(index); + + mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index); + mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index); + + /* + * mLowestIndexAccessed / mHighestIndexAccessed have been updated, so check if we need to + * dispatch boundary callbacks. Boundary callbacks are deferred until last items are loaded, + * and accesses happen near the boundaries. + * + * Note: we post here, since RecyclerView may want to add items in response, and this + * call occurs in PagedListAdapter bind. + */ + tryDispatchBoundaryCallbacks(true); + } + + // Creation thread for initial synchronous load, otherwise main thread + // Safe to access main thread only state - no other thread has reference during construction + @AnyThread + void deferBoundaryCallbacks(final boolean deferEmpty, + final boolean deferBegin, final boolean deferEnd) { + if (mBoundaryCallback == null) { + throw new IllegalStateException("Can't defer BoundaryCallback, no instance"); + } + + /* + * If lowest/highest haven't been initialized, set them to storage size, + * since placeholders must already be computed by this point. + * + * This is just a minor optimization so that BoundaryCallback callbacks are sent immediately + * if the initial load size is smaller than the prefetch window (see + * TiledPagedListTest#boundaryCallback_immediate()) + */ + if (mLowestIndexAccessed == Integer.MAX_VALUE) { + mLowestIndexAccessed = mStorage.size(); + } + if (mHighestIndexAccessed == Integer.MIN_VALUE) { + mHighestIndexAccessed = 0; + } + + if (deferEmpty || deferBegin || deferEnd) { + // Post to the main thread, since we may be on creation thread currently + mMainThreadExecutor.execute(new Runnable() { + @Override + public void run() { + // on is dispatched immediately, since items won't be accessed + //noinspection ConstantConditions + if (deferEmpty) { + mBoundaryCallback.onZeroItemsLoaded(); + } + + // for other callbacks, mark deferred, and only dispatch if loadAround + // has been called near to the position + if (deferBegin) { + mBoundaryCallbackBeginDeferred = true; + } + if (deferEnd) { + mBoundaryCallbackEndDeferred = true; + } + tryDispatchBoundaryCallbacks(false); + } + }); + } + } + + /** + * Call this when mLowest/HighestIndexAccessed are changed, or + * mBoundaryCallbackBegin/EndDeferred is set. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void tryDispatchBoundaryCallbacks(boolean post) { + final boolean dispatchBegin = mBoundaryCallbackBeginDeferred + && mLowestIndexAccessed <= mConfig.prefetchDistance; + final boolean dispatchEnd = mBoundaryCallbackEndDeferred + && mHighestIndexAccessed >= size() - 1 - mConfig.prefetchDistance; + + if (!dispatchBegin && !dispatchEnd) { + return; + } + + if (dispatchBegin) { + mBoundaryCallbackBeginDeferred = false; + } + if (dispatchEnd) { + mBoundaryCallbackEndDeferred = false; + } + if (post) { + mMainThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd); + } + }); + } else { + dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd); + } + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void dispatchBoundaryCallbacks(boolean begin, boolean end) { + // safe to deref mBoundaryCallback here, since we only defer if mBoundaryCallback present + if (begin) { + //noinspection ConstantConditions + mBoundaryCallback.onItemAtFrontLoaded(mStorage.getFirstLoadedItem()); + } + if (end) { + //noinspection ConstantConditions + mBoundaryCallback.onItemAtEndLoaded(mStorage.getLastLoadedItem()); + } + } + + /** @hide */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + void offsetAccessIndices(int offset) { + // update last loadAround index + mLastLoad += offset; + + // update access range + mLowestIndexAccessed += offset; + mHighestIndexAccessed += offset; + } + + /** + * Returns size of the list, including any not-yet-loaded null padding. + * + * To get the number of loaded items, not counting placeholders, use {@link #getLoadedCount()}. + * + * @return Current total size of the list, including placeholders. + * + * @see #getLoadedCount() + */ + @Override + public int size() { + return mStorage.size(); + } + + /** + * Returns the number of items loaded in the PagedList. + * + * Unlike {@link #size()} this counts only loaded items, not placeholders. + *

+ * If placeholders are {@link Config#enablePlaceholders disabled}, this method is equivalent to + * {@link #size()}. + * + * @return Number of items currently loaded, not counting placeholders. + * + * @see #size() + */ + public int getLoadedCount() { + return mStorage.getLoadedCount(); + } + + /** + * Returns whether the list is immutable. + * + * Immutable lists may not become mutable again, and may safely be accessed from any thread. + *

+ * In the future, this method may return true when a PagedList has completed loading from its + * DataSource. Currently, it is equivalent to {@link #isDetached()}. + * + * @return True if the PagedList is immutable. + */ + @SuppressWarnings("WeakerAccess") + public boolean isImmutable() { + return isDetached(); + } + + /** + * Returns an immutable snapshot of the PagedList in its current state. + * + * If this PagedList {@link #isImmutable() is immutable} due to its DataSource being invalid, it + * will be returned. + * + * @return Immutable snapshot of PagedList data. + */ + @SuppressWarnings("WeakerAccess") + @NonNull + public List snapshot() { + if (isImmutable()) { + return this; + } + return new SnapshotPagedList<>(this); + } + + abstract boolean isContiguous(); + + /** + * Return the Config used to construct this PagedList. + * + * @return the Config of this PagedList + */ + @NonNull + public Config getConfig() { + return mConfig; + } + + /** + * Return the DataSource that provides data to this PagedList. + * + * @return the DataSource of this PagedList. + */ + @NonNull + public abstract DataSource getDataSource(); + + /** + * Return the key for the position passed most recently to {@link #loadAround(int)}. + *

+ * When a PagedList is invalidated, you can pass the key returned by this function to initialize + * the next PagedList. This ensures (depending on load times) that the next PagedList that + * arrives will have data that overlaps. If you use {@link LivePagedListBuilder}, it will do + * this for you. + * + * @return Key of position most recently passed to {@link #loadAround(int)}. + */ + @Nullable + public abstract Object getLastKey(); + + /** + * True if the PagedList has detached the DataSource it was loading from, and will no longer + * load new data. + *

+ * A detached list is {@link #isImmutable() immutable}. + * + * @return True if the data source is detached. + */ + @SuppressWarnings("WeakerAccess") + public boolean isDetached() { + return mDetached.get(); + } + + /** + * Detach the PagedList from its DataSource, and attempt to load no more data. + *

+ * This is called automatically when a DataSource load returns null, which is a + * signal to stop loading. The PagedList will continue to present existing data, but will not + * initiate new loads. + */ + @SuppressWarnings("WeakerAccess") + public void detach() { + mDetached.set(true); + } + + /** + * Position offset of the data in the list. + *

+ * If data is supplied by a {@link PositionalDataSource}, the item returned from + * get(i) has a position of i + getPositionOffset(). + *

+ * If the DataSource is a {@link ItemKeyedDataSource} or {@link PageKeyedDataSource}, it + * doesn't use positions, returns 0. + */ + public int getPositionOffset() { + return mStorage.getPositionOffset(); + } + + /** + * Adds a callback, and issues updates since the previousSnapshot was created. + *

+ * If previousSnapshot is passed, the callback will also immediately be dispatched any + * differences between the previous snapshot, and the current state. For example, if the + * previousSnapshot was of 5 nulls, 10 items, 5 nulls, and the current state was 5 nulls, + * 12 items, 3 nulls, the callback would immediately receive a call of + * onChanged(14, 2). + *

+ * This allows an observer that's currently presenting a snapshot to catch up to the most recent + * version, including any changes that may have been made. + *

+ * The callback is internally held as weak reference, so PagedList doesn't hold a strong + * reference to its observer, such as a {@link PagedListAdapter}. If an adapter were held with a + * strong reference, it would be necessary to clear its PagedList observer before it could be + * GC'd. + * + * @param previousSnapshot Snapshot previously captured from this List, or null. + * @param callback Callback to dispatch to. + * + * @see #removeWeakCallback(Callback) + */ + @SuppressWarnings("WeakerAccess") + public void addWeakCallback(@Nullable List previousSnapshot, @NonNull Callback callback) { + if (previousSnapshot != null && previousSnapshot != this) { + + if (previousSnapshot.isEmpty()) { + if (!mStorage.isEmpty()) { + // If snapshot is empty, diff is trivial - just notify number new items. + // Note: occurs in async init, when snapshot taken before init page arrives + callback.onInserted(0, mStorage.size()); + } + } else { + PagedList storageSnapshot = (PagedList) previousSnapshot; + + //noinspection unchecked + dispatchUpdatesSinceSnapshot(storageSnapshot, callback); + } + } + + // first, clean up any empty weak refs + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + final Callback currentCallback = mCallbacks.get(i).get(); + if (currentCallback == null) { + mCallbacks.remove(i); + } + } + + // then add the new one + mCallbacks.add(new WeakReference<>(callback)); + } + /** + * Removes a previously added callback. + * + * @param callback Callback, previously added. + * @see #addWeakCallback(List, Callback) + */ + @SuppressWarnings("WeakerAccess") + public void removeWeakCallback(@NonNull Callback callback) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + final Callback currentCallback = mCallbacks.get(i).get(); + if (currentCallback == null || currentCallback == callback) { + // found callback, or empty weak ref + mCallbacks.remove(i); + } + } + } + + void notifyInserted(int position, int count) { + if (count != 0) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + final Callback callback = mCallbacks.get(i).get(); + if (callback != null) { + callback.onInserted(position, count); + } + } + } + } + + void notifyChanged(int position, int count) { + if (count != 0) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + final Callback callback = mCallbacks.get(i).get(); + + if (callback != null) { + callback.onChanged(position, count); + } + } + } + } + + void notifyRemoved(int position, int count) { + if (count != 0) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + final Callback callback = mCallbacks.get(i).get(); + + if (callback != null) { + callback.onRemoved(position, count); + } + } + } + } + + /** + * Dispatch updates since the non-empty snapshot was taken. + * + * @param snapshot Non-empty snapshot. + * @param callback Callback for updates that have occurred since snapshot. + */ + abstract void dispatchUpdatesSinceSnapshot(@NonNull PagedList snapshot, + @NonNull Callback callback); + + abstract void loadAroundInternal(int index); + + /** + * Callback signaling when content is loaded into the list. + *

+ * Can be used to listen to items being paged in and out. These calls will be dispatched on + * the executor defined by {@link Builder#setNotifyExecutor(Executor)}, which is generally + * the main/UI thread. + */ + public abstract static class Callback { + /** + * Called when null padding items have been loaded to signal newly available data, or when + * data that hasn't been used in a while has been dropped, and swapped back to null. + * + * @param position Position of first newly loaded items, out of total number of items + * (including padded nulls). + * @param count Number of items loaded. + */ + public abstract void onChanged(int position, int count); + + /** + * Called when new items have been loaded at the end or beginning of the list. + * + * @param position Position of the first newly loaded item (in practice, either + * 0 or size - 1. + * @param count Number of items loaded. + */ + public abstract void onInserted(int position, int count); + + /** + * Called when items have been removed at the end or beginning of the list, and have not + * been replaced by padded nulls. + * + * @param position Position of the first newly loaded item (in practice, either + * 0 or size - 1. + * @param count Number of items loaded. + */ + @SuppressWarnings("unused") + public abstract void onRemoved(int position, int count); + } + + /** + * Configures how a PagedList loads content from its DataSource. + *

+ * Use a Config {@link Builder} to construct and define custom loading behavior, such as + * {@link Builder#setPageSize(int)}, which defines number of items loaded at a time}. + */ + public static class Config { + /** + * When {@link #maxSize} is set to {@code MAX_SIZE_UNBOUNDED}, the maximum number of items + * loaded is unbounded, and pages will never be dropped. + */ + @SuppressWarnings("WeakerAccess") + public static final int MAX_SIZE_UNBOUNDED = Integer.MAX_VALUE; + + /** + * Size of each page loaded by the PagedList. + */ + public final int pageSize; + + /** + * Prefetch distance which defines how far ahead to load. + *

+ * If this value is set to 50, the paged list will attempt to load 50 items in advance of + * data that's already been accessed. + * + * @see PagedList#loadAround(int) + */ + @SuppressWarnings("WeakerAccess") + public final int prefetchDistance; + + /** + * Defines whether the PagedList may display null placeholders, if the DataSource provides + * them. + */ + @SuppressWarnings("WeakerAccess") + public final boolean enablePlaceholders; + + /** + * Defines the maximum number of items that may be loaded into this pagedList before pages + * should be dropped. + *

+ * {@link PageKeyedDataSource} does not currently support dropping pages - when + * loading from a {@code PageKeyedDataSource}, this value is ignored. + * + * @see #MAX_SIZE_UNBOUNDED + * @see Builder#setMaxSize(int) + */ + public final int maxSize; + + /** + * Size hint for initial load of PagedList, often larger than a regular page. + */ + @SuppressWarnings("WeakerAccess") + public final int initialLoadSizeHint; + + Config(int pageSize, int prefetchDistance, + boolean enablePlaceholders, int initialLoadSizeHint, int maxSize) { + this.pageSize = pageSize; + this.prefetchDistance = prefetchDistance; + this.enablePlaceholders = enablePlaceholders; + this.initialLoadSizeHint = initialLoadSizeHint; + this.maxSize = maxSize; + } + + /** + * Builder class for {@link Config}. + *

+ * You must at minimum specify page size with {@link #setPageSize(int)}. + */ + public static final class Builder { + static final int DEFAULT_INITIAL_PAGE_MULTIPLIER = 3; + + private int mPageSize = -1; + private int mPrefetchDistance = -1; + private int mInitialLoadSizeHint = -1; + private boolean mEnablePlaceholders = true; + private int mMaxSize = MAX_SIZE_UNBOUNDED; + + /** + * Defines the number of items loaded at once from the DataSource. + *

+ * Should be several times the number of visible items onscreen. + *

+ * Configuring your page size depends on how your data is being loaded and used. Smaller + * page sizes improve memory usage, latency, and avoid GC churn. Larger pages generally + * improve loading throughput, to a point + * (avoid loading more than 2MB from SQLite at once, since it incurs extra cost). + *

+ * If you're loading data for very large, social-media style cards that take up most of + * a screen, and your database isn't a bottleneck, 10-20 may make sense. If you're + * displaying dozens of items in a tiled grid, which can present items during a scroll + * much more quickly, consider closer to 100. + * + * @param pageSize Number of items loaded at once from the DataSource. + * @return this + */ + @NonNull + public Builder setPageSize(@IntRange(from = 1) int pageSize) { + if (pageSize < 1) { + throw new IllegalArgumentException("Page size must be a positive number"); + } + mPageSize = pageSize; + return this; + } + + /** + * Defines how far from the edge of loaded content an access must be to trigger further + * loading. + *

+ * Should be several times the number of visible items onscreen. + *

+ * If not set, defaults to page size. + *

+ * A value of 0 indicates that no list items will be loaded until they are specifically + * requested. This is generally not recommended, so that users don't observe a + * placeholder item (with placeholders) or end of list (without) while scrolling. + * + * @param prefetchDistance Distance the PagedList should prefetch. + * @return this + */ + @NonNull + public Builder setPrefetchDistance(@IntRange(from = 0) int prefetchDistance) { + mPrefetchDistance = prefetchDistance; + return this; + } + + /** + * Pass false to disable null placeholders in PagedLists using this Config. + *

+ * If not set, defaults to true. + *

+ * A PagedList will present null placeholders for not-yet-loaded content if two + * conditions are met: + *

+ * 1) Its DataSource can count all unloaded items (so that the number of nulls to + * present is known). + *

+ * 2) placeholders are not disabled on the Config. + *

+ * Call {@code setEnablePlaceholders(false)} to ensure the receiver of the PagedList + * (often a {@link PagedListAdapter}) doesn't need to account for null items. + *

+ * If placeholders are disabled, not-yet-loaded content will not be present in the list. + * Paging will still occur, but as items are loaded or removed, they will be signaled + * as inserts to the {@link PagedList.Callback}. + * {@link PagedList.Callback#onChanged(int, int)} will not be issued as part of loading, + * though a {@link PagedListAdapter} may still receive change events as a result of + * PagedList diffing. + * + * @param enablePlaceholders False if null placeholders should be disabled. + * @return this + */ + @SuppressWarnings("SameParameterValue") + @NonNull + public Builder setEnablePlaceholders(boolean enablePlaceholders) { + mEnablePlaceholders = enablePlaceholders; + return this; + } + + /** + * Defines how many items to load when first load occurs. + *

+ * This value is typically larger than page size, so on first load data there's a large + * enough range of content loaded to cover small scrolls. + *

+ * When using a {@link PositionalDataSource}, the initial load size will be coerced to + * an integer multiple of pageSize, to enable efficient tiling. + *

+ * If not set, defaults to three times page size. + * + * @param initialLoadSizeHint Number of items to load while initializing the PagedList. + * @return this + */ + @SuppressWarnings("WeakerAccess") + @NonNull + public Builder setInitialLoadSizeHint(@IntRange(from = 1) int initialLoadSizeHint) { + mInitialLoadSizeHint = initialLoadSizeHint; + return this; + } + + /** + * Defines how many items to keep loaded at once. + *

+ * This can be used to cap the number of items kept in memory by dropping pages. This + * value is typically many pages so old pages are cached in case the user scrolls back. + *

+ * This value must be at least two times the + * {@link #setPrefetchDistance(int)} prefetch distance} plus the + * {@link #setPageSize(int) page size}). This constraint prevent loads from being + * continuously fetched and discarded due to prefetching. + *

+ * The max size specified here best effort, not a guarantee. In practice, if maxSize + * is many times the page size, the number of items held by the PagedList will not grow + * above this number. Exceptions are made as necessary to guarantee: + *

    + *
  • Pages are never dropped until there are more than two pages loaded. Note that + * a DataSource may not be held strictly to + * {@link Config#pageSize requested pageSize}, so two pages may be larger than + * expected. + *
  • Pages are never dropped if they are within a prefetch window (defined to be + * {@code pageSize + (2 * prefetchDistance)}) of the most recent load. + *
+ *

+ * {@link PageKeyedDataSource} does not currently support dropping pages - when + * loading from a {@code PageKeyedDataSource}, this value is ignored. + *

+ * If not set, defaults to {@code MAX_SIZE_UNBOUNDED}, which disables page dropping. + * + * @param maxSize Maximum number of items to keep in memory, or + * {@code MAX_SIZE_UNBOUNDED} to disable page dropping. + * @return this + * + * @see Config#MAX_SIZE_UNBOUNDED + * @see Config#maxSize + */ + @NonNull + public Builder setMaxSize(@IntRange(from = 2) int maxSize) { + mMaxSize = maxSize; + return this; + } + + /** + * Creates a {@link Config} with the given parameters. + * + * @return A new Config. + */ + @NonNull + public Config build() { + if (mPrefetchDistance < 0) { + mPrefetchDistance = mPageSize; + } + if (mInitialLoadSizeHint < 0) { + mInitialLoadSizeHint = mPageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER; + } + if (!mEnablePlaceholders && mPrefetchDistance == 0) { + throw new IllegalArgumentException("Placeholders and prefetch are the only ways" + + " to trigger loading of more data in the PagedList, so either" + + " placeholders must be enabled, or prefetch distance must be > 0."); + } + if (mMaxSize != MAX_SIZE_UNBOUNDED) { + if (mMaxSize < mPageSize + mPrefetchDistance * 2) { + throw new IllegalArgumentException("Maximum size must be at least" + + " pageSize + 2*prefetchDist, pageSize=" + mPageSize + + ", prefetchDist=" + mPrefetchDistance + ", maxSize=" + mMaxSize); + } + } + + return new Config(mPageSize, mPrefetchDistance, + mEnablePlaceholders, mInitialLoadSizeHint, mMaxSize); + } + } + } + + /** + * Signals when a PagedList has reached the end of available data. + *

+ * When local storage is a cache of network data, it's common to set up a streaming pipeline: + * Network data is paged into the database, database is paged into UI. Paging from the database + * to UI can be done with a {@code LiveData}, but it's still necessary to know when + * to trigger network loads. + *

+ * BoundaryCallback does this signaling - when a DataSource runs out of data at the end of + * the list, {@link #onItemAtEndLoaded(Object)} is called, and you can start an async network + * load that will write the result directly to the database. Because the database is being + * observed, the UI bound to the {@code LiveData} will update automatically to + * account for the new items. + *

+ * Note that a BoundaryCallback instance shared across multiple PagedLists (e.g. when passed to + * {@link LivePagedListBuilder#setBoundaryCallback}), the callbacks may be issued multiple + * times. If for example {@link #onItemAtEndLoaded(Object)} triggers a network load, it should + * avoid triggering it again while the load is ongoing. + *

+ * The database + network Repository in the + * PagingWithNetworkSample + * shows how to implement a network BoundaryCallback using + * Retrofit, while + * handling swipe-to-refresh, network errors, and retry. + *

Requesting Network Data

+ * BoundaryCallback only passes the item at front or end of the list when out of data. This + * makes it an easy fit for item-keyed network requests, where you can use the item passed to + * the BoundaryCallback to request more data from the network. In these cases, the source of + * truth for next page to load is coming from local storage, based on what's already loaded. + *

+ * If you aren't using an item-keyed network API, you may be using page-keyed, or page-indexed. + * If this is the case, the paging library doesn't know about the page key or index used in the + * BoundaryCallback, so you need to track it yourself. You can do this in one of two ways: + *

Local storage Page key
+ * If you want to perfectly resume your query, even if the app is killed and resumed, you can + * store the key on disk. Note that with a positional/page index network API, there's a simple + * way to do this, by using the {@code listSize} as an input to the next load (or + * {@code listSize / NETWORK_PAGE_SIZE}, for page indexing). + *

+ * The current list size isn't passed to the BoundaryCallback though. This is because the + * PagedList doesn't necessarily know the number of items in local storage. Placeholders may be + * disabled, or the DataSource may not count total number of items. + *

+ * Instead, for these positional cases, you can query the database for the number of items, and + * pass that to the network. + *

In-Memory Page key
+ * Often it doesn't make sense to query the next page from network if the last page you fetched + * was loaded many hours or days before. If you keep the key in memory, you can refresh any time + * you start paging from a network source. + *

+ * Store the next key in memory, inside your BoundaryCallback. When you create a new + * BoundaryCallback when creating a new {@code LiveData}/{@code Observable} of + * {@code PagedList}, refresh data. For example, + * in the + * Paging Codelab, the GitHub network page index is stored in memory. + * + * @param Type loaded by the PagedList. + */ + @MainThread + public abstract static class BoundaryCallback { + /** + * Called when zero items are returned from an initial load of the PagedList's data source. + */ + public void onZeroItemsLoaded() {} + + /** + * Called when the item at the front of the PagedList has been loaded, and access has + * occurred within {@link Config#prefetchDistance} of it. + *

+ * No more data will be prepended to the PagedList before this item. + * + * @param itemAtFront The first item of PagedList + */ + public void onItemAtFrontLoaded(@NonNull T itemAtFront) {} + + /** + * Called when the item at the end of the PagedList has been loaded, and access has + * occurred within {@link Config#prefetchDistance} of it. + *

+ * No more data will be appended to the PagedList after this item. + * + * @param itemAtEnd The first item of PagedList + */ + public void onItemAtEndLoaded(@NonNull T itemAtEnd) {} + } +} diff --git a/app/src/main/java/androidx/paging/PagedListAdapter.java b/app/src/main/java/androidx/paging/PagedListAdapter.java new file mode 100644 index 0000000000..cfc8aa5116 --- /dev/null +++ b/app/src/main/java/androidx/paging/PagedListAdapter.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 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 androidx.paging; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.AdapterListUpdateCallback; +import androidx.recyclerview.widget.AsyncDifferConfig; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +/** + * {@link RecyclerView.Adapter RecyclerView.Adapter} base class for presenting paged data from + * {@link PagedList}s in a {@link RecyclerView}. + *

+ * This class is a convenience wrapper around {@link AsyncPagedListDiffer} that implements common + * default behavior for item counting, and listening to PagedList update callbacks. + *

+ * While using a LiveData<PagedList> is an easy way to provide data to the adapter, it isn't + * required - you can use {@link #submitList(PagedList)} when new lists are available. + *

+ * PagedListAdapter listens to PagedList loading callbacks as pages are loaded, and uses DiffUtil on + * a background thread to compute fine grained updates as new PagedLists are received. + *

+ * Handles both the internal paging of the list as more data is loaded, and updates in the form of + * new PagedLists. + *

+ * A complete usage pattern with Room would look like this: + *

+ * {@literal @}Dao
+ * interface UserDao {
+ *     {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
+ *     public abstract DataSource.Factory<Integer, User> usersByLastName();
+ * }
+ *
+ * class MyViewModel extends ViewModel {
+ *     public final LiveData<PagedList<User>> usersList;
+ *     public MyViewModel(UserDao userDao) {
+ *         usersList = new LivePagedListBuilder<>(
+ *                 userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
+ *     }
+ * }
+ *
+ * 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, pagedList -> adapter.submitList(pagedList));
+ *         recyclerView.setAdapter(adapter);
+ *     }
+ * }
+ *
+ * class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
+ *     public UserAdapter() {
+ *         super(DIFF_CALLBACK);
+ *     }
+ *     {@literal @}Override
+ *     public void onBindViewHolder(UserViewHolder holder, int position) {
+ *         User user = getItem(position);
+ *         if (user != null) {
+ *             holder.bindTo(user);
+ *         } else {
+ *             // Null defines a placeholder item - PagedListAdapter will automatically invalidate
+ *             // this row when the actual object is loaded from the database
+ *             holder.clear();
+ *         }
+ *     }
+ *     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);
+ *         }
+ *     }
+ * }
+ * + * Advanced users that wish for more control over adapter behavior, or to provide a specific base + * class should refer to {@link AsyncPagedListDiffer}, which provides the mapping from paging + * events to adapter-friendly callbacks. + * + * @param Type of the PagedLists this Adapter will receive. + * @param A class that extends ViewHolder that will be used by the adapter. + */ +public abstract class PagedListAdapter + extends RecyclerView.Adapter { + final AsyncPagedListDiffer mDiffer; + private final AsyncPagedListDiffer.PagedListListener mListener = + new AsyncPagedListDiffer.PagedListListener() { + @Override + public void onCurrentListChanged( + @Nullable PagedList previousList, @Nullable PagedList currentList) { + PagedListAdapter.this.onCurrentListChanged(currentList); + PagedListAdapter.this.onCurrentListChanged(previousList, currentList); + } + }; + + /** + * Creates a PagedListAdapter with default threading and + * {@link androidx.recyclerview.widget.ListUpdateCallback}. + * + * Convenience for {@link #PagedListAdapter(AsyncDifferConfig)}, which uses default threading + * behavior. + * + * @param diffCallback The {@link DiffUtil.ItemCallback DiffUtil.ItemCallback} instance to + * compare items in the list. + */ + protected PagedListAdapter(@NonNull DiffUtil.ItemCallback diffCallback) { + mDiffer = new AsyncPagedListDiffer<>(this, diffCallback); + mDiffer.addPagedListListener(mListener); + } + + protected PagedListAdapter(@NonNull AsyncDifferConfig config) { + mDiffer = new AsyncPagedListDiffer<>(new AdapterListUpdateCallback(this), config); + mDiffer.addPagedListListener(mListener); + } + + /** + * Set the new list to be displayed. + *

+ * If a list is already being displayed, a diff will be computed on a background thread, which + * will dispatch Adapter.notifyItem events on the main thread. + * + * @param pagedList The new list to be displayed. + */ + public void submitList(@Nullable PagedList pagedList) { + mDiffer.submitList(pagedList); + } + + /** + * Set the new list to be displayed. + *

+ * If a list is already being displayed, a diff will be computed on a background thread, which + * will dispatch Adapter.notifyItem events on the main thread. + *

+ * The commit callback can be used to know when the PagedList is committed, but note that it + * may not be executed. If PagedList B is submitted immediately after PagedList A, and is + * committed directly, the callback associated with PagedList A will not be run. + * + * @param pagedList The new list to be displayed. + * @param commitCallback Optional runnable that is executed when the PagedList is committed, if + * it is committed. + */ + public void submitList(@Nullable PagedList pagedList, + @Nullable final Runnable commitCallback) { + mDiffer.submitList(pagedList, commitCallback); + } + + @Nullable + protected T getItem(int position) { + return mDiffer.getItem(position); + } + + @Override + public int getItemCount() { + return mDiffer.getItemCount(); + } + + /** + * Returns the PagedList currently being displayed by the Adapter. + *

+ * This is not necessarily the most recent list passed to {@link #submitList(PagedList)}, + * because a diff is computed asynchronously between the new list and the current list before + * updating the currentList value. May be null if no PagedList is being presented. + * + * @return The list currently being displayed. + * + * @see #onCurrentListChanged(PagedList, PagedList) + */ + @Nullable + public PagedList getCurrentList() { + return mDiffer.getCurrentList(); + } + + /** + * Called when the current PagedList is updated. + *

+ * This may be dispatched as part of {@link #submitList(PagedList)} if a background diff isn't + * needed (such as when the first list is passed, or the list is cleared). In either case, + * PagedListAdapter will simply call + * {@link #notifyItemRangeInserted(int, int) notifyItemRangeInserted/Removed(0, mPreviousSize)}. + *

+ * This method will notbe called when the Adapter switches from presenting a PagedList + * to a snapshot version of the PagedList during a diff. This means you cannot observe each + * PagedList via this method. + * + * @deprecated Use the two argument variant instead: + * {@link #onCurrentListChanged(PagedList, PagedList)} + * + * @param currentList new PagedList being displayed, may be null. + * + * @see #getCurrentList() + */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + public void onCurrentListChanged(@Nullable PagedList currentList) { + } + + /** + * Called when the current PagedList is updated. + *

+ * This may be dispatched as part of {@link #submitList(PagedList)} if a background diff isn't + * needed (such as when the first list is passed, or the list is cleared). In either case, + * PagedListAdapter will simply call + * {@link #notifyItemRangeInserted(int, int) notifyItemRangeInserted/Removed(0, mPreviousSize)}. + *

+ * This method will notbe called when the Adapter switches from presenting a PagedList + * to a snapshot version of the PagedList during a diff. This means you cannot observe each + * PagedList via this method. + * + * @param previousList PagedList that was previously displayed, may be null. + * @param currentList new PagedList being displayed, may be null. + * + * @see #getCurrentList() + */ + public void onCurrentListChanged( + @Nullable PagedList previousList, @Nullable PagedList currentList) { + } +} diff --git a/app/src/main/java/androidx/paging/PagedStorage.java b/app/src/main/java/androidx/paging/PagedStorage.java new file mode 100644 index 0000000000..c644234c7e --- /dev/null +++ b/app/src/main/java/androidx/paging/PagedStorage.java @@ -0,0 +1,649 @@ +/* + * 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.paging; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.List; + +/** + * Class holding the pages of data backing a PagedList, presenting sparse loaded data as a List. + *

+ * It has two modes of operation: contiguous and non-contiguous (tiled). This class only holds + * data, and does not have any notion of the ideas of async loads, or prefetching. + */ +final class PagedStorage extends AbstractList { + /** + * Lists instances are compared (with instance equality) to PLACEHOLDER_LIST to check if an item + * in that position is already loading. We use a singleton placeholder list that is distinct + * from Collections.emptyList() for safety. + */ + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private static final List PLACEHOLDER_LIST = new ArrayList(); + + // Always set + private int mLeadingNullCount; + /** + * List of pages in storage. + * + * Two storage modes: + * + * Contiguous - all content in mPages is valid and loaded, but may return false from isTiled(). + * Safe to access any item in any page. + * + * Non-contiguous - mPages may have nulls or a placeholder page, isTiled() always returns true. + * mPages may have nulls, or placeholder (empty) pages while content is loading. + */ + private final ArrayList> mPages; + private int mTrailingNullCount; + + private int mPositionOffset; + /** + * Number of loaded items held by {@link #mPages}. When tiling, doesn't count unloaded pages in + * {@link #mPages}. If tiling is disabled, same as {@link #mStorageCount}. + * + * This count is the one used for trimming. + */ + private int mLoadedCount; + + /** + * Number of items represented by {@link #mPages}. If tiling is enabled, unloaded items in + * {@link #mPages} may be null, but this value still counts them. + */ + private int mStorageCount; + + // If mPageSize > 0, tiling is enabled, 'mPages' may have gaps, and leadingPages is set + private int mPageSize; + + private int mNumberPrepended; + private int mNumberAppended; + + PagedStorage() { + mLeadingNullCount = 0; + mPages = new ArrayList<>(); + mTrailingNullCount = 0; + mPositionOffset = 0; + mLoadedCount = 0; + mStorageCount = 0; + mPageSize = 1; + mNumberPrepended = 0; + mNumberAppended = 0; + } + + PagedStorage(int leadingNulls, List page, int trailingNulls) { + this(); + init(leadingNulls, page, trailingNulls, 0); + } + + private PagedStorage(PagedStorage other) { + mLeadingNullCount = other.mLeadingNullCount; + mPages = new ArrayList<>(other.mPages); + mTrailingNullCount = other.mTrailingNullCount; + mPositionOffset = other.mPositionOffset; + mLoadedCount = other.mLoadedCount; + mStorageCount = other.mStorageCount; + mPageSize = other.mPageSize; + mNumberPrepended = other.mNumberPrepended; + mNumberAppended = other.mNumberAppended; + } + + PagedStorage snapshot() { + return new PagedStorage<>(this); + } + + private void init(int leadingNulls, List page, int trailingNulls, int positionOffset) { + mLeadingNullCount = leadingNulls; + mPages.clear(); + mPages.add(page); + mTrailingNullCount = trailingNulls; + + mPositionOffset = positionOffset; + mLoadedCount = page.size(); + mStorageCount = mLoadedCount; + + // initialized as tiled. There may be 3 nulls, 2 items, but we still call this tiled + // even if it will break if nulls convert. + mPageSize = page.size(); + + mNumberPrepended = 0; + mNumberAppended = 0; + } + + void init(int leadingNulls, @NonNull List page, int trailingNulls, int positionOffset, + @NonNull Callback callback) { + init(leadingNulls, page, trailingNulls, positionOffset); + callback.onInitialized(size()); + } + + @Override + public T get(int i) { + if (i < 0 || i >= size()) { + throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size()); + } + + // is it definitely outside 'mPages'? + int localIndex = i - mLeadingNullCount; + if (localIndex < 0 || localIndex >= mStorageCount) { + return null; + } + + int localPageIndex; + int pageInternalIndex; + + if (isTiled()) { + // it's inside mPages, and we're tiled. Jump to correct tile. + localPageIndex = localIndex / mPageSize; + pageInternalIndex = localIndex % mPageSize; + } else { + // it's inside mPages, but page sizes aren't regular. Walk to correct tile. + // Pages can only be null while tiled, so accessing page count is safe. + pageInternalIndex = localIndex; + final int localPageCount = mPages.size(); + for (localPageIndex = 0; localPageIndex < localPageCount; localPageIndex++) { + int pageSize = mPages.get(localPageIndex).size(); + if (pageSize > pageInternalIndex) { + // stop, found the page + break; + } + pageInternalIndex -= pageSize; + } + } + + List page = mPages.get(localPageIndex); + if (page == null || page.size() == 0) { + // can only occur in tiled case, with untouched inner/placeholder pages + return null; + } + return page.get(pageInternalIndex); + } + + /** + * Returns true if all pages are the same size, except for the last, which may be smaller + */ + boolean isTiled() { + return mPageSize > 0; + } + + int getLeadingNullCount() { + return mLeadingNullCount; + } + + int getTrailingNullCount() { + return mTrailingNullCount; + } + + int getStorageCount() { + return mStorageCount; + } + + int getNumberAppended() { + return mNumberAppended; + } + + int getNumberPrepended() { + return mNumberPrepended; + } + + int getPageCount() { + return mPages.size(); + } + + int getLoadedCount() { + return mLoadedCount; + } + + interface Callback { + void onInitialized(int count); + void onPagePrepended(int leadingNulls, int changed, int added); + void onPageAppended(int endPosition, int changed, int added); + void onPagePlaceholderInserted(int pageIndex); + void onPageInserted(int start, int count); + void onPagesRemoved(int startOfDrops, int count); + void onPagesSwappedToPlaceholder(int startOfDrops, int count); + void onEmptyPrepend(); + void onEmptyAppend(); + } + + int getPositionOffset() { + return mPositionOffset; + } + + int getMiddleOfLoadedRange() { + return mLeadingNullCount + mPositionOffset + mStorageCount / 2; + } + + @Override + public int size() { + return mLeadingNullCount + mStorageCount + mTrailingNullCount; + } + + int computeLeadingNulls() { + int total = mLeadingNullCount; + final int pageCount = mPages.size(); + for (int i = 0; i < pageCount; i++) { + List page = mPages.get(i); + if (page != null && page != PLACEHOLDER_LIST) { + break; + } + total += mPageSize; + } + return total; + } + + int computeTrailingNulls() { + int total = mTrailingNullCount; + for (int i = mPages.size() - 1; i >= 0; i--) { + List page = mPages.get(i); + if (page != null && page != PLACEHOLDER_LIST) { + break; + } + total += mPageSize; + } + return total; + } + + // ---------------- Trimming API ------------------- + // Trimming is always done at the beginning or end of the list, as content is loaded. + // In addition to trimming pages in the storage, we also support pre-trimming pages (dropping + // them just before they're added) to avoid dispatching an add followed immediately by a trim. + // + // Note - we avoid trimming down to a single page to reduce chances of dropping page in + // viewport, since we don't strictly know the viewport. If trim is aggressively set to size of a + // single page, trimming while the user can see a page boundary is dangerous. To be safe, we + // just avoid trimming in these cases entirely. + + private boolean needsTrim(int maxSize, int requiredRemaining, int localPageIndex) { + List page = mPages.get(localPageIndex); + return page == null || (mLoadedCount > maxSize + && mPages.size() > 2 + && page != PLACEHOLDER_LIST + && mLoadedCount - page.size() >= requiredRemaining); + } + + boolean needsTrimFromFront(int maxSize, int requiredRemaining) { + return needsTrim(maxSize, requiredRemaining, 0); + } + + boolean needsTrimFromEnd(int maxSize, int requiredRemaining) { + return needsTrim(maxSize, requiredRemaining, mPages.size() - 1); + } + + boolean shouldPreTrimNewPage(int maxSize, int requiredRemaining, int countToBeAdded) { + return mLoadedCount + countToBeAdded > maxSize + && mPages.size() > 1 + && mLoadedCount >= requiredRemaining; + } + + boolean trimFromFront(boolean insertNulls, int maxSize, int requiredRemaining, + @NonNull Callback callback) { + int totalRemoved = 0; + while (needsTrimFromFront(maxSize, requiredRemaining)) { + List page = mPages.remove(0); + int removed = (page == null) ? mPageSize : page.size(); + totalRemoved += removed; + mStorageCount -= removed; + mLoadedCount -= (page == null) ? 0 : page.size(); + } + + if (totalRemoved > 0) { + if (insertNulls) { + // replace removed items with nulls + int previousLeadingNulls = mLeadingNullCount; + mLeadingNullCount += totalRemoved; + callback.onPagesSwappedToPlaceholder(previousLeadingNulls, totalRemoved); + } else { + // simply remove, and handle offset + mPositionOffset += totalRemoved; + callback.onPagesRemoved(mLeadingNullCount, totalRemoved); + } + } + return totalRemoved > 0; + } + + boolean trimFromEnd(boolean insertNulls, int maxSize, int requiredRemaining, + @NonNull Callback callback) { + int totalRemoved = 0; + while (needsTrimFromEnd(maxSize, requiredRemaining)) { + List page = mPages.remove(mPages.size() - 1); + int removed = (page == null) ? mPageSize : page.size(); + totalRemoved += removed; + mStorageCount -= removed; + mLoadedCount -= (page == null) ? 0 : page.size(); + } + + if (totalRemoved > 0) { + int newEndPosition = mLeadingNullCount + mStorageCount; + if (insertNulls) { + // replace removed items with nulls + mTrailingNullCount += totalRemoved; + callback.onPagesSwappedToPlaceholder(newEndPosition, totalRemoved); + } else { + // items were just removed, signal + callback.onPagesRemoved(newEndPosition, totalRemoved); + } + } + return totalRemoved > 0; + } + + // ---------------- Contiguous API ------------------- + + T getFirstLoadedItem() { + // safe to access first page's first item here: + // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty + return mPages.get(0).get(0); + } + + T getLastLoadedItem() { + // safe to access last page's last item here: + // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty + List page = mPages.get(mPages.size() - 1); + return page.get(page.size() - 1); + } + + void prependPage(@NonNull List page, @NonNull Callback callback) { + final int count = page.size(); + if (count == 0) { + // Nothing returned from source, stop loading in this direction + callback.onEmptyPrepend(); + return; + } + if (mPageSize > 0 && count != mPageSize) { + if (mPages.size() == 1 && count > mPageSize) { + // prepending to a single item - update current page size to that of 'inner' page + mPageSize = count; + } else { + // no longer tiled + mPageSize = -1; + } + } + + mPages.add(0, page); + mLoadedCount += count; + mStorageCount += count; + + final int changedCount = Math.min(mLeadingNullCount, count); + final int addedCount = count - changedCount; + + if (changedCount != 0) { + mLeadingNullCount -= changedCount; + } + mPositionOffset -= addedCount; + mNumberPrepended += count; + + callback.onPagePrepended(mLeadingNullCount, changedCount, addedCount); + } + + void appendPage(@NonNull List page, @NonNull Callback callback) { + final int count = page.size(); + if (count == 0) { + // Nothing returned from source, stop loading in this direction + callback.onEmptyAppend(); + return; + } + + if (mPageSize > 0) { + // if the previous page was smaller than mPageSize, + // or if this page is larger than the previous, disable tiling + if (mPages.get(mPages.size() - 1).size() != mPageSize + || count > mPageSize) { + mPageSize = -1; + } + } + + mPages.add(page); + mLoadedCount += count; + mStorageCount += count; + + final int changedCount = Math.min(mTrailingNullCount, count); + final int addedCount = count - changedCount; + + if (changedCount != 0) { + mTrailingNullCount -= changedCount; + } + mNumberAppended += count; + callback.onPageAppended(mLeadingNullCount + mStorageCount - count, + changedCount, addedCount); + } + + // ------------------ Non-Contiguous API (tiling required) ---------------------- + + /** + * Return true if the page at the passed position would be the first (if trimFromFront) or last + * page that's currently loading. + */ + boolean pageWouldBeBoundary(int positionOfPage, boolean trimFromFront) { + if (mPageSize < 1 || mPages.size() < 2) { + throw new IllegalStateException("Trimming attempt before sufficient load"); + } + + if (positionOfPage < mLeadingNullCount) { + // position represent page in leading nulls + return trimFromFront; + } + + if (positionOfPage >= mLeadingNullCount + mStorageCount) { + // position represent page in trailing nulls + return !trimFromFront; + } + + int localPageIndex = (positionOfPage - mLeadingNullCount) / mPageSize; + + // walk outside in, return false if we find non-placeholder page before localPageIndex + if (trimFromFront) { + for (int i = 0; i < localPageIndex; i++) { + if (mPages.get(i) != null) { + return false; + } + } + } else { + for (int i = mPages.size() - 1; i > localPageIndex; i--) { + if (mPages.get(i) != null) { + return false; + } + } + } + + // didn't find another page, so this one would be a boundary + return true; + } + + void initAndSplit(int leadingNulls, @NonNull List multiPageList, + int trailingNulls, int positionOffset, int pageSize, @NonNull Callback callback) { + + int pageCount = (multiPageList.size() + (pageSize - 1)) / pageSize; + for (int i = 0; i < pageCount; i++) { + int beginInclusive = i * pageSize; + int endExclusive = Math.min(multiPageList.size(), (i + 1) * pageSize); + + List sublist = multiPageList.subList(beginInclusive, endExclusive); + + if (i == 0) { + // Trailing nulls for first page includes other pages in multiPageList + int initialTrailingNulls = trailingNulls + multiPageList.size() - sublist.size(); + init(leadingNulls, sublist, initialTrailingNulls, positionOffset); + } else { + int insertPosition = leadingNulls + beginInclusive; + insertPage(insertPosition, sublist, null); + } + } + callback.onInitialized(size()); + } + + void tryInsertPageAndTrim( + int position, + @NonNull List page, + int lastLoad, + int maxSize, + int requiredRemaining, + @NonNull Callback callback) { + boolean trim = maxSize != PagedList.Config.MAX_SIZE_UNBOUNDED; + boolean trimFromFront = lastLoad > getMiddleOfLoadedRange(); + + boolean pageInserted = !trim + || !shouldPreTrimNewPage(maxSize, requiredRemaining, page.size()) + || !pageWouldBeBoundary(position, trimFromFront); + + if (pageInserted) { + insertPage(position, page, callback); + } else { + // trim would have us drop the page we just loaded - swap it to null + int localPageIndex = (position - mLeadingNullCount) / mPageSize; + mPages.set(localPageIndex, null); + + // note: we also remove it, so we don't have to guess how large a 'null' page is later + mStorageCount -= page.size(); + if (trimFromFront) { + mPages.remove(0); + mLeadingNullCount += page.size(); + } else { + mPages.remove(mPages.size() - 1); + mTrailingNullCount += page.size(); + } + } + + if (trim) { + if (trimFromFront) { + trimFromFront(true, maxSize, requiredRemaining, callback); + } else { + trimFromEnd(true, maxSize, requiredRemaining, callback); + } + } + } + + public void insertPage(int position, @NonNull List page, @Nullable Callback callback) { + final int newPageSize = page.size(); + if (newPageSize != mPageSize) { + // differing page size is OK in 2 cases, when the page is being added: + // 1) to the end (in which case, ignore new smaller size) + // 2) only the last page has been added so far (in which case, adopt new bigger size) + + int size = size(); + boolean addingLastPage = position == (size - size % mPageSize) + && newPageSize < mPageSize; + boolean onlyEndPagePresent = mTrailingNullCount == 0 && mPages.size() == 1 + && newPageSize > mPageSize; + + // OK only if existing single page, and it's the last one + if (!onlyEndPagePresent && !addingLastPage) { + throw new IllegalArgumentException("page introduces incorrect tiling"); + } + if (onlyEndPagePresent) { + mPageSize = newPageSize; + } + } + + int pageIndex = position / mPageSize; + + allocatePageRange(pageIndex, pageIndex); + + int localPageIndex = pageIndex - mLeadingNullCount / mPageSize; + + List oldPage = mPages.get(localPageIndex); + if (oldPage != null && oldPage != PLACEHOLDER_LIST) { + throw new IllegalArgumentException( + "Invalid position " + position + ": data already loaded"); + } + mPages.set(localPageIndex, page); + mLoadedCount += newPageSize; + if (callback != null) { + callback.onPageInserted(position, newPageSize); + } + } + + void allocatePageRange(final int minimumPage, final int maximumPage) { + int leadingNullPages = mLeadingNullCount / mPageSize; + + if (minimumPage < leadingNullPages) { + for (int i = 0; i < leadingNullPages - minimumPage; i++) { + mPages.add(0, null); + } + int newStorageAllocated = (leadingNullPages - minimumPage) * mPageSize; + mStorageCount += newStorageAllocated; + mLeadingNullCount -= newStorageAllocated; + + leadingNullPages = minimumPage; + } + if (maximumPage >= leadingNullPages + mPages.size()) { + int newStorageAllocated = Math.min(mTrailingNullCount, + (maximumPage + 1 - (leadingNullPages + mPages.size())) * mPageSize); + for (int i = mPages.size(); i <= maximumPage - leadingNullPages; i++) { + mPages.add(mPages.size(), null); + } + mStorageCount += newStorageAllocated; + mTrailingNullCount -= newStorageAllocated; + } + } + + public void allocatePlaceholders(int index, int prefetchDistance, + int pageSize, Callback callback) { + if (pageSize != mPageSize) { + if (pageSize < mPageSize) { + throw new IllegalArgumentException("Page size cannot be reduced"); + } + if (mPages.size() != 1 || mTrailingNullCount != 0) { + // not in single, last page allocated case - can't change page size + throw new IllegalArgumentException( + "Page size can change only if last page is only one present"); + } + mPageSize = pageSize; + } + + final int maxPageCount = (size() + mPageSize - 1) / mPageSize; + int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0); + int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1); + + allocatePageRange(minimumPage, maximumPage); + int leadingNullPages = mLeadingNullCount / mPageSize; + for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) { + int localPageIndex = pageIndex - leadingNullPages; + if (mPages.get(localPageIndex) == null) { + //noinspection unchecked + mPages.set(localPageIndex, PLACEHOLDER_LIST); + callback.onPagePlaceholderInserted(pageIndex); + } + } + } + + public boolean hasPage(int pageSize, int index) { + // NOTE: we pass pageSize here to avoid in case mPageSize + // not fully initialized (when last page only one loaded) + int leadingNullPages = mLeadingNullCount / pageSize; + + if (index < leadingNullPages || index >= leadingNullPages + mPages.size()) { + return false; + } + + List page = mPages.get(index - leadingNullPages); + + return page != null && page != PLACEHOLDER_LIST; + } + + @Override + public String toString() { + StringBuilder ret = new StringBuilder("leading " + mLeadingNullCount + + ", storage " + mStorageCount + + ", trailing " + getTrailingNullCount()); + + for (int i = 0; i < mPages.size(); i++) { + ret.append(" ").append(mPages.get(i)); + } + return ret.toString(); + } +} diff --git a/app/src/main/java/androidx/paging/PagedStorageDiffHelper.java b/app/src/main/java/androidx/paging/PagedStorageDiffHelper.java new file mode 100644 index 0000000000..886bfd1259 --- /dev/null +++ b/app/src/main/java/androidx/paging/PagedStorageDiffHelper.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 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 androidx.paging; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListUpdateCallback; + +/** + * Methods for computing and applying DiffResults between PagedLists. + * + * To minimize the amount of diffing caused by placeholders, we only execute DiffUtil in a reduced + * 'diff space' - in the range (computeLeadingNulls..size-computeTrailingNulls). + * + * This allows the diff of a PagedList, e.g.: + * 100 nulls, placeholder page, (empty page) x 5, page, 100 nulls + * + * To only inform DiffUtil about single loaded page in this case, by pruning all other nulls from + * consideration. + * + * @see PagedStorage#computeLeadingNulls() + * @see PagedStorage#computeTrailingNulls() + */ +class PagedStorageDiffHelper { + private PagedStorageDiffHelper() { + } + + static DiffUtil.DiffResult computeDiff( + final PagedStorage oldList, + final PagedStorage newList, + final DiffUtil.ItemCallback diffCallback) { + final int oldOffset = oldList.computeLeadingNulls(); + final int newOffset = newList.computeLeadingNulls(); + + final int oldSize = oldList.size() - oldOffset - oldList.computeTrailingNulls(); + final int newSize = newList.size() - newOffset - newList.computeTrailingNulls(); + + return DiffUtil.calculateDiff(new DiffUtil.Callback() { + @Nullable + @Override + public Object getChangePayload(int oldItemPosition, int newItemPosition) { + T oldItem = oldList.get(oldItemPosition + oldOffset); + T newItem = newList.get(newItemPosition + newList.getLeadingNullCount()); + if (oldItem == null || newItem == null) { + return null; + } + return diffCallback.getChangePayload(oldItem, newItem); + } + + @Override + public int getOldListSize() { + return oldSize; + } + + @Override + public int getNewListSize() { + return newSize; + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + T oldItem = oldList.get(oldItemPosition + oldOffset); + T newItem = newList.get(newItemPosition + newList.getLeadingNullCount()); + if (oldItem == newItem) { + return true; + } + //noinspection SimplifiableIfStatement + if (oldItem == null || newItem == null) { + return false; + } + return diffCallback.areItemsTheSame(oldItem, newItem); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + T oldItem = oldList.get(oldItemPosition + oldOffset); + T newItem = newList.get(newItemPosition + newList.getLeadingNullCount()); + if (oldItem == newItem) { + return true; + } + //noinspection SimplifiableIfStatement + if (oldItem == null || newItem == null) { + return false; + } + + return diffCallback.areContentsTheSame(oldItem, newItem); + } + }, true); + } + + private static class OffsettingListUpdateCallback implements ListUpdateCallback { + private final int mOffset; + private final ListUpdateCallback mCallback; + + OffsettingListUpdateCallback(int offset, ListUpdateCallback callback) { + mOffset = offset; + mCallback = callback; + } + + @Override + public void onInserted(int position, int count) { + mCallback.onInserted(position + mOffset, count); + } + + @Override + public void onRemoved(int position, int count) { + mCallback.onRemoved(position + mOffset, count); + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + mCallback.onMoved(fromPosition + mOffset, toPosition + mOffset); + } + + @Override + public void onChanged(int position, int count, Object payload) { + mCallback.onChanged(position + mOffset, count, payload); + } + } + + /** + * TODO: improve diffing logic + * + * This function currently does a naive diff, assuming null does not become an item, and vice + * versa (so it won't dispatch onChange events for these). It's similar to passing a list with + * leading/trailing nulls in the beginning / end to DiffUtil, but dispatches the remove/insert + * for changed nulls at the beginning / end of the list. + * + * Note: if lists mutate between diffing the snapshot and dispatching the diff here, then we + * handle this by passing the snapshot to the callback, and dispatching those changes + * immediately after dispatching this diff. + */ + static void dispatchDiff(ListUpdateCallback callback, + final PagedStorage oldList, + final PagedStorage newList, + final DiffUtil.DiffResult diffResult) { + + final int trailingOld = oldList.computeTrailingNulls(); + final int trailingNew = newList.computeTrailingNulls(); + final int leadingOld = oldList.computeLeadingNulls(); + final int leadingNew = newList.computeLeadingNulls(); + + if (trailingOld == 0 + && trailingNew == 0 + && leadingOld == 0 + && leadingNew == 0) { + // Simple case, dispatch & return + diffResult.dispatchUpdatesTo(callback); + return; + } + + // First, remove or insert trailing nulls + if (trailingOld > trailingNew) { + int count = trailingOld - trailingNew; + callback.onRemoved(oldList.size() - count, count); + } else if (trailingOld < trailingNew) { + callback.onInserted(oldList.size(), trailingNew - trailingOld); + } + + // Second, remove or insert leading nulls + if (leadingOld > leadingNew) { + callback.onRemoved(0, leadingOld - leadingNew); + } else if (leadingOld < leadingNew) { + callback.onInserted(0, leadingNew - leadingOld); + } + + // apply the diff, with an offset if needed + if (leadingNew != 0) { + diffResult.dispatchUpdatesTo(new OffsettingListUpdateCallback(leadingNew, callback)); + } else { + diffResult.dispatchUpdatesTo(callback); + } + } + + /** + * Given an oldPosition representing an anchor in the old data set, computes its new position + * after the diff, or a guess if it no longer exists. + */ + static int transformAnchorIndex(@NonNull DiffUtil.DiffResult diffResult, + @NonNull PagedStorage oldList, @NonNull PagedStorage newList, final int oldPosition) { + final int oldOffset = oldList.computeLeadingNulls(); + + // diffResult's indices starting after nulls, need to transform to diffutil indices + // (see also dispatchDiff(), which adds this offset when dispatching) + int diffIndex = oldPosition - oldOffset; + + final int oldSize = oldList.size() - oldOffset - oldList.computeTrailingNulls(); + + // if our anchor is non-null, use it or close item's position in new list + if (diffIndex >= 0 && diffIndex < oldSize) { + // search outward from old position for position that maps + for (int i = 0; i < 30; i++) { + int positionToTry = diffIndex + (i / 2 * (i % 2 == 1 ? -1 : 1)); + + // reject if (null) item was not passed to DiffUtil, and wouldn't be in the result + if (positionToTry < 0 || positionToTry >= oldList.getStorageCount()) { + continue; + } + + try { + int result = diffResult.convertOldPositionToNew(positionToTry); + if (result != -1) { + // also need to transform from diffutil output indices to newList + return result + newList.getLeadingNullCount(); + } + } catch (IndexOutOfBoundsException e) { + // Rare crash, just give up the search for the old item + break; + } + } + } + + // not anchored to an item in new list, so just reuse position (clamped to newList size) + return Math.max(0, Math.min(oldPosition, newList.size() - 1)); + } +} diff --git a/app/src/main/java/androidx/paging/PositionalDataSource.java b/app/src/main/java/androidx/paging/PositionalDataSource.java new file mode 100644 index 0000000000..d54cd7c906 --- /dev/null +++ b/app/src/main/java/androidx/paging/PositionalDataSource.java @@ -0,0 +1,576 @@ +/* + * 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.paging; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import androidx.arch.core.util.Function; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at + * arbitrary page positions. + *

+ * Extend PositionalDataSource if you can load pages of a requested size at arbitrary + * positions, and provide a fixed item count. If your data source can't support loading arbitrary + * requested page sizes (e.g. when network page size constraints are only known at runtime), use + * either {@link PageKeyedDataSource} or {@link ItemKeyedDataSource} instead. + *

+ * Note that unless {@link PagedList.Config#enablePlaceholders placeholders are disabled} + * PositionalDataSource requires counting the size of the data set. This allows pages to be tiled in + * at arbitrary, non-contiguous locations based upon what the user observes in a {@link PagedList}. + * If placeholders are disabled, initialize with the two parameter + * {@link LoadInitialCallback#onResult(List, int)}. + *

+ * Room can generate a Factory of PositionalDataSources for you: + *

+ * {@literal @}Dao
+ * interface UserDao {
+ *     {@literal @}Query("SELECT * FROM user ORDER BY mAge DESC")
+ *     public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc();
+ * }
+ * + * @param Type of items being loaded by the PositionalDataSource. + */ +public abstract class PositionalDataSource extends DataSource { + + /** + * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}. + */ + @SuppressWarnings("WeakerAccess") + public static class LoadInitialParams { + /** + * Initial load position requested. + *

+ * Note that this may not be within the bounds of your data set, it may need to be adjusted + * before you execute your load. + */ + public final int requestedStartPosition; + + /** + * Requested number of items to load. + *

+ * Note that this may be larger than available data. + */ + public final int requestedLoadSize; + + /** + * Defines page size acceptable for return values. + *

+ * List of items passed to the callback must be an integer multiple of page size. + */ + public final int pageSize; + + /** + * Defines whether placeholders are enabled, and whether the total count passed to + * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored. + */ + public final boolean placeholdersEnabled; + + public LoadInitialParams( + int requestedStartPosition, + int requestedLoadSize, + int pageSize, + boolean placeholdersEnabled) { + this.requestedStartPosition = requestedStartPosition; + this.requestedLoadSize = requestedLoadSize; + this.pageSize = pageSize; + this.placeholdersEnabled = placeholdersEnabled; + } + } + + /** + * Holder object for inputs to {@link #loadRange(LoadRangeParams, LoadRangeCallback)}. + */ + @SuppressWarnings("WeakerAccess") + public static class LoadRangeParams { + /** + * Start position of data to load. + *

+ * Returned data must start at this position. + */ + public final int startPosition; + /** + * Number of items to load. + *

+ * Returned data must be of this size, unless at end of the list. + */ + public final int loadSize; + + public LoadRangeParams(int startPosition, int loadSize) { + this.startPosition = startPosition; + this.loadSize = loadSize; + } + } + + /** + * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} + * to return data, position, and count. + *

+ * A callback should be called only once, and may throw if called again. + *

+ * It is always valid for a DataSource loading method that takes a callback to stash the + * callback and call it later. This enables DataSources to be fully asynchronous, and to handle + * temporary, recoverable error states (such as a network error that can be retried). + * + * @param Type of items being loaded. + */ + public abstract static class LoadInitialCallback { + /** + * Called to pass initial load state from a DataSource. + *

+ * Call this method from your DataSource's {@code loadInitial} function to return data, + * and inform how many placeholders should be shown before and after. If counting is cheap + * to compute (for example, if a network load returns the information regardless), it's + * recommended to pass the total size to the totalCount parameter. If placeholders are not + * requested (when {@link LoadInitialParams#placeholdersEnabled} is false), you can instead + * call {@link #onResult(List, int)}. + * + * @param data List of items loaded from the DataSource. If this is empty, the DataSource + * is treated as empty, and no further loads will occur. + * @param position Position of the item at the front of the list. If there are {@code N} + * items before the items in data that can be loaded from this DataSource, + * pass {@code N}. + * @param totalCount Total number of items that may be returned from this DataSource. + * Includes the number in the initial {@code data} parameter + * as well as any items that can be loaded in front or behind of + * {@code data}. + */ + public abstract void onResult(@NonNull List data, int position, int totalCount); + + /** + * Called to pass initial load state from a DataSource without total count, + * when placeholders aren't requested. + *

Note: This method can only be called when placeholders + * are disabled ({@link LoadInitialParams#placeholdersEnabled} is false). + *

+ * Call this method from your DataSource's {@code loadInitial} function to return data, + * if position is known but total size is not. If placeholders are requested, call the three + * parameter variant: {@link #onResult(List, int, int)}. + * + * @param data List of items loaded from the DataSource. If this is empty, the DataSource + * is treated as empty, and no further loads will occur. + * @param position Position of the item at the front of the list. If there are {@code N} + * items before the items in data that can be provided by this DataSource, + * pass {@code N}. + */ + public abstract void onResult(@NonNull List data, int position); + } + + /** + * Callback for PositionalDataSource {@link #loadRange(LoadRangeParams, LoadRangeCallback)} + * to return data. + *

+ * A callback should be called only once, and may throw if called again. + *

+ * It is always valid for a DataSource loading method that takes a callback to stash the + * callback and call it later. This enables DataSources to be fully asynchronous, and to handle + * temporary, recoverable error states (such as a network error that can be retried). + * + * @param Type of items being loaded. + */ + public abstract static class LoadRangeCallback { + /** + * Called to pass loaded data from {@link #loadRange(LoadRangeParams, LoadRangeCallback)}. + * + * @param data List of items loaded from the DataSource. Must be same size as requested, + * unless at end of list. + */ + public abstract void onResult(@NonNull List data); + } + + static class LoadInitialCallbackImpl extends LoadInitialCallback { + final LoadCallbackHelper mCallbackHelper; + private final boolean mCountingEnabled; + private final int mPageSize; + + LoadInitialCallbackImpl(@NonNull PositionalDataSource dataSource, boolean countingEnabled, + int pageSize, PageResult.Receiver receiver) { + mCallbackHelper = new LoadCallbackHelper<>(dataSource, PageResult.INIT, null, receiver); + mCountingEnabled = countingEnabled; + mPageSize = pageSize; + if (mPageSize < 1) { + throw new IllegalArgumentException("Page size must be non-negative"); + } + } + + @Override + public void onResult(@NonNull List data, int position, int totalCount) { + if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) { + LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount); + if (position + data.size() != totalCount + && data.size() % mPageSize != 0) { + throw new IllegalArgumentException("PositionalDataSource requires initial load" + + " size to be a multiple of page size to support internal tiling." + + " loadSize " + data.size() + ", position " + position + + ", totalCount " + totalCount + ", pageSize " + mPageSize); + } + + if (mCountingEnabled) { + int trailingUnloadedCount = totalCount - position - data.size(); + mCallbackHelper.dispatchResultToReceiver( + new PageResult<>(data, position, trailingUnloadedCount, 0)); + } else { + // Only occurs when wrapped as contiguous + mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position)); + } + } + } + + @Override + public void onResult(@NonNull List data, int position) { + if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) { + if (position < 0) { + throw new IllegalArgumentException("Position must be non-negative"); + } + if (data.isEmpty() && position != 0) { + throw new IllegalArgumentException( + "Initial result cannot be empty if items are present in data set."); + } + if (mCountingEnabled) { + throw new IllegalStateException("Placeholders requested, but totalCount not" + + " provided. Please call the three-parameter onResult method, or" + + " disable placeholders in the PagedList.Config"); + } + mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position)); + } + } + } + + static class LoadRangeCallbackImpl extends LoadRangeCallback { + private LoadCallbackHelper mCallbackHelper; + private final int mPositionOffset; + LoadRangeCallbackImpl(@NonNull PositionalDataSource dataSource, + @PageResult.ResultType int resultType, int positionOffset, + Executor mainThreadExecutor, PageResult.Receiver receiver) { + mCallbackHelper = new LoadCallbackHelper<>( + dataSource, resultType, mainThreadExecutor, receiver); + mPositionOffset = positionOffset; + } + + @Override + public void onResult(@NonNull List data) { + if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) { + mCallbackHelper.dispatchResultToReceiver(new PageResult<>( + data, 0, 0, mPositionOffset)); + } + } + } + + final void dispatchLoadInitial(boolean acceptCount, + int requestedStartPosition, int requestedLoadSize, int pageSize, + @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) { + LoadInitialCallbackImpl callback = + new LoadInitialCallbackImpl<>(this, acceptCount, pageSize, receiver); + + LoadInitialParams params = new LoadInitialParams( + requestedStartPosition, requestedLoadSize, pageSize, acceptCount); + loadInitial(params, callback); + + // If initialLoad's callback is not called within the body, we force any following calls + // to post to the UI thread. This constructor may be run on a background thread, but + // after constructor, mutation must happen on UI thread. + callback.mCallbackHelper.setPostExecutor(mainThreadExecutor); + } + + final void dispatchLoadRange(@PageResult.ResultType int resultType, int startPosition, + int count, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + LoadRangeCallback callback = new LoadRangeCallbackImpl<>( + this, resultType, startPosition, mainThreadExecutor, receiver); + if (count == 0) { + callback.onResult(Collections.emptyList()); + } else { + loadRange(new LoadRangeParams(startPosition, count), callback); + } + } + + /** + * Load initial list data. + *

+ * This method is called to load the initial page(s) from the DataSource. + *

+ * Result list must be a multiple of pageSize to enable efficient tiling. + * + * @param params Parameters for initial load, including requested start position, load size, and + * page size. + * @param callback Callback that receives initial load data, including + * position and total data set size. + */ + @WorkerThread + public abstract void loadInitial( + @NonNull LoadInitialParams params, + @NonNull LoadInitialCallback callback); + + /** + * Called to load a range of data from the DataSource. + *

+ * This method is called to load additional pages from the DataSource after the + * LoadInitialCallback passed to dispatchLoadInitial has initialized a PagedList. + *

+ * Unlike {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, this method must return + * the number of items requested, at the position requested. + * + * @param params Parameters for load, including start position and load size. + * @param callback Callback that receives loaded data. + */ + @WorkerThread + public abstract void loadRange(@NonNull LoadRangeParams params, + @NonNull LoadRangeCallback callback); + + @Override + boolean isContiguous() { + return false; + } + + @NonNull + ContiguousDataSource wrapAsContiguousWithoutPlaceholders() { + return new ContiguousWithoutPlaceholdersWrapper<>(this); + } + + /** + * Helper for computing an initial position in + * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be + * computed ahead of loading. + *

+ * The value computed by this function will do bounds checking, page alignment, and positioning + * based on initial load size requested. + *

+ * Example usage in a PositionalDataSource subclass: + *

+     * class ItemDataSource extends PositionalDataSource<Item> {
+     *     private int computeCount() {
+     *         // actual count code here
+     *     }
+     *
+     *     private List<Item> loadRangeInternal(int startPosition, int loadCount) {
+     *         // actual load code here
+     *     }
+     *
+     *     {@literal @}Override
+     *     public void loadInitial({@literal @}NonNull LoadInitialParams params,
+     *             {@literal @}NonNull LoadInitialCallback<Item> callback) {
+     *         int totalCount = computeCount();
+     *         int position = computeInitialLoadPosition(params, totalCount);
+     *         int loadSize = computeInitialLoadSize(params, position, totalCount);
+     *         callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
+     *     }
+     *
+     *     {@literal @}Override
+     *     public void loadRange({@literal @}NonNull LoadRangeParams params,
+     *             {@literal @}NonNull LoadRangeCallback<Item> callback) {
+     *         callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
+     *     }
+     * }
+ * + * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, + * including page size, and requested start/loadSize. + * @param totalCount Total size of the data set. + * @return Position to start loading at. + * + * @see #computeInitialLoadSize(LoadInitialParams, int, int) + */ + public static int computeInitialLoadPosition(@NonNull LoadInitialParams params, + int totalCount) { + int position = params.requestedStartPosition; + int initialLoadSize = params.requestedLoadSize; + int pageSize = params.pageSize; + + int pageStart = position / pageSize * pageSize; + + // maximum start pos is that which will encompass end of list + int maximumLoadPage = ((totalCount - initialLoadSize + pageSize - 1) / pageSize) * pageSize; + pageStart = Math.min(maximumLoadPage, pageStart); + + // minimum start position is 0 + pageStart = Math.max(0, pageStart); + + return pageStart; + } + + /** + * Helper for computing an initial load size in + * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be + * computed ahead of loading. + *

+ * This function takes the requested load size, and bounds checks it against the value returned + * by {@link #computeInitialLoadPosition(LoadInitialParams, int)}. + *

+ * Example usage in a PositionalDataSource subclass: + *

+     * class ItemDataSource extends PositionalDataSource<Item> {
+     *     private int computeCount() {
+     *         // actual count code here
+     *     }
+     *
+     *     private List<Item> loadRangeInternal(int startPosition, int loadCount) {
+     *         // actual load code here
+     *     }
+     *
+     *     {@literal @}Override
+     *     public void loadInitial({@literal @}NonNull LoadInitialParams params,
+     *             {@literal @}NonNull LoadInitialCallback<Item> callback) {
+     *         int totalCount = computeCount();
+     *         int position = computeInitialLoadPosition(params, totalCount);
+     *         int loadSize = computeInitialLoadSize(params, position, totalCount);
+     *         callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
+     *     }
+     *
+     *     {@literal @}Override
+     *     public void loadRange({@literal @}NonNull LoadRangeParams params,
+     *             {@literal @}NonNull LoadRangeCallback<Item> callback) {
+     *         callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
+     *     }
+     * }
+ * + * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, + * including page size, and requested start/loadSize. + * @param initialLoadPosition Value returned by + * {@link #computeInitialLoadPosition(LoadInitialParams, int)} + * @param totalCount Total size of the data set. + * @return Number of items to load. + * + * @see #computeInitialLoadPosition(LoadInitialParams, int) + */ + @SuppressWarnings("WeakerAccess") + public static int computeInitialLoadSize(@NonNull LoadInitialParams params, + int initialLoadPosition, int totalCount) { + return Math.min(totalCount - initialLoadPosition, params.requestedLoadSize); + } + + @SuppressWarnings("deprecation") + static class ContiguousWithoutPlaceholdersWrapper + extends ContiguousDataSource { + @NonNull + final PositionalDataSource mSource; + + ContiguousWithoutPlaceholdersWrapper( + @NonNull PositionalDataSource source) { + mSource = source; + } + + @Override + public void addInvalidatedCallback( + @NonNull InvalidatedCallback onInvalidatedCallback) { + mSource.addInvalidatedCallback(onInvalidatedCallback); + } + + @Override + public void removeInvalidatedCallback( + @NonNull InvalidatedCallback onInvalidatedCallback) { + mSource.removeInvalidatedCallback(onInvalidatedCallback); + } + + @Override + public void invalidate() { + mSource.invalidate(); + } + + @Override + public boolean isInvalid() { + return mSource.isInvalid(); + } + + @NonNull + @Override + public DataSource mapByPage( + @NonNull Function, List> function) { + throw new UnsupportedOperationException( + "Inaccessible inner type doesn't support map op"); + } + + @NonNull + @Override + public DataSource map( + @NonNull Function function) { + throw new UnsupportedOperationException( + "Inaccessible inner type doesn't support map op"); + } + + @Override + void dispatchLoadInitial(@Nullable Integer position, int initialLoadSize, int pageSize, + boolean enablePlaceholders, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + + if (position == null) { + position = 0; + } else { + // snap load size to page multiple (minimum two) + initialLoadSize = (Math.max(initialLoadSize / pageSize, 2)) * pageSize; + + // move start pos so that the load is centered around the key, not starting at it + final int idealStart = position - initialLoadSize / 2; + position = Math.max(0, idealStart / pageSize * pageSize); + } + + // Note enablePlaceholders will be false here, but we don't have a way to communicate + // this to PositionalDataSource. This is fine, because only the list and its position + // offset will be consumed by the LoadInitialCallback. + mSource.dispatchLoadInitial(false, position, initialLoadSize, + pageSize, mainThreadExecutor, receiver); + } + + @Override + void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize, + @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + int startIndex = currentEndIndex + 1; + mSource.dispatchLoadRange( + PageResult.APPEND, startIndex, pageSize, mainThreadExecutor, receiver); + } + + @Override + void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, + int pageSize, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + int startIndex = currentBeginIndex - 1; + if (startIndex < 0) { + // trigger empty list load + mSource.dispatchLoadRange( + PageResult.PREPEND, startIndex, 0, mainThreadExecutor, receiver); + } else { + int loadSize = Math.min(pageSize, startIndex + 1); + startIndex = startIndex - loadSize + 1; + mSource.dispatchLoadRange( + PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver); + } + } + + @Override + Integer getKey(int position, Value item) { + return position; + } + + } + + @NonNull + @Override + public final PositionalDataSource mapByPage( + @NonNull Function, List> function) { + return new WrapperPositionalDataSource<>(this, function); + } + + @NonNull + @Override + public final PositionalDataSource map(@NonNull Function function) { + return mapByPage(createListFunction(function)); + } +} diff --git a/app/src/main/java/androidx/paging/SnapshotPagedList.java b/app/src/main/java/androidx/paging/SnapshotPagedList.java new file mode 100644 index 0000000000..42d8cfe629 --- /dev/null +++ b/app/src/main/java/androidx/paging/SnapshotPagedList.java @@ -0,0 +1,74 @@ +/* + * 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.paging; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +class SnapshotPagedList extends PagedList { + private final boolean mContiguous; + private final Object mLastKey; + private final DataSource mDataSource; + + SnapshotPagedList(@NonNull PagedList pagedList) { + super(pagedList.mStorage.snapshot(), + pagedList.mMainThreadExecutor, + pagedList.mBackgroundThreadExecutor, + null, + pagedList.mConfig); + mDataSource = pagedList.getDataSource(); + mContiguous = pagedList.isContiguous(); + mLastLoad = pagedList.mLastLoad; + mLastKey = pagedList.getLastKey(); + } + + @Override + public boolean isImmutable() { + return true; + } + + @Override + public boolean isDetached() { + return true; + } + + @Override + boolean isContiguous() { + return mContiguous; + } + + @Nullable + @Override + public Object getLastKey() { + return mLastKey; + } + + @NonNull + @Override + public DataSource getDataSource() { + return mDataSource; + } + + @Override + void dispatchUpdatesSinceSnapshot(@NonNull PagedList storageSnapshot, + @NonNull Callback callback) { + } + + @Override + void loadAroundInternal(int index) { + } +} diff --git a/app/src/main/java/androidx/paging/TiledDataSource.java b/app/src/main/java/androidx/paging/TiledDataSource.java new file mode 100644 index 0000000000..5f864219aa --- /dev/null +++ b/app/src/main/java/androidx/paging/TiledDataSource.java @@ -0,0 +1,86 @@ +/* + * 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.paging; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.WorkerThread; + +import java.util.Collections; +import java.util.List; + +// NOTE: Room 1.0 depends on this class, so it should not be removed until +// we can require a version of Room that uses PositionalDataSource directly +/** + * @param Type loaded by the TiledDataSource. + * + * @deprecated Use {@link PositionalDataSource} + * @hide + */ +@SuppressWarnings("DeprecatedIsStillUsed") +@Deprecated +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public abstract class TiledDataSource extends PositionalDataSource { + + @WorkerThread + public abstract int countItems(); + + @Override + boolean isContiguous() { + return false; + } + + @Nullable + @WorkerThread + public abstract List loadRange(int startPosition, int count); + + @Override + public void loadInitial(@NonNull LoadInitialParams params, + @NonNull LoadInitialCallback callback) { + int totalCount = countItems(); + if (totalCount == 0) { + callback.onResult(Collections.emptyList(), 0, 0); + return; + } + + // bound the size requested, based on known count + final int firstLoadPosition = computeInitialLoadPosition(params, totalCount); + final int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount); + + // convert from legacy behavior + List list = loadRange(firstLoadPosition, firstLoadSize); + if (list != null && list.size() == firstLoadSize) { + callback.onResult(list, firstLoadPosition, totalCount); + } else { + // null list, or size doesn't match request + // The size check is a WAR for Room 1.0, subsequent versions do the check in Room + invalidate(); + } + } + + @Override + public void loadRange(@NonNull LoadRangeParams params, + @NonNull LoadRangeCallback callback) { + List list = loadRange(params.startPosition, params.loadSize); + if (list != null) { + callback.onResult(list); + } else { + invalidate(); + } + } +} diff --git a/app/src/main/java/androidx/paging/TiledPagedList.java b/app/src/main/java/androidx/paging/TiledPagedList.java new file mode 100644 index 0000000000..e68869e486 --- /dev/null +++ b/app/src/main/java/androidx/paging/TiledPagedList.java @@ -0,0 +1,230 @@ +/* + * 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.paging; + +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; + +import java.util.List; +import java.util.concurrent.Executor; + +class TiledPagedList extends PagedList + implements PagedStorage.Callback { + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final PositionalDataSource mDataSource; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + PageResult.Receiver mReceiver = new PageResult.Receiver() { + // Creation thread for initial synchronous load, otherwise main thread + // Safe to access main thread only state - no other thread has reference during construction + @AnyThread + @Override + public void onPageResult(@PageResult.ResultType int type, + @NonNull PageResult pageResult) { + if (pageResult.isInvalid()) { + detach(); + return; + } + + if (isDetached()) { + // No op, have detached + return; + } + + if (type != PageResult.INIT && type != PageResult.TILE) { + throw new IllegalArgumentException("unexpected resultType" + type); + } + + List page = pageResult.page; + if (mStorage.getPageCount() == 0) { + mStorage.initAndSplit( + pageResult.leadingNulls, page, pageResult.trailingNulls, + pageResult.positionOffset, mConfig.pageSize, TiledPagedList.this); + } else { + mStorage.tryInsertPageAndTrim( + pageResult.positionOffset, + page, + mLastLoad, + mConfig.maxSize, + mRequiredRemainder, + TiledPagedList.this); + } + + if (mBoundaryCallback != null) { + boolean deferEmpty = mStorage.size() == 0; + boolean deferBegin = !deferEmpty + && pageResult.leadingNulls == 0 + && pageResult.positionOffset == 0; + int size = size(); + boolean deferEnd = !deferEmpty + && ((type == PageResult.INIT && pageResult.trailingNulls == 0) + || (type == PageResult.TILE + && (pageResult.positionOffset + mConfig.pageSize >= size))); + deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd); + } + } + }; + + @WorkerThread + TiledPagedList(@NonNull PositionalDataSource dataSource, + @NonNull Executor mainThreadExecutor, + @NonNull Executor backgroundThreadExecutor, + @Nullable BoundaryCallback boundaryCallback, + @NonNull Config config, + int position) { + super(new PagedStorage(), mainThreadExecutor, backgroundThreadExecutor, + boundaryCallback, config); + mDataSource = dataSource; + + final int pageSize = mConfig.pageSize; + mLastLoad = position; + + if (mDataSource.isInvalid()) { + detach(); + } else { + final int firstLoadSize = + (Math.max(mConfig.initialLoadSizeHint / pageSize, 2)) * pageSize; + + final int idealStart = position - firstLoadSize / 2; + final int roundedPageStart = Math.max(0, idealStart / pageSize * pageSize); + + mDataSource.dispatchLoadInitial(true, roundedPageStart, firstLoadSize, + pageSize, mMainThreadExecutor, mReceiver); + } + } + + @Override + boolean isContiguous() { + return false; + } + + @NonNull + @Override + public DataSource getDataSource() { + return mDataSource; + } + + @Nullable + @Override + public Object getLastKey() { + return mLastLoad; + } + + @Override + protected void dispatchUpdatesSinceSnapshot(@NonNull PagedList pagedListSnapshot, + @NonNull Callback callback) { + //noinspection UnnecessaryLocalVariable + final PagedStorage snapshot = pagedListSnapshot.mStorage; + + if (snapshot.isEmpty() + || mStorage.size() != snapshot.size()) { + throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear" + + " to be a snapshot of this PagedList"); + } + + // loop through each page and signal the callback for any pages that are present now, + // but not in the snapshot. + final int pageSize = mConfig.pageSize; + final int leadingNullPages = mStorage.getLeadingNullCount() / pageSize; + final int pageCount = mStorage.getPageCount(); + for (int i = 0; i < pageCount; i++) { + int pageIndex = i + leadingNullPages; + int updatedPages = 0; + // count number of consecutive pages that were added since the snapshot... + while (updatedPages < mStorage.getPageCount() + && mStorage.hasPage(pageSize, pageIndex + updatedPages) + && !snapshot.hasPage(pageSize, pageIndex + updatedPages)) { + updatedPages++; + } + // and signal them all at once to the callback + if (updatedPages > 0) { + callback.onChanged(pageIndex * pageSize, pageSize * updatedPages); + i += updatedPages - 1; + } + } + } + + @Override + protected void loadAroundInternal(int index) { + mStorage.allocatePlaceholders(index, mConfig.prefetchDistance, mConfig.pageSize, this); + } + + @Override + public void onInitialized(int count) { + notifyInserted(0, count); + } + + @Override + public void onPagePrepended(int leadingNulls, int changed, int added) { + throw new IllegalStateException("Contiguous callback on TiledPagedList"); + } + + @Override + public void onPageAppended(int endPosition, int changed, int added) { + throw new IllegalStateException("Contiguous callback on TiledPagedList"); + } + + @Override + public void onEmptyPrepend() { + throw new IllegalStateException("Contiguous callback on TiledPagedList"); + } + + @Override + public void onEmptyAppend() { + throw new IllegalStateException("Contiguous callback on TiledPagedList"); + } + + @Override + public void onPagePlaceholderInserted(final int pageIndex) { + // placeholder means initialize a load + mBackgroundThreadExecutor.execute(new Runnable() { + @Override + public void run() { + if (isDetached()) { + return; + } + final int pageSize = mConfig.pageSize; + + if (mDataSource.isInvalid()) { + detach(); + } else { + int startPosition = pageIndex * pageSize; + int count = Math.min(pageSize, mStorage.size() - startPosition); + mDataSource.dispatchLoadRange( + PageResult.TILE, startPosition, count, mMainThreadExecutor, mReceiver); + } + } + }); + } + + @Override + public void onPageInserted(int start, int count) { + notifyChanged(start, count); + } + + @Override + public void onPagesRemoved(int startOfDrops, int count) { + notifyRemoved(startOfDrops, count); + } + + @Override + public void onPagesSwappedToPlaceholder(int startOfDrops, int count) { + notifyChanged(startOfDrops, count); + } +} diff --git a/app/src/main/java/androidx/paging/WrapperItemKeyedDataSource.java b/app/src/main/java/androidx/paging/WrapperItemKeyedDataSource.java new file mode 100644 index 0000000000..dc9f908139 --- /dev/null +++ b/app/src/main/java/androidx/paging/WrapperItemKeyedDataSource.java @@ -0,0 +1,116 @@ +/* + * 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.paging; + +import androidx.annotation.NonNull; +import androidx.arch.core.util.Function; + +import java.util.IdentityHashMap; +import java.util.List; + +class WrapperItemKeyedDataSource extends ItemKeyedDataSource { + private final ItemKeyedDataSource mSource; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final Function, List> mListFunction; + + private final IdentityHashMap mKeyMap = new IdentityHashMap<>(); + + WrapperItemKeyedDataSource(ItemKeyedDataSource source, + Function, List> listFunction) { + mSource = source; + mListFunction = listFunction; + } + + @Override + public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { + mSource.addInvalidatedCallback(onInvalidatedCallback); + } + + @Override + public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { + mSource.removeInvalidatedCallback(onInvalidatedCallback); + } + + @Override + public void invalidate() { + mSource.invalidate(); + } + + @Override + public boolean isInvalid() { + return mSource.isInvalid(); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + List convertWithStashedKeys(List source) { + List dest = convert(mListFunction, source); + synchronized (mKeyMap) { + // synchronize on mKeyMap, since multiple loads may occur simultaneously. + // Note: manually sync avoids locking per-item (e.g. Collections.synchronizedMap) + for (int i = 0; i < dest.size(); i++) { + mKeyMap.put(dest.get(i), mSource.getKey(source.get(i))); + } + } + return dest; + } + + @Override + public void loadInitial(@NonNull LoadInitialParams params, + final @NonNull LoadInitialCallback callback) { + mSource.loadInitial(params, new LoadInitialCallback() { + @Override + public void onResult(@NonNull List data, int position, int totalCount) { + callback.onResult(convertWithStashedKeys(data), position, totalCount); + } + + @Override + public void onResult(@NonNull List data) { + callback.onResult(convertWithStashedKeys(data)); + } + }); + } + + @Override + public void loadAfter(@NonNull LoadParams params, + final @NonNull LoadCallback callback) { + mSource.loadAfter(params, new LoadCallback() { + @Override + public void onResult(@NonNull List data) { + callback.onResult(convertWithStashedKeys(data)); + } + }); + } + + @Override + public void loadBefore(@NonNull LoadParams params, + final @NonNull LoadCallback callback) { + mSource.loadBefore(params, new LoadCallback() { + @Override + public void onResult(@NonNull List data) { + callback.onResult(convertWithStashedKeys(data)); + } + }); + } + + @NonNull + @Override + public K getKey(@NonNull B item) { + synchronized (mKeyMap) { + return mKeyMap.get(item); + } + } +} diff --git a/app/src/main/java/androidx/paging/WrapperPageKeyedDataSource.java b/app/src/main/java/androidx/paging/WrapperPageKeyedDataSource.java new file mode 100644 index 0000000000..6658df10d3 --- /dev/null +++ b/app/src/main/java/androidx/paging/WrapperPageKeyedDataSource.java @@ -0,0 +1,96 @@ +/* + * 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.paging; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.arch.core.util.Function; + +import java.util.List; + +class WrapperPageKeyedDataSource extends PageKeyedDataSource { + private final PageKeyedDataSource mSource; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final Function, List> mListFunction; + + WrapperPageKeyedDataSource(PageKeyedDataSource source, + Function, List> listFunction) { + mSource = source; + mListFunction = listFunction; + } + + @Override + public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { + mSource.addInvalidatedCallback(onInvalidatedCallback); + } + + @Override + public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { + mSource.removeInvalidatedCallback(onInvalidatedCallback); + } + + @Override + public void invalidate() { + mSource.invalidate(); + } + + @Override + public boolean isInvalid() { + return mSource.isInvalid(); + } + + @Override + public void loadInitial(@NonNull LoadInitialParams params, + final @NonNull LoadInitialCallback callback) { + mSource.loadInitial(params, new LoadInitialCallback() { + @Override + public void onResult(@NonNull List data, int position, int totalCount, + @Nullable K previousPageKey, @Nullable K nextPageKey) { + callback.onResult(convert(mListFunction, data), position, totalCount, + previousPageKey, nextPageKey); + } + + @Override + public void onResult(@NonNull List data, @Nullable K previousPageKey, + @Nullable K nextPageKey) { + callback.onResult(convert(mListFunction, data), previousPageKey, nextPageKey); + } + }); + } + + @Override + public void loadBefore(@NonNull LoadParams params, + final @NonNull LoadCallback callback) { + mSource.loadBefore(params, new LoadCallback() { + @Override + public void onResult(@NonNull List data, @Nullable K adjacentPageKey) { + callback.onResult(convert(mListFunction, data), adjacentPageKey); + } + }); + } + + @Override + public void loadAfter(@NonNull LoadParams params, + final @NonNull LoadCallback callback) { + mSource.loadAfter(params, new LoadCallback() { + @Override + public void onResult(@NonNull List data, @Nullable K adjacentPageKey) { + callback.onResult(convert(mListFunction, data), adjacentPageKey); + } + }); + } +} diff --git a/app/src/main/java/androidx/paging/WrapperPositionalDataSource.java b/app/src/main/java/androidx/paging/WrapperPositionalDataSource.java new file mode 100644 index 0000000000..3a265d6eee --- /dev/null +++ b/app/src/main/java/androidx/paging/WrapperPositionalDataSource.java @@ -0,0 +1,81 @@ +/* + * 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.paging; + +import androidx.annotation.NonNull; +import androidx.arch.core.util.Function; + +import java.util.List; + +class WrapperPositionalDataSource extends PositionalDataSource { + private final PositionalDataSource mSource; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final Function, List> mListFunction; + + WrapperPositionalDataSource(PositionalDataSource source, + Function, List> listFunction) { + mSource = source; + mListFunction = listFunction; + } + + @Override + public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { + mSource.addInvalidatedCallback(onInvalidatedCallback); + } + + @Override + public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { + mSource.removeInvalidatedCallback(onInvalidatedCallback); + } + + @Override + public void invalidate() { + mSource.invalidate(); + } + + @Override + public boolean isInvalid() { + return mSource.isInvalid(); + } + + @Override + public void loadInitial(@NonNull LoadInitialParams params, + final @NonNull LoadInitialCallback callback) { + mSource.loadInitial(params, new LoadInitialCallback() { + @Override + public void onResult(@NonNull List data, int position, int totalCount) { + callback.onResult(convert(mListFunction, data), position, totalCount); + } + + @Override + public void onResult(@NonNull List data, int position) { + callback.onResult(convert(mListFunction, data), position); + } + }); + } + + @Override + public void loadRange(@NonNull LoadRangeParams params, + final @NonNull LoadRangeCallback callback) { + mSource.loadRange(params, new LoadRangeCallback() { + @Override + public void onResult(@NonNull List data) { + callback.onResult(convert(mListFunction, data)); + } + }); + } +}