mirror of https://github.com/M66B/FairEmail.git
parent
9f9aa0525e
commit
5785890083
@ -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<Integer, User> usersByLastName();
|
||||
* }
|
||||
*
|
||||
* class MyViewModel extends ViewModel {
|
||||
* public final LiveData<PagedList<User>> usersList;
|
||||
* public MyViewModel(UserDao userDao) {
|
||||
* usersList = new LivePagedListBuilder<>(
|
||||
* userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class MyActivity extends AppCompatActivity {
|
||||
* {@literal @}Override
|
||||
* public void onCreate(Bundle savedState) {
|
||||
* super.onCreate(savedState);
|
||||
* MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
|
||||
* RecyclerView recyclerView = findViewById(R.id.user_list);
|
||||
* final UserAdapter adapter = new UserAdapter();
|
||||
* viewModel.usersList.observe(this, pagedList -> adapter.submitList(pagedList));
|
||||
* recyclerView.setAdapter(adapter);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
|
||||
* private final AsyncPagedListDiffer<User> mDiffer
|
||||
* = new AsyncPagedListDiffer(this, DIFF_CALLBACK);
|
||||
* {@literal @}Override
|
||||
* public int getItemCount() {
|
||||
* return mDiffer.getItemCount();
|
||||
* }
|
||||
* public void submitList(PagedList<User> pagedList) {
|
||||
* mDiffer.submitList(pagedList);
|
||||
* }
|
||||
* {@literal @}Override
|
||||
* public void onBindViewHolder(UserViewHolder holder, int position) {
|
||||
* User user = mDiffer.getItem(position);
|
||||
* if (user != null) {
|
||||
* holder.bindTo(user);
|
||||
* } else {
|
||||
* // Null defines a placeholder item - AsyncPagedListDiffer will automatically
|
||||
* // invalidate this row when the actual object is loaded from the database
|
||||
* holder.clear();
|
||||
* }
|
||||
* }
|
||||
* public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
|
||||
* new DiffUtil.ItemCallback<User>() {
|
||||
* {@literal @}Override
|
||||
* public boolean areItemsTheSame(
|
||||
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
|
||||
* // User properties may have changed if reloaded from the DB, but ID is fixed
|
||||
* return oldUser.getId() == newUser.getId();
|
||||
* }
|
||||
* {@literal @}Override
|
||||
* public boolean areContentsTheSame(
|
||||
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
|
||||
* // NOTE: if you use equals, your object must properly override Object#equals()
|
||||
* // Incorrectly returning false here will result in too many animations.
|
||||
* return oldUser.equals(newUser);
|
||||
* }
|
||||
* }
|
||||
* }</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 < {@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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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
@ -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<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<Integer, User> usersByLastName();
|
||||
* }
|
||||
*
|
||||
* class MyViewModel extends ViewModel {
|
||||
* public final LiveData<PagedList<User>> usersList;
|
||||
* public MyViewModel(UserDao userDao) {
|
||||
* usersList = new LivePagedListBuilder<>(
|
||||
* userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class MyActivity extends AppCompatActivity {
|
||||
* {@literal @}Override
|
||||
* public void onCreate(Bundle savedState) {
|
||||
* super.onCreate(savedState);
|
||||
* MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
|
||||
* RecyclerView recyclerView = findViewById(R.id.user_list);
|
||||
* UserAdapter<User> adapter = new UserAdapter();
|
||||
* viewModel.usersList.observe(this, pagedList -> adapter.submitList(pagedList));
|
||||
* recyclerView.setAdapter(adapter);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
|
||||
* public UserAdapter() {
|
||||
* super(DIFF_CALLBACK);
|
||||
* }
|
||||
* {@literal @}Override
|
||||
* public void onBindViewHolder(UserViewHolder holder, int position) {
|
||||
* User user = getItem(position);
|
||||
* if (user != null) {
|
||||
* holder.bindTo(user);
|
||||
* } else {
|
||||
* // Null defines a placeholder item - PagedListAdapter will automatically invalidate
|
||||
* // this row when the actual object is loaded from the database
|
||||
* holder.clear();
|
||||
* }
|
||||
* }
|
||||
* public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
|
||||
* new DiffUtil.ItemCallback<User>() {
|
||||
* {@literal @}Override
|
||||
* public boolean areItemsTheSame(
|
||||
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
|
||||
* // User properties may have changed if reloaded from the DB, but ID is fixed
|
||||
* return oldUser.getId() == newUser.getId();
|
||||
* }
|
||||
* {@literal @}Override
|
||||
* public boolean areContentsTheSame(
|
||||
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
|
||||
* // NOTE: if you use equals, your object must properly override Object#equals()
|
||||
* // Incorrectly returning false here will result in too many animations.
|
||||
* return oldUser.equals(newUser);
|
||||
* }
|
||||
* }
|
||||
* }</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) {
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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<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<Item> {
|
||||
* private int computeCount() {
|
||||
* // actual count code here
|
||||
* }
|
||||
*
|
||||
* private List<Item> loadRangeInternal(int startPosition, int loadCount) {
|
||||
* // actual load code here
|
||||
* }
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void loadInitial({@literal @}NonNull LoadInitialParams params,
|
||||
* {@literal @}NonNull LoadInitialCallback<Item> callback) {
|
||||
* int totalCount = computeCount();
|
||||
* int position = computeInitialLoadPosition(params, totalCount);
|
||||
* int loadSize = computeInitialLoadSize(params, position, totalCount);
|
||||
* callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
|
||||
* }
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void loadRange({@literal @}NonNull LoadRangeParams params,
|
||||
* {@literal @}NonNull LoadRangeCallback<Item> callback) {
|
||||
* callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
|
||||
* }
|
||||
* }</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<Item> {
|
||||
* private int computeCount() {
|
||||
* // actual count code here
|
||||
* }
|
||||
*
|
||||
* private List<Item> loadRangeInternal(int startPosition, int loadCount) {
|
||||
* // actual load code here
|
||||
* }
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void loadInitial({@literal @}NonNull LoadInitialParams params,
|
||||
* {@literal @}NonNull LoadInitialCallback<Item> callback) {
|
||||
* int totalCount = computeCount();
|
||||
* int position = computeInitialLoadPosition(params, totalCount);
|
||||
* int loadSize = computeInitialLoadSize(params, position, totalCount);
|
||||
* callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
|
||||
* }
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void loadRange({@literal @}NonNull LoadRangeParams params,
|
||||
* {@literal @}NonNull LoadRangeCallback<Item> callback) {
|
||||
* callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
|
||||
* }
|
||||
* }</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));
|
||||
}
|
||||
}
|
@ -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) {
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in new issue