Revert "Revert "Compile paging inline""

This reverts commit 5ff77cacd8.
This commit is contained in:
M66B 2020-07-22 08:39:30 +02:00
parent 9f9aa0525e
commit 5785890083
21 changed files with 6077 additions and 1 deletions

View File

@ -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"

View File

@ -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}.
* <p>
* 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.
* <p>
* 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.
* <p>
* It provides a simple list-like API with {@link #getItem(int)} and {@link #getItemCount()} for an
* adapter to acquire and present data objects.
* <p>
* A complete usage pattern with Room would look like this:
* <pre>
* {@literal @}Dao
* interface UserDao {
* {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
* public abstract DataSource.Factory&lt;Integer, User> usersByLastName();
* }
*
* class MyViewModel extends ViewModel {
* public final LiveData&lt;PagedList&lt;User>> usersList;
* public MyViewModel(UserDao userDao) {
* usersList = new LivePagedListBuilder&lt;>(
* 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&lt;UserViewHolder> {
* private final AsyncPagedListDiffer&lt;User> mDiffer
* = new AsyncPagedListDiffer(this, DIFF_CALLBACK);
* {@literal @}Override
* public int getItemCount() {
* return mDiffer.getItemCount();
* }
* public void submitList(PagedList&lt;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&lt;User> DIFF_CALLBACK =
* new DiffUtil.ItemCallback&lt;User>() {
* {@literal @}Override
* public boolean areItemsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
* // User properties may have changed if reloaded from the DB, but ID is fixed
* return oldUser.getId() == newUser.getId();
* }
* {@literal @}Override
* public boolean areContentsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
* // NOTE: if you use equals, your object must properly override Object#equals()
* // Incorrectly returning false here will result in too many animations.
* return oldUser.equals(newUser);
* }
* }
* }</pre>
*
* @param <T> Type of the PagedLists this differ will receive.
*/
public class AsyncPagedListDiffer<T> {
// 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<T> mConfig;
@SuppressWarnings("RestrictedApi")
Executor mMainThreadExecutor = ArchTaskExecutor.getMainThreadExecutor();
/**
* Listener for when the current PagedList is updated.
*
* @param <T> Type of items in PagedList
*/
public interface PagedListListener<T> {
/**
* 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<T> previousList, @Nullable PagedList<T> currentList);
}
private final List<PagedListListener<T>> mListeners = new CopyOnWriteArrayList<>();
private boolean mIsContiguous;
private PagedList<T> mPagedList;
private PagedList<T> mSnapshot;
// Max generation of currently scheduled runnable
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mMaxScheduledGeneration;
/**
* Convenience for {@code AsyncPagedListDiffer(new AdapterListUpdateCallback(adapter),
* new AsyncDifferConfig.Builder<T>(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<T> diffCallback) {
mUpdateCallback = new AdapterListUpdateCallback(adapter);
mConfig = new AsyncDifferConfig.Builder<>(diffCallback).build();
}
@SuppressWarnings("WeakerAccess")
public AsyncPagedListDiffer(@NonNull ListUpdateCallback listUpdateCallback,
@NonNull AsyncDifferConfig<T> 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.
* <p>
* 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 &lt; {@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.
* <p>
* 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<T> pagedList) {
submitList(pagedList, null);
}
/**
* Pass a new PagedList to the differ.
* <p>
* 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}.
* <p>
* 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<T> 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<T> 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<T>) mPagedList.snapshot();
mPagedList = null;
}
if (mSnapshot == null || mPagedList != null) {
throw new IllegalStateException("must be in snapshot state to diff");
}
final PagedList<T> oldSnapshot = mSnapshot;
final PagedList<T> newSnapshot = (PagedList<T>) 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<T> newList,
@NonNull PagedList<T> 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<T> 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<T> previousList,
@Nullable PagedList<T> currentList,
@Nullable Runnable commitCallback) {
for (PagedListListener<T> 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<T> listener) {
mListeners.add(listener);
}
/**
* Remove a previously registered PagedListListener.
*
* @param listener Previously registered listener.
* @see #getCurrentList()
* @see #addPagedListListener(PagedListListener)
*/
public void removePagedListListener(@NonNull PagedListListener<T> listener) {
mListeners.remove(listener);
}
/**
* Returns the PagedList currently being displayed by the differ.
* <p>
* 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<T> getCurrentList() {
if (mSnapshot != null) {
return mSnapshot;
}
return mPagedList;
}
}

View File

@ -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<Key, Value> extends DataSource<Key, Value> {
@Override
boolean isContiguous() {
return true;
}
abstract void dispatchLoadInitial(
@Nullable Key key,
int initialLoadSize,
int pageSize,
boolean enablePlaceholders,
@NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver);
abstract void dispatchLoadAfter(
int currentEndIndex,
@NonNull Value currentEndItem,
int pageSize,
@NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver);
abstract void dispatchLoadBefore(
int currentBeginIndex,
@NonNull Value currentBeginItem,
int pageSize,
@NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver);
/**
* Get the key from either the position, or item, or null if position/item invalid.
* <p>
* 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;
}
}

View File

@ -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<K, V> extends PagedList<V> implements PagedStorage.Callback {
@SuppressWarnings("WeakerAccess") /* synthetic access */
final ContiguousDataSource<K, V> 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<V> mReceiver = new PageResult.Receiver<V>() {
// 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<V> pageResult) {
if (pageResult.isInvalid()) {
detach();
return;
}
if (isDetached()) {
// No op, have detached
return;
}
List<V> 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<K, V> dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback<V> boundaryCallback,
@NonNull Config config,
final @Nullable K key,
int lastLoad) {
super(new PagedStorage<V>(), 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<V> pagedListSnapshot, @NonNull Callback callback) {
final PagedStorage<V> 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<?, V> 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);
}
}

View File

@ -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}.
* <p>
* 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.
* <h4>Loading Pages</h4>
* 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.
* <p>
* 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.
* <h4>Updating Paged Data</h4>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <h4>Implementing a DataSource</h4>
* To implement, extend one of the subclasses: {@link PageKeyedDataSource},
* {@link ItemKeyedDataSource}, or {@link PositionalDataSource}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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 <Key> 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> Value type loaded by the DataSource.
*/
@SuppressWarnings("unused") // suppress warning to remove Key/Value, needed for subclass type safety
public abstract class DataSource<Key, Value> {
/**
* Factory for DataSources.
* <p>
* Data-loading systems of an application or library can implement this interface to allow
* {@code LiveData<PagedList>}s to be created. For example, Room can provide a
* DataSource.Factory for a given SQL query:
*
* <pre>
* {@literal @}Dao
* interface UserDao {
* {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
* public abstract DataSource.Factory&lt;Integer, User> usersByLastName();
* }
* </pre>
* 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> Key identifying items in DataSource.
* @param <Value> Type of items in the list loaded by the DataSources.
*/
public abstract static class Factory<Key, Value> {
/**
* Create a DataSource.
* <p>
* 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.
* <p>
* {@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<PagedList>} to observers.
*
* @return the new DataSource.
*/
@NonNull
public abstract DataSource<Key, Value> create();
/**
* Applies the given function to each value emitted by DataSources produced by this Factory.
* <p>
* 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 <ToValue> 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 <ToValue> DataSource.Factory<Key, ToValue> map(
@NonNull Function<Value, ToValue> function) {
return mapByPage(createListFunction(function));
}
/**
* Applies the given function to each value emitted by DataSources produced by this Factory.
* <p>
* 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 <ToValue> 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 <ToValue> DataSource.Factory<Key, ToValue> mapByPage(
@NonNull final Function<List<Value>, List<ToValue>> function) {
return new Factory<Key, ToValue>() {
@Override
public DataSource<Key, ToValue> create() {
return Factory.this.create().mapByPage(function);
}
};
}
}
@NonNull
static <X, Y> Function<List<X>, List<Y>> createListFunction(
final @NonNull Function<X, Y> innerFunc) {
return new Function<List<X>, List<Y>>() {
@Override
public List<Y> apply(@NonNull List<X> source) {
List<Y> out = new ArrayList<>(source.size());
for (int i = 0; i < source.size(); i++) {
out.add(innerFunc.apply(source.get(i)));
}
return out;
}
};
}
static <A, B> List<B> convert(Function<List<A>, List<B>> function, List<A> source) {
List<B> 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.
* <p>
* 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 <ToValue> 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 <ToValue> DataSource<Key, ToValue> mapByPage(
@NonNull Function<List<Value>, List<ToValue>> function);
/**
* Applies the given function to each value emitted by the DataSource.
* <p>
* 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 <ToValue> 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 <ToValue> DataSource<Key, ToValue> map(
@NonNull Function<Value, ToValue> function);
/**
* Returns true if the data source guaranteed to produce a contiguous set of items,
* never producing gaps.
*/
abstract boolean isContiguous();
static class LoadCallbackHelper<T> {
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<T> 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<T> 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.<T>getInvalidResult());
return true;
}
return false;
}
void dispatchResultToReceiver(final @NonNull PageResult<T> 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.
* <p>
* 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.
* <p>
* 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<InvalidatedCallback> mOnInvalidatedCallbacks =
new CopyOnWriteArrayList<>();
/**
* Add a callback to invoke when the DataSource is first invalidated.
* <p>
* Once invalidated, a data source will not become valid again.
* <p>
* 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.
* <p>
* 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();
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* The {@code InMemoryByItemRepository} in the
* <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
* shows how to implement a network ItemKeyedDataSource using
* <a href="https://square.github.io/retrofit/">Retrofit</a>, while
* handling swipe-to-refresh, network errors, and retry.
*
* @param <Key> Type of data used to query Value types out of the DataSource.
* @param <Value> Type of items being loaded by the DataSource.
*/
public abstract class ItemKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {
/**
* Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
*
* @param <Key> Type of data used to query Value types out of the DataSource.
*/
@SuppressWarnings("WeakerAccess")
public static class LoadInitialParams<Key> {
/**
* Load items around this key, or at the beginning of the data set if {@code null} is
* passed.
* <p>
* 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.
* <p>
* 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 <Key> Type of data used to query Value types out of the DataSource.
*/
@SuppressWarnings("WeakerAccess")
public static class LoadParams<Key> {
/**
* Load items before/after this key.
* <p>
* Returned data must begin directly adjacent to this position.
*/
@NonNull
public final Key key;
/**
* Requested number of items to load.
* <p>
* 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.
* <p>
* A callback can be called only once, and will throw if called again.
* <p>
* 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.
* <p>
* 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 <Value> Type of items being loaded.
*/
public abstract static class LoadInitialCallback<Value> extends LoadCallback<Value> {
/**
* Called to pass initial load state from a DataSource.
* <p>
* 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.
* <p>
* 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<Value> data, int position, int totalCount);
}
/**
* Callback for ItemKeyedDataSource {@link #loadBefore(LoadParams, LoadCallback)}
* and {@link #loadAfter(LoadParams, LoadCallback)} to return data.
* <p>
* A callback can be called only once, and will throw if called again.
* <p>
* 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 <Value> Type of items being loaded.
*/
public abstract static class LoadCallback<Value> {
/**
* Called to pass loaded data from a DataSource.
* <p>
* Call this method from your ItemKeyedDataSource's
* {@link #loadBefore(LoadParams, LoadCallback)} and
* {@link #loadAfter(LoadParams, LoadCallback)} methods to return data.
* <p>
* Call this from {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} to
* initialize without counting available data, or supporting placeholders.
* <p>
* 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<Value> data);
}
static class LoadInitialCallbackImpl<Value> extends LoadInitialCallback<Value> {
final LoadCallbackHelper<Value> mCallbackHelper;
private final boolean mCountingEnabled;
LoadInitialCallbackImpl(@NonNull ItemKeyedDataSource dataSource, boolean countingEnabled,
@NonNull PageResult.Receiver<Value> receiver) {
mCallbackHelper = new LoadCallbackHelper<>(dataSource, PageResult.INIT, null, receiver);
mCountingEnabled = countingEnabled;
}
@Override
public void onResult(@NonNull List<Value> 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<Value> data) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
}
}
}
static class LoadCallbackImpl<Value> extends LoadCallback<Value> {
final LoadCallbackHelper<Value> mCallbackHelper;
LoadCallbackImpl(@NonNull ItemKeyedDataSource dataSource, @PageResult.ResultType int type,
@Nullable Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
mCallbackHelper = new LoadCallbackHelper<>(
dataSource, type, mainThreadExecutor, receiver);
}
@Override
public void onResult(@NonNull List<Value> 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<Value> receiver) {
LoadInitialCallbackImpl<Value> 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<Value> 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<Value> receiver) {
loadBefore(new LoadParams<>(getKey(currentBeginItem), pageSize),
new LoadCallbackImpl<>(this, PageResult.PREPEND, mainThreadExecutor, receiver));
}
/**
* Load initial data.
* <p>
* 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.
* <p>
* {@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<Key> params,
@NonNull LoadInitialCallback<Value> callback);
/**
* Load list data after the key specified in {@link LoadParams#key LoadParams.key}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<Key> params,
@NonNull LoadCallback<Value> callback);
/**
* Load list data before the key specified in {@link LoadParams#key LoadParams.key}.
* <p>
* 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.
* <p>
* <p class="note"><strong>Note:</strong> 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.
* <p>
* 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.
* <p>
* 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<Key> params,
@NonNull LoadCallback<Value> callback);
/**
* Return a key associated with the given item.
* <p>
* 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.
* <p>
* 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<String, Integer>} 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 <ToValue> ItemKeyedDataSource<Key, ToValue> mapByPage(
@NonNull Function<List<Value>, List<ToValue>> function) {
return new WrapperItemKeyedDataSource<>(this, function);
}
@NonNull
@Override
public final <ToValue> ItemKeyedDataSource<Key, ToValue> map(
@NonNull Function<Value, ToValue> function) {
return mapByPage(createListFunction(function));
}
}

View File

@ -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<T> extends PositionalDataSource<T> {
private final List<T> mList;
public ListDataSource(List<T> list) {
mList = new ArrayList<>(list);
}
@Override
public void loadInitial(@NonNull LoadInitialParams params,
@NonNull LoadInitialCallback<T> 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<T> sublist = mList.subList(position, position + loadSize);
callback.onResult(sublist, position, totalCount);
}
@Override
public void loadRange(@NonNull LoadRangeParams params,
@NonNull LoadRangeCallback<T> callback) {
callback.onResult(mList.subList(params.startPosition,
params.startPosition + params.loadSize));
}
}

View File

@ -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<PagedList>}, given a {@link DataSource.Factory} and a
* {@link PagedList.Config}.
* <p>
* 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 <Key> Type of input valued used to load data from the DataSource. Must be integer if
* you're using PositionalDataSource.
* @param <Value> Item type being presented.
*/
public final class LivePagedListBuilder<Key, Value> {
private Key mInitialLoadKey;
private PagedList.Config mConfig;
private DataSource.Factory<Key, Value> 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<Key, Value> 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.
* <p>
* This method is a convenience for:
* <pre>
* LivePagedListBuilder(dataSourceFactory,
* new PagedList.Config.Builder().setPageSize(pageSize).build())
* </pre>
*
* @param dataSourceFactory DataSource.Factory providing DataSource generations.
* @param pageSize Size of pages to load.
*/
public LivePagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
int pageSize) {
this(dataSourceFactory, new PagedList.Config.Builder().setPageSize(pageSize).build());
}
/**
* First loading key passed to the first PagedList/DataSource.
* <p>
* 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<Key, Value> 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* Note that when using a BoundaryCallback with a {@code LiveData<PagedList>}, 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<Key, Value> setBoundaryCallback(
@Nullable PagedList.BoundaryCallback<Value> boundaryCallback) {
mBoundaryCallback = boundaryCallback;
return this;
}
/**
* Sets executor used for background fetching of PagedLists, and the pages within.
* <p>
* 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<Key, Value> setFetchExecutor(
@NonNull Executor fetchExecutor) {
mFetchExecutor = fetchExecutor;
return this;
}
/**
* Constructs the {@code LiveData<PagedList>}.
* <p>
* 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<PagedList<Value>> build() {
return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
ArchTaskExecutor.getMainThreadExecutor(), mFetchExecutor);
}
@AnyThread
@NonNull
@SuppressLint("RestrictedApi")
private static <Key, Value> LiveData<PagedList<Value>> create(
@Nullable final Key initialLoadKey,
@NonNull final PagedList.Config config,
@Nullable final PagedList.BoundaryCallback boundaryCallback,
@NonNull final DataSource.Factory<Key, Value> dataSourceFactory,
@NonNull final Executor notifyExecutor,
@NonNull final Executor fetchExecutor) {
return new ComputableLiveData<PagedList<Value>>(fetchExecutor) {
@Nullable
private PagedList<Value> mList;
@Nullable
private DataSource<Key, Value> mDataSource;
private final DataSource.InvalidatedCallback mCallback =
new DataSource.InvalidatedCallback() {
@Override
public void onInvalidated() {
invalidate();
}
};
@SuppressWarnings("unchecked") // for casting getLastKey to Key
@Override
protected PagedList<Value> 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();
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* The {@code InMemoryByPageRepository} in the
* <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
* shows how to implement a network PageKeyedDataSource using
* <a href="https://square.github.io/retrofit/">Retrofit</a>, while
* handling swipe-to-refresh, network errors, and retry.
*
* @param <Key> Type of data used to query Value types out of the DataSource.
* @param <Value> Type of items being loaded by the DataSource.
*/
public abstract class PageKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {
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 <Key> Type of data used to query pages.
*/
@SuppressWarnings("WeakerAccess")
public static class LoadInitialParams<Key> {
/**
* Requested number of items to load.
* <p>
* 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 <Key> Type of data used to query pages.
*/
@SuppressWarnings("WeakerAccess")
public static class LoadParams<Key> {
/**
* Load items before/after this key.
* <p>
* Returned data must begin directly adjacent to this position.
*/
@NonNull
public final Key key;
/**
* Requested number of items to load.
* <p>
* 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.
* <p>
* A callback can be called only once, and will throw if called again.
* <p>
* 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.
* <p>
* 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 <Key> Type of data used to query pages.
* @param <Value> Type of items being loaded.
*/
public abstract static class LoadInitialCallback<Key, Value> {
/**
* Called to pass initial load state from a DataSource.
* <p>
* 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.
* <p>
* 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<Value> data, int position, int totalCount,
@Nullable Key previousPageKey, @Nullable Key nextPageKey);
/**
* Called to pass loaded data from a DataSource.
* <p>
* Call this from {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} to
* initialize without counting available data, or supporting placeholders.
* <p>
* 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<Value> data, @Nullable Key previousPageKey,
@Nullable Key nextPageKey);
}
/**
* Callback for PageKeyedDataSource {@link #loadBefore(LoadParams, LoadCallback)} and
* {@link #loadAfter(LoadParams, LoadCallback)} to return data.
* <p>
* A callback can be called only once, and will throw if called again.
* <p>
* 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 <Key> Type of data used to query pages.
* @param <Value> Type of items being loaded.
*/
public abstract static class LoadCallback<Key, Value> {
/**
* Called to pass loaded data from a DataSource.
* <p>
* Call this method from your PageKeyedDataSource's
* {@link #loadBefore(LoadParams, LoadCallback)} and
* {@link #loadAfter(LoadParams, LoadCallback)} methods to return data.
* <p>
* 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.
* <p>
* 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<Value> data, @Nullable Key adjacentPageKey);
}
static class LoadInitialCallbackImpl<Key, Value> extends LoadInitialCallback<Key, Value> {
final LoadCallbackHelper<Value> mCallbackHelper;
private final PageKeyedDataSource<Key, Value> mDataSource;
private final boolean mCountingEnabled;
LoadInitialCallbackImpl(@NonNull PageKeyedDataSource<Key, Value> dataSource,
boolean countingEnabled, @NonNull PageResult.Receiver<Value> receiver) {
mCallbackHelper = new LoadCallbackHelper<>(
dataSource, PageResult.INIT, null, receiver);
mDataSource = dataSource;
mCountingEnabled = countingEnabled;
}
@Override
public void onResult(@NonNull List<Value> 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<Value> 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<Key, Value> extends LoadCallback<Key, Value> {
final LoadCallbackHelper<Value> mCallbackHelper;
private final PageKeyedDataSource<Key, Value> mDataSource;
LoadCallbackImpl(@NonNull PageKeyedDataSource<Key, Value> dataSource,
@PageResult.ResultType int type, @Nullable Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
mCallbackHelper = new LoadCallbackHelper<>(
dataSource, type, mainThreadExecutor, receiver);
mDataSource = dataSource;
}
@Override
public void onResult(@NonNull List<Value> 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<Value> receiver) {
LoadInitialCallbackImpl<Key, Value> 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<Value> 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.<Value>getEmptyResult());
}
}
@Override
final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
int pageSize, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> 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.<Value>getEmptyResult());
}
}
/**
* Load initial data.
* <p>
* 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.
* <p>
* {@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<Key> params,
@NonNull LoadInitialCallback<Key, Value> callback);
/**
* Prepend page with the key specified by {@link LoadParams#key LoadParams.key}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<Key> params,
@NonNull LoadCallback<Key, Value> callback);
/**
* Append page with the key specified by {@link LoadParams#key LoadParams.key}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<Key> params,
@NonNull LoadCallback<Key, Value> callback);
@NonNull
@Override
public final <ToValue> PageKeyedDataSource<Key, ToValue> mapByPage(
@NonNull Function<List<Value>, List<ToValue>> function) {
return new WrapperPageKeyedDataSource<>(this, function);
}
@NonNull
@Override
public final <ToValue> PageKeyedDataSource<Key, ToValue> map(
@NonNull Function<Value, ToValue> function) {
return mapByPage(createListFunction(function));
}
}

View File

@ -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<T> {
/**
* Single empty instance to avoid allocations.
* <p>
* 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 <T> PageResult<T> getEmptyResult() {
return EMPTY_RESULT;
}
@SuppressWarnings("unchecked")
static <T> PageResult<T> 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<T> page;
@SuppressWarnings("WeakerAccess")
public final int leadingNulls;
@SuppressWarnings("WeakerAccess")
public final int trailingNulls;
@SuppressWarnings("WeakerAccess")
public final int positionOffset;
PageResult(@NonNull List<T> list, int leadingNulls, int trailingNulls, int positionOffset) {
this.page = list;
this.leadingNulls = leadingNulls;
this.trailingNulls = trailingNulls;
this.positionOffset = positionOffset;
}
PageResult(@NonNull List<T> 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<T> {
@MainThread
public abstract void onPageResult(@ResultType int type, @NonNull PageResult<T> pageResult);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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}.
* <p>
* This class is a convenience wrapper around {@link AsyncPagedListDiffer} that implements common
* default behavior for item counting, and listening to PagedList update callbacks.
* <p>
* While using a LiveData&lt;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.
* <p>
* 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.
* <p>
* Handles both the internal paging of the list as more data is loaded, and updates in the form of
* new PagedLists.
* <p>
* A complete usage pattern with Room would look like this:
* <pre>
* {@literal @}Dao
* interface UserDao {
* {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
* public abstract DataSource.Factory&lt;Integer, User> usersByLastName();
* }
*
* class MyViewModel extends ViewModel {
* public final LiveData&lt;PagedList&lt;User>> usersList;
* public MyViewModel(UserDao userDao) {
* usersList = new LivePagedListBuilder&lt;>(
* 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&lt;User> adapter = new UserAdapter();
* viewModel.usersList.observe(this, pagedList -> adapter.submitList(pagedList));
* recyclerView.setAdapter(adapter);
* }
* }
*
* class UserAdapter extends PagedListAdapter&lt;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&lt;User> DIFF_CALLBACK =
* new DiffUtil.ItemCallback&lt;User>() {
* {@literal @}Override
* public boolean areItemsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
* // User properties may have changed if reloaded from the DB, but ID is fixed
* return oldUser.getId() == newUser.getId();
* }
* {@literal @}Override
* public boolean areContentsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
* // NOTE: if you use equals, your object must properly override Object#equals()
* // Incorrectly returning false here will result in too many animations.
* return oldUser.equals(newUser);
* }
* }
* }</pre>
*
* 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 <T> Type of the PagedLists this Adapter will receive.
* @param <VH> A class that extends ViewHolder that will be used by the adapter.
*/
public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
final AsyncPagedListDiffer<T> mDiffer;
private final AsyncPagedListDiffer.PagedListListener<T> mListener =
new AsyncPagedListDiffer.PagedListListener<T>() {
@Override
public void onCurrentListChanged(
@Nullable PagedList<T> previousList, @Nullable PagedList<T> 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<T> diffCallback) {
mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
mDiffer.addPagedListListener(mListener);
}
protected PagedListAdapter(@NonNull AsyncDifferConfig<T> config) {
mDiffer = new AsyncPagedListDiffer<>(new AdapterListUpdateCallback(this), config);
mDiffer.addPagedListListener(mListener);
}
/**
* Set the new list to be displayed.
* <p>
* 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<T> pagedList) {
mDiffer.submitList(pagedList);
}
/**
* Set the new list to be displayed.
* <p>
* 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.
* <p>
* 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<T> 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.
* <p>
* 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<T> getCurrentList() {
return mDiffer.getCurrentList();
}
/**
* Called when the current PagedList is updated.
* <p>
* 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)}.
* <p>
* This method will <em>not</em>be 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<T> currentList) {
}
/**
* Called when the current PagedList is updated.
* <p>
* 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)}.
* <p>
* This method will <em>not</em>be 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<T> previousList, @Nullable PagedList<T> currentList) {
}
}

View File

@ -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.
* <p>
* 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<T> extends AbstractList<T> {
/**
* 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<List<T>> 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<T> page, int trailingNulls) {
this();
init(leadingNulls, page, trailingNulls, 0);
}
private PagedStorage(PagedStorage<T> 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<T> snapshot() {
return new PagedStorage<>(this);
}
private void init(int leadingNulls, List<T> 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<T> 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<T> 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<T> 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<T> page = mPages.get(mPages.size() - 1);
return page.get(page.size() - 1);
}
void prependPage(@NonNull List<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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();
}
}

View File

@ -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 <T> DiffUtil.DiffResult computeDiff(
final PagedStorage<T> oldList,
final PagedStorage<T> newList,
final DiffUtil.ItemCallback<T> 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 <T> void dispatchDiff(ListUpdateCallback callback,
final PagedStorage<T> oldList,
final PagedStorage<T> 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));
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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)}.
* <p>
* Room can generate a Factory of PositionalDataSources for you:
* <pre>
* {@literal @}Dao
* interface UserDao {
* {@literal @}Query("SELECT * FROM user ORDER BY mAge DESC")
* public abstract DataSource.Factory&lt;Integer, User> loadUsersByAgeDesc();
* }</pre>
*
* @param <T> Type of items being loaded by the PositionalDataSource.
*/
public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
/**
* Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
*/
@SuppressWarnings("WeakerAccess")
public static class LoadInitialParams {
/**
* Initial load position requested.
* <p>
* 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.
* <p>
* Note that this may be larger than available data.
*/
public final int requestedLoadSize;
/**
* Defines page size acceptable for return values.
* <p>
* 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.
* <p>
* Returned data must start at this position.
*/
public final int startPosition;
/**
* Number of items to load.
* <p>
* 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.
* <p>
* A callback should be called only once, and may throw if called again.
* <p>
* 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 <T> Type of items being loaded.
*/
public abstract static class LoadInitialCallback<T> {
/**
* Called to pass initial load state from a DataSource.
* <p>
* 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<T> data, int position, int totalCount);
/**
* Called to pass initial load state from a DataSource without total count,
* when placeholders aren't requested.
* <p class="note"><strong>Note:</strong> This method can only be called when placeholders
* are disabled ({@link LoadInitialParams#placeholdersEnabled} is false).
* <p>
* 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<T> data, int position);
}
/**
* Callback for PositionalDataSource {@link #loadRange(LoadRangeParams, LoadRangeCallback)}
* to return data.
* <p>
* A callback should be called only once, and may throw if called again.
* <p>
* 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 <T> Type of items being loaded.
*/
public abstract static class LoadRangeCallback<T> {
/**
* 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<T> data);
}
static class LoadInitialCallbackImpl<T> extends LoadInitialCallback<T> {
final LoadCallbackHelper<T> mCallbackHelper;
private final boolean mCountingEnabled;
private final int mPageSize;
LoadInitialCallbackImpl(@NonNull PositionalDataSource dataSource, boolean countingEnabled,
int pageSize, PageResult.Receiver<T> 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<T> 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<T> 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<T> extends LoadRangeCallback<T> {
private LoadCallbackHelper<T> mCallbackHelper;
private final int mPositionOffset;
LoadRangeCallbackImpl(@NonNull PositionalDataSource dataSource,
@PageResult.ResultType int resultType, int positionOffset,
Executor mainThreadExecutor, PageResult.Receiver<T> receiver) {
mCallbackHelper = new LoadCallbackHelper<>(
dataSource, resultType, mainThreadExecutor, receiver);
mPositionOffset = positionOffset;
}
@Override
public void onResult(@NonNull List<T> 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<T> receiver) {
LoadInitialCallbackImpl<T> 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<T> receiver) {
LoadRangeCallback<T> callback = new LoadRangeCallbackImpl<>(
this, resultType, startPosition, mainThreadExecutor, receiver);
if (count == 0) {
callback.onResult(Collections.<T>emptyList());
} else {
loadRange(new LoadRangeParams(startPosition, count), callback);
}
}
/**
* Load initial list data.
* <p>
* This method is called to load the initial page(s) from the DataSource.
* <p>
* 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<T> callback);
/**
* Called to load a range of data from the DataSource.
* <p>
* This method is called to load additional pages from the DataSource after the
* LoadInitialCallback passed to dispatchLoadInitial has initialized a PagedList.
* <p>
* 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<T> callback);
@Override
boolean isContiguous() {
return false;
}
@NonNull
ContiguousDataSource<Integer, T> 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.
* <p>
* The value computed by this function will do bounds checking, page alignment, and positioning
* based on initial load size requested.
* <p>
* Example usage in a PositionalDataSource subclass:
* <pre>
* class ItemDataSource extends PositionalDataSource&lt;Item> {
* private int computeCount() {
* // actual count code here
* }
*
* private List&lt;Item> loadRangeInternal(int startPosition, int loadCount) {
* // actual load code here
* }
*
* {@literal @}Override
* public void loadInitial({@literal @}NonNull LoadInitialParams params,
* {@literal @}NonNull LoadInitialCallback&lt;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&lt;Item> callback) {
* callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
* }
* }</pre>
*
* @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.
* <p>
* This function takes the requested load size, and bounds checks it against the value returned
* by {@link #computeInitialLoadPosition(LoadInitialParams, int)}.
* <p>
* Example usage in a PositionalDataSource subclass:
* <pre>
* class ItemDataSource extends PositionalDataSource&lt;Item> {
* private int computeCount() {
* // actual count code here
* }
*
* private List&lt;Item> loadRangeInternal(int startPosition, int loadCount) {
* // actual load code here
* }
*
* {@literal @}Override
* public void loadInitial({@literal @}NonNull LoadInitialParams params,
* {@literal @}NonNull LoadInitialCallback&lt;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&lt;Item> callback) {
* callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
* }
* }</pre>
*
* @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<Value>
extends ContiguousDataSource<Integer, Value> {
@NonNull
final PositionalDataSource<Value> mSource;
ContiguousWithoutPlaceholdersWrapper(
@NonNull PositionalDataSource<Value> 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 <ToValue> DataSource<Integer, ToValue> mapByPage(
@NonNull Function<List<Value>, List<ToValue>> function) {
throw new UnsupportedOperationException(
"Inaccessible inner type doesn't support map op");
}
@NonNull
@Override
public <ToValue> DataSource<Integer, ToValue> map(
@NonNull Function<Value, ToValue> 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<Value> 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<Value> 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<Value> 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 <V> PositionalDataSource<V> mapByPage(
@NonNull Function<List<T>, List<V>> function) {
return new WrapperPositionalDataSource<>(this, function);
}
@NonNull
@Override
public final <V> PositionalDataSource<V> map(@NonNull Function<T, V> function) {
return mapByPage(createListFunction(function));
}
}

View File

@ -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<T> extends PagedList<T> {
private final boolean mContiguous;
private final Object mLastKey;
private final DataSource<?, T> mDataSource;
SnapshotPagedList(@NonNull PagedList<T> 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<?, T> getDataSource() {
return mDataSource;
}
@Override
void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> storageSnapshot,
@NonNull Callback callback) {
}
@Override
void loadAroundInternal(int index) {
}
}

View File

@ -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 <T> Type loaded by the TiledDataSource.
*
* @deprecated Use {@link PositionalDataSource}
* @hide
*/
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract class TiledDataSource<T> extends PositionalDataSource<T> {
@WorkerThread
public abstract int countItems();
@Override
boolean isContiguous() {
return false;
}
@Nullable
@WorkerThread
public abstract List<T> loadRange(int startPosition, int count);
@Override
public void loadInitial(@NonNull LoadInitialParams params,
@NonNull LoadInitialCallback<T> callback) {
int totalCount = countItems();
if (totalCount == 0) {
callback.onResult(Collections.<T>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<T> 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<T> callback) {
List<T> list = loadRange(params.startPosition, params.loadSize);
if (list != null) {
callback.onResult(list);
} else {
invalidate();
}
}
}

View File

@ -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<T> extends PagedList<T>
implements PagedStorage.Callback {
@SuppressWarnings("WeakerAccess") /* synthetic access */
final PositionalDataSource<T> mDataSource;
@SuppressWarnings("WeakerAccess") /* synthetic access */
PageResult.Receiver<T> mReceiver = new PageResult.Receiver<T>() {
// 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<T> 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<T> 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<T> dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback<T> boundaryCallback,
@NonNull Config config,
int position) {
super(new PagedStorage<T>(), 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<?, T> getDataSource() {
return mDataSource;
}
@Nullable
@Override
public Object getLastKey() {
return mLastLoad;
}
@Override
protected void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> pagedListSnapshot,
@NonNull Callback callback) {
//noinspection UnnecessaryLocalVariable
final PagedStorage<T> 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);
}
}

View File

@ -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<K, A, B> extends ItemKeyedDataSource<K, B> {
private final ItemKeyedDataSource<K, A> mSource;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Function<List<A>, List<B>> mListFunction;
private final IdentityHashMap<B, K> mKeyMap = new IdentityHashMap<>();
WrapperItemKeyedDataSource(ItemKeyedDataSource<K, A> source,
Function<List<A>, List<B>> 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<B> convertWithStashedKeys(List<A> source) {
List<B> 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<K> params,
final @NonNull LoadInitialCallback<B> callback) {
mSource.loadInitial(params, new LoadInitialCallback<A>() {
@Override
public void onResult(@NonNull List<A> data, int position, int totalCount) {
callback.onResult(convertWithStashedKeys(data), position, totalCount);
}
@Override
public void onResult(@NonNull List<A> data) {
callback.onResult(convertWithStashedKeys(data));
}
});
}
@Override
public void loadAfter(@NonNull LoadParams<K> params,
final @NonNull LoadCallback<B> callback) {
mSource.loadAfter(params, new LoadCallback<A>() {
@Override
public void onResult(@NonNull List<A> data) {
callback.onResult(convertWithStashedKeys(data));
}
});
}
@Override
public void loadBefore(@NonNull LoadParams<K> params,
final @NonNull LoadCallback<B> callback) {
mSource.loadBefore(params, new LoadCallback<A>() {
@Override
public void onResult(@NonNull List<A> data) {
callback.onResult(convertWithStashedKeys(data));
}
});
}
@NonNull
@Override
public K getKey(@NonNull B item) {
synchronized (mKeyMap) {
return mKeyMap.get(item);
}
}
}

View File

@ -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<K, A, B> extends PageKeyedDataSource<K, B> {
private final PageKeyedDataSource<K, A> mSource;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Function<List<A>, List<B>> mListFunction;
WrapperPageKeyedDataSource(PageKeyedDataSource<K, A> source,
Function<List<A>, List<B>> 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<K> params,
final @NonNull LoadInitialCallback<K, B> callback) {
mSource.loadInitial(params, new LoadInitialCallback<K, A>() {
@Override
public void onResult(@NonNull List<A> 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<A> data, @Nullable K previousPageKey,
@Nullable K nextPageKey) {
callback.onResult(convert(mListFunction, data), previousPageKey, nextPageKey);
}
});
}
@Override
public void loadBefore(@NonNull LoadParams<K> params,
final @NonNull LoadCallback<K, B> callback) {
mSource.loadBefore(params, new LoadCallback<K, A>() {
@Override
public void onResult(@NonNull List<A> data, @Nullable K adjacentPageKey) {
callback.onResult(convert(mListFunction, data), adjacentPageKey);
}
});
}
@Override
public void loadAfter(@NonNull LoadParams<K> params,
final @NonNull LoadCallback<K, B> callback) {
mSource.loadAfter(params, new LoadCallback<K, A>() {
@Override
public void onResult(@NonNull List<A> data, @Nullable K adjacentPageKey) {
callback.onResult(convert(mListFunction, data), adjacentPageKey);
}
});
}
}

View File

@ -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<A, B> extends PositionalDataSource<B> {
private final PositionalDataSource<A> mSource;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Function<List<A>, List<B>> mListFunction;
WrapperPositionalDataSource(PositionalDataSource<A> source,
Function<List<A>, List<B>> 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<B> callback) {
mSource.loadInitial(params, new LoadInitialCallback<A>() {
@Override
public void onResult(@NonNull List<A> data, int position, int totalCount) {
callback.onResult(convert(mListFunction, data), position, totalCount);
}
@Override
public void onResult(@NonNull List<A> data, int position) {
callback.onResult(convert(mListFunction, data), position);
}
});
}
@Override
public void loadRange(@NonNull LoadRangeParams params,
final @NonNull LoadRangeCallback<B> callback) {
mSource.loadRange(params, new LoadRangeCallback<A>() {
@Override
public void onResult(@NonNull List<A> data) {
callback.onResult(convert(mListFunction, data));
}
});
}
}