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