Initial support for PagedList loading state
Bug: 73246446
Bug: 112714476
Test: ./gradlew paging:paging-runtime:cC paging:paging-common:test
Add error/retry capability to PagedList start/end. Can be observed via
Adapter/Differ.
Still TODO:
- refresh error/retry. Requires coordination with observables.
- TiledPagedList state support
- NW + DB extension for DataSources (using new error/retry)
Change-Id: I4f6e19301775b6973fbae0b9668954d37f61ab5e
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index e62d10c..051ba3c 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -48,7 +48,7 @@
val MEDIAROUTER = Version("1.1.0-alpha01")
val MEDIA_WIDGET = Version("1.0.0-alpha5")
val NAVIGATION = Version("1.0.0-alpha07")
- val PAGING = Version("2.1.0-alpha01")
+ val PAGING = Version("2.2.0-alpha01")
val PALETTE = Version("1.1.0-alpha01")
val PERSISTENCE = Version("2.0.0")
val PREFERENCE = Version("1.1.0-alpha01")
diff --git a/paging/common/api/2.2.0-alpha01.txt b/paging/common/api/2.2.0-alpha01.txt
new file mode 100644
index 0000000..e5a1266
--- /dev/null
+++ b/paging/common/api/2.2.0-alpha01.txt
@@ -0,0 +1,213 @@
+// Signature format: 2.0
+package androidx.paging {
+
+ public abstract class DataSource<Key, Value> {
+ method @AnyThread public void addInvalidatedCallback(androidx.paging.DataSource.InvalidatedCallback);
+ method @AnyThread public void invalidate();
+ method @WorkerThread public boolean isInvalid();
+ method public abstract <ToValue> androidx.paging.DataSource<Key,ToValue> map(androidx.arch.core.util.Function<Value,ToValue>);
+ method public abstract <ToValue> androidx.paging.DataSource<Key,ToValue> mapByPage(androidx.arch.core.util.Function<java.util.List<Value>,java.util.List<ToValue>>);
+ method @AnyThread public void removeInvalidatedCallback(androidx.paging.DataSource.InvalidatedCallback);
+ }
+
+ public abstract static class DataSource.Factory<Key, Value> {
+ ctor public DataSource.Factory();
+ method public abstract androidx.paging.DataSource<Key,Value> create();
+ method public <ToValue> androidx.paging.DataSource.Factory<Key,ToValue> map(androidx.arch.core.util.Function<Value,ToValue>);
+ method public <ToValue> androidx.paging.DataSource.Factory<Key,ToValue> mapByPage(androidx.arch.core.util.Function<java.util.List<Value>,java.util.List<ToValue>>);
+ }
+
+ public static interface DataSource.InvalidatedCallback {
+ method @AnyThread public void onInvalidated();
+ }
+
+ public abstract class ItemKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
+ method public abstract Key getKey(Value);
+ method public abstract void loadAfter(androidx.paging.ItemKeyedDataSource.LoadParams<Key>, androidx.paging.ItemKeyedDataSource.LoadCallback<Value>);
+ method public abstract void loadBefore(androidx.paging.ItemKeyedDataSource.LoadParams<Key>, androidx.paging.ItemKeyedDataSource.LoadCallback<Value>);
+ method public abstract void loadInitial(androidx.paging.ItemKeyedDataSource.LoadInitialParams<Key>, androidx.paging.ItemKeyedDataSource.LoadInitialCallback<Value>);
+ method public final <ToValue> androidx.paging.ItemKeyedDataSource<Key,ToValue> map(androidx.arch.core.util.Function<Value,ToValue>);
+ method public final <ToValue> androidx.paging.ItemKeyedDataSource<Key,ToValue> mapByPage(androidx.arch.core.util.Function<java.util.List<Value>,java.util.List<ToValue>>);
+ }
+
+ public abstract static class ItemKeyedDataSource.LoadCallback<Value> {
+ ctor public ItemKeyedDataSource.LoadCallback();
+ method public void onError(Throwable);
+ method public abstract void onResult(java.util.List<Value>);
+ method public void onRetryableError(Throwable);
+ }
+
+ public abstract static class ItemKeyedDataSource.LoadInitialCallback<Value> extends androidx.paging.ItemKeyedDataSource.LoadCallback<Value> {
+ ctor public ItemKeyedDataSource.LoadInitialCallback();
+ method public abstract void onResult(java.util.List<Value>, int, int);
+ }
+
+ public static class ItemKeyedDataSource.LoadInitialParams<Key> {
+ ctor public ItemKeyedDataSource.LoadInitialParams(Key?, int, boolean);
+ field public final boolean placeholdersEnabled;
+ field public final Key? requestedInitialKey;
+ field public final int requestedLoadSize;
+ }
+
+ public static class ItemKeyedDataSource.LoadParams<Key> {
+ ctor public ItemKeyedDataSource.LoadParams(Key, int);
+ field public final Key key;
+ field public final int requestedLoadSize;
+ }
+
+ public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
+ method public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key>, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value>);
+ method public abstract void loadBefore(androidx.paging.PageKeyedDataSource.LoadParams<Key>, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value>);
+ method public abstract void loadInitial(androidx.paging.PageKeyedDataSource.LoadInitialParams<Key>, androidx.paging.PageKeyedDataSource.LoadInitialCallback<Key,Value>);
+ method public final <ToValue> androidx.paging.PageKeyedDataSource<Key,ToValue> map(androidx.arch.core.util.Function<Value,ToValue>);
+ method public final <ToValue> androidx.paging.PageKeyedDataSource<Key,ToValue> mapByPage(androidx.arch.core.util.Function<java.util.List<Value>,java.util.List<ToValue>>);
+ }
+
+ public abstract static class PageKeyedDataSource.LoadCallback<Key, Value> {
+ ctor public PageKeyedDataSource.LoadCallback();
+ method public void onError(Throwable);
+ method public abstract void onResult(java.util.List<Value>, Key?);
+ method public void onRetryableError(Throwable);
+ }
+
+ public abstract static class PageKeyedDataSource.LoadInitialCallback<Key, Value> {
+ ctor public PageKeyedDataSource.LoadInitialCallback();
+ method public void onError(Throwable);
+ method public abstract void onResult(java.util.List<Value>, int, int, Key?, Key?);
+ method public abstract void onResult(java.util.List<Value>, Key?, Key?);
+ method public void onRetryableError(Throwable);
+ }
+
+ public static class PageKeyedDataSource.LoadInitialParams<Key> {
+ ctor public PageKeyedDataSource.LoadInitialParams(int, boolean);
+ field public final boolean placeholdersEnabled;
+ field public final int requestedLoadSize;
+ }
+
+ public static class PageKeyedDataSource.LoadParams<Key> {
+ ctor public PageKeyedDataSource.LoadParams(Key, int);
+ field public final Key key;
+ field public final int requestedLoadSize;
+ }
+
+ public abstract class PagedList<T> extends java.util.AbstractList<T> {
+ method public void addWeakCallback(java.util.List<T>?, androidx.paging.PagedList.Callback);
+ method public void addWeakLoadStateListener(androidx.paging.PagedList.LoadStateListener);
+ method public void detach();
+ method public T? get(int);
+ method public androidx.paging.PagedList.Config getConfig();
+ method public abstract androidx.paging.DataSource<?,T> getDataSource();
+ method public abstract Object? getLastKey();
+ method public int getLoadedCount();
+ method public int getPositionOffset();
+ method public boolean isDetached();
+ method public boolean isImmutable();
+ method public void loadAround(int);
+ method public void removeWeakCallback(androidx.paging.PagedList.Callback);
+ method public void removeWeakLoadStateListener(androidx.paging.PagedList.LoadStateListener);
+ method public void retry();
+ method public int size();
+ method public java.util.List<T> snapshot();
+ }
+
+ @MainThread public abstract static class PagedList.BoundaryCallback<T> {
+ ctor public PagedList.BoundaryCallback();
+ method public void onItemAtEndLoaded(T);
+ method public void onItemAtFrontLoaded(T);
+ method public void onZeroItemsLoaded();
+ }
+
+ public static final class PagedList.Builder<Key, Value> {
+ ctor public PagedList.Builder(androidx.paging.DataSource<Key,Value>, androidx.paging.PagedList.Config);
+ ctor public PagedList.Builder(androidx.paging.DataSource<Key,Value>, int);
+ method @WorkerThread public androidx.paging.PagedList<Value> build();
+ method public androidx.paging.PagedList.Builder<Key,Value> setBoundaryCallback(androidx.paging.PagedList.BoundaryCallback?);
+ method public androidx.paging.PagedList.Builder<Key,Value> setFetchExecutor(java.util.concurrent.Executor);
+ method public androidx.paging.PagedList.Builder<Key,Value> setInitialKey(Key?);
+ method public androidx.paging.PagedList.Builder<Key,Value> setNotifyExecutor(java.util.concurrent.Executor);
+ }
+
+ public abstract static class PagedList.Callback {
+ ctor public PagedList.Callback();
+ method public abstract void onChanged(int, int);
+ method public abstract void onInserted(int, int);
+ method public abstract void onRemoved(int, int);
+ }
+
+ public static class PagedList.Config {
+ field public static final int MAX_SIZE_UNBOUNDED = 2147483647; // 0x7fffffff
+ field public final boolean enablePlaceholders;
+ field public final int initialLoadSizeHint;
+ field public final int maxSize;
+ field public final int pageSize;
+ field public final int prefetchDistance;
+ }
+
+ public static final class PagedList.Config.Builder {
+ ctor public PagedList.Config.Builder();
+ method public androidx.paging.PagedList.Config build();
+ method public androidx.paging.PagedList.Config.Builder setEnablePlaceholders(boolean);
+ method public androidx.paging.PagedList.Config.Builder setInitialLoadSizeHint(@IntRange(from=1) int);
+ method public androidx.paging.PagedList.Config.Builder setMaxSize(@IntRange(from=2) int);
+ method public androidx.paging.PagedList.Config.Builder setPageSize(@IntRange(from=1) int);
+ method public androidx.paging.PagedList.Config.Builder setPrefetchDistance(@IntRange(from=0) int);
+ }
+
+ public static enum PagedList.LoadState {
+ enum_constant public static final androidx.paging.PagedList.LoadState DONE;
+ enum_constant public static final androidx.paging.PagedList.LoadState ERROR;
+ enum_constant public static final androidx.paging.PagedList.LoadState IDLE;
+ enum_constant public static final androidx.paging.PagedList.LoadState LOADING;
+ enum_constant public static final androidx.paging.PagedList.LoadState RETRYABLE_ERROR;
+ }
+
+ public static interface PagedList.LoadStateListener {
+ method public void onLoadStateChanged(androidx.paging.PagedList.LoadType, androidx.paging.PagedList.LoadState, Throwable?);
+ }
+
+ public static enum PagedList.LoadType {
+ enum_constant public static final androidx.paging.PagedList.LoadType END;
+ enum_constant public static final androidx.paging.PagedList.LoadType REFRESH;
+ enum_constant public static final androidx.paging.PagedList.LoadType START;
+ }
+
+ public abstract class PositionalDataSource<T> extends androidx.paging.DataSource<java.lang.Integer,T> {
+ method public static int computeInitialLoadPosition(androidx.paging.PositionalDataSource.LoadInitialParams, int);
+ method public static int computeInitialLoadSize(androidx.paging.PositionalDataSource.LoadInitialParams, int, int);
+ method @WorkerThread public abstract void loadInitial(androidx.paging.PositionalDataSource.LoadInitialParams, androidx.paging.PositionalDataSource.LoadInitialCallback<T>);
+ method @WorkerThread public abstract void loadRange(androidx.paging.PositionalDataSource.LoadRangeParams, androidx.paging.PositionalDataSource.LoadRangeCallback<T>);
+ method public final <V> androidx.paging.PositionalDataSource<V> map(androidx.arch.core.util.Function<T,V>);
+ method public final <V> androidx.paging.PositionalDataSource<V> mapByPage(androidx.arch.core.util.Function<java.util.List<T>,java.util.List<V>>);
+ }
+
+ public abstract static class PositionalDataSource.LoadInitialCallback<T> {
+ ctor public PositionalDataSource.LoadInitialCallback();
+ method public void onError(Throwable);
+ method public abstract void onResult(java.util.List<T>, int, int);
+ method public abstract void onResult(java.util.List<T>, int);
+ method public void onRetryableError(Throwable);
+ }
+
+ public static class PositionalDataSource.LoadInitialParams {
+ ctor public PositionalDataSource.LoadInitialParams(int, int, int, boolean);
+ field public final int pageSize;
+ field public final boolean placeholdersEnabled;
+ field public final int requestedLoadSize;
+ field public final int requestedStartPosition;
+ }
+
+ public abstract static class PositionalDataSource.LoadRangeCallback<T> {
+ ctor public PositionalDataSource.LoadRangeCallback();
+ method public void onError(Throwable);
+ method public abstract void onResult(java.util.List<T>);
+ method public void onRetryableError(Throwable);
+ }
+
+ public static class PositionalDataSource.LoadRangeParams {
+ ctor public PositionalDataSource.LoadRangeParams(int, int);
+ field public final int loadSize;
+ field public final int startPosition;
+ }
+
+}
+
diff --git a/paging/common/api/current.txt b/paging/common/api/current.txt
index c8e0be1..e5a1266 100644
--- a/paging/common/api/current.txt
+++ b/paging/common/api/current.txt
@@ -32,7 +32,9 @@
public abstract static class ItemKeyedDataSource.LoadCallback<Value> {
ctor public ItemKeyedDataSource.LoadCallback();
+ method public void onError(Throwable);
method public abstract void onResult(java.util.List<Value>);
+ method public void onRetryableError(Throwable);
}
public abstract static class ItemKeyedDataSource.LoadInitialCallback<Value> extends androidx.paging.ItemKeyedDataSource.LoadCallback<Value> {
@@ -63,13 +65,17 @@
public abstract static class PageKeyedDataSource.LoadCallback<Key, Value> {
ctor public PageKeyedDataSource.LoadCallback();
+ method public void onError(Throwable);
method public abstract void onResult(java.util.List<Value>, Key?);
+ method public void onRetryableError(Throwable);
}
public abstract static class PageKeyedDataSource.LoadInitialCallback<Key, Value> {
ctor public PageKeyedDataSource.LoadInitialCallback();
+ method public void onError(Throwable);
method public abstract void onResult(java.util.List<Value>, int, int, Key?, Key?);
method public abstract void onResult(java.util.List<Value>, Key?, Key?);
+ method public void onRetryableError(Throwable);
}
public static class PageKeyedDataSource.LoadInitialParams<Key> {
@@ -86,6 +92,7 @@
public abstract class PagedList<T> extends java.util.AbstractList<T> {
method public void addWeakCallback(java.util.List<T>?, androidx.paging.PagedList.Callback);
+ method public void addWeakLoadStateListener(androidx.paging.PagedList.LoadStateListener);
method public void detach();
method public T? get(int);
method public androidx.paging.PagedList.Config getConfig();
@@ -97,6 +104,8 @@
method public boolean isImmutable();
method public void loadAround(int);
method public void removeWeakCallback(androidx.paging.PagedList.Callback);
+ method public void removeWeakLoadStateListener(androidx.paging.PagedList.LoadStateListener);
+ method public void retry();
method public int size();
method public java.util.List<T> snapshot();
}
@@ -144,6 +153,24 @@
method public androidx.paging.PagedList.Config.Builder setPrefetchDistance(@IntRange(from=0) int);
}
+ public static enum PagedList.LoadState {
+ enum_constant public static final androidx.paging.PagedList.LoadState DONE;
+ enum_constant public static final androidx.paging.PagedList.LoadState ERROR;
+ enum_constant public static final androidx.paging.PagedList.LoadState IDLE;
+ enum_constant public static final androidx.paging.PagedList.LoadState LOADING;
+ enum_constant public static final androidx.paging.PagedList.LoadState RETRYABLE_ERROR;
+ }
+
+ public static interface PagedList.LoadStateListener {
+ method public void onLoadStateChanged(androidx.paging.PagedList.LoadType, androidx.paging.PagedList.LoadState, Throwable?);
+ }
+
+ public static enum PagedList.LoadType {
+ enum_constant public static final androidx.paging.PagedList.LoadType END;
+ enum_constant public static final androidx.paging.PagedList.LoadType REFRESH;
+ enum_constant public static final androidx.paging.PagedList.LoadType START;
+ }
+
public abstract class PositionalDataSource<T> extends androidx.paging.DataSource<java.lang.Integer,T> {
method public static int computeInitialLoadPosition(androidx.paging.PositionalDataSource.LoadInitialParams, int);
method public static int computeInitialLoadSize(androidx.paging.PositionalDataSource.LoadInitialParams, int, int);
@@ -155,8 +182,10 @@
public abstract static class PositionalDataSource.LoadInitialCallback<T> {
ctor public PositionalDataSource.LoadInitialCallback();
+ method public void onError(Throwable);
method public abstract void onResult(java.util.List<T>, int, int);
method public abstract void onResult(java.util.List<T>, int);
+ method public void onRetryableError(Throwable);
}
public static class PositionalDataSource.LoadInitialParams {
@@ -169,7 +198,9 @@
public abstract static class PositionalDataSource.LoadRangeCallback<T> {
ctor public PositionalDataSource.LoadRangeCallback();
+ method public void onError(Throwable);
method public abstract void onResult(java.util.List<T>);
+ method public void onRetryableError(Throwable);
}
public static class PositionalDataSource.LoadRangeParams {
diff --git a/paging/common/ktx/api/2.2.0-alpha01.txt b/paging/common/ktx/api/2.2.0-alpha01.txt
new file mode 100644
index 0000000..0f070c2
--- /dev/null
+++ b/paging/common/ktx/api/2.2.0-alpha01.txt
@@ -0,0 +1,15 @@
+// Signature format: 2.0
+package androidx.paging {
+
+ public final class PagedListConfigKt {
+ ctor public PagedListConfigKt();
+ method public static androidx.paging.PagedList.Config Config(int pageSize, int prefetchDistance = pageSize, boolean enablePlaceholders = true, int initialLoadSizeHint = pageSize * androidx.paging.PagedList.Config.Builder.DEFAULT_INITIAL_PAGE_MULTIPLIER, int maxSize = 2147483647);
+ }
+
+ public final class PagedListKt {
+ ctor public PagedListKt();
+ method public static <Key, Value> androidx.paging.PagedList<Value> PagedList(androidx.paging.DataSource<Key,Value> dataSource, androidx.paging.PagedList.Config config, java.util.concurrent.Executor notifyExecutor, java.util.concurrent.Executor fetchExecutor, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, Key? initialKey = null);
+ }
+
+}
+
diff --git a/paging/common/src/main/java/androidx/paging/ContiguousPagedList.java b/paging/common/src/main/java/androidx/paging/ContiguousPagedList.java
index 9906067..7233b7e 100644
--- a/paging/common/src/main/java/androidx/paging/ContiguousPagedList.java
+++ b/paging/common/src/main/java/androidx/paging/ContiguousPagedList.java
@@ -16,15 +16,11 @@
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;
@@ -32,21 +28,6 @@
@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 */
@@ -80,6 +61,8 @@
if (resultType == PageResult.INIT) {
mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
pageResult.positionOffset, ContiguousPagedList.this);
+ // TODO: signal that this list is ready to be dispatched to observer
+
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
@@ -99,7 +82,7 @@
if (skipNewPage && !trimFromFront) {
// don't append this data, drop it
mAppendItemsRequested = 0;
- mAppendWorkerState = READY_TO_FETCH;
+ mLoadStateManager.setState(LoadType.END, LoadState.IDLE, null);
} else {
mStorage.appendPage(page, ContiguousPagedList.this);
}
@@ -107,7 +90,7 @@
if (skipNewPage && trimFromFront) {
// don't append this data, drop it
mPrependItemsRequested = 0;
- mPrependWorkerState = READY_TO_FETCH;
+ mLoadStateManager.setState(LoadType.START, LoadState.IDLE, null);
} else {
mStorage.prependPage(page, ContiguousPagedList.this);
}
@@ -116,25 +99,28 @@
}
if (mShouldTrim) {
+ // Try and trim, but only if the side being trimmed isn't actually fetching.
+ // For simplicity (both of impl here, and contract w/ DataSource) we don't want
+ // simultaneous fetches in same direction.
if (trimFromFront) {
- if (mPrependWorkerState != FETCHING) {
+ if (mLoadStateManager.getStart() != LoadState.LOADING) {
if (mStorage.trimFromFront(
mReplacePagesWithNulls,
mConfig.maxSize,
mRequiredRemainder,
ContiguousPagedList.this)) {
// trimmed from front, ensure we can fetch in that dir
- mPrependWorkerState = READY_TO_FETCH;
+ mLoadStateManager.setState(LoadType.START, LoadState.IDLE, null);
}
}
} else {
- if (mAppendWorkerState != FETCHING) {
+ if (mLoadStateManager.getEnd() != LoadState.LOADING) {
if (mStorage.trimFromEnd(
mReplacePagesWithNulls,
mConfig.maxSize,
mRequiredRemainder,
ContiguousPagedList.this)) {
- mAppendWorkerState = READY_TO_FETCH;
+ mLoadStateManager.setState(LoadType.END, LoadState.IDLE, null);
}
}
}
@@ -152,8 +138,34 @@
deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
}
}
+
+ @Override
+ public void onPageError(@PageResult.ResultType int resultType,
+ @NonNull Throwable error, boolean retryable) {
+ LoadState errorState = retryable ? LoadState.RETRYABLE_ERROR : LoadState.ERROR;
+
+ if (resultType == PageResult.PREPEND) {
+ mLoadStateManager.setState(LoadType.START, errorState, error);
+ } else if (resultType == PageResult.APPEND) {
+ mLoadStateManager.setState(LoadType.END, errorState, error);
+ } else {
+ // TODO: pass init signal through to *previous* list
+ throw new IllegalStateException("TODO");
+ }
+ }
};
+ @Override
+ public void retry() {
+ super.retry();
+ if (mLoadStateManager.getStart() == LoadState.RETRYABLE_ERROR) {
+ schedulePrepend();
+ }
+ if (mLoadStateManager.getEnd() == LoadState.RETRYABLE_ERROR) {
+ scheduleAppend();
+ }
+ }
+
static final int LAST_LOAD_UNSPECIFIED = -1;
ContiguousPagedList(
@@ -162,7 +174,7 @@
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback<V> boundaryCallback,
@NonNull Config config,
- final @Nullable K key,
+ @Nullable K key,
int lastLoad) {
super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
boundaryCallback, config);
@@ -251,22 +263,19 @@
mStorage.getLeadingNullCount() + mStorage.getStorageCount());
mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
- if (mPrependItemsRequested > 0) {
+ if (mPrependItemsRequested > 0 && mLoadStateManager.getStart() == LoadState.IDLE) {
schedulePrepend();
}
mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
- if (mAppendItemsRequested > 0) {
+ if (mAppendItemsRequested > 0 && mLoadStateManager.getEnd() == LoadState.IDLE) {
scheduleAppend();
}
}
@MainThread
private void schedulePrepend() {
- if (mPrependWorkerState != READY_TO_FETCH) {
- return;
- }
- mPrependWorkerState = FETCHING;
+ mLoadStateManager.setState(LoadType.START, LoadState.LOADING, null);
final int position = mStorage.getLeadingNullCount() + mStorage.getPositionOffset();
@@ -290,10 +299,7 @@
@MainThread
private void scheduleAppend() {
- if (mAppendWorkerState != READY_TO_FETCH) {
- return;
- }
- mAppendWorkerState = FETCHING;
+ mLoadStateManager.setState(LoadType.END, LoadState.LOADING, null);
final int position = mStorage.getLeadingNullCount()
+ mStorage.getStorageCount() - 1 + mStorage.getPositionOffset();
@@ -347,10 +353,11 @@
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();
+ } else {
+ mLoadStateManager.setState(LoadType.START, LoadState.IDLE, null);
}
// finally dispatch callbacks, after prepend may have already been scheduled
@@ -363,7 +370,7 @@
@MainThread
@Override
public void onEmptyPrepend() {
- mPrependWorkerState = DONE_FETCHING;
+ mLoadStateManager.setState(LoadType.START, LoadState.DONE, null);
}
@MainThread
@@ -371,10 +378,11 @@
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();
+ } else {
+ mLoadStateManager.setState(LoadType.END, LoadState.IDLE, null);
}
// finally dispatch callbacks, after append may have already been scheduled
@@ -385,7 +393,7 @@
@MainThread
@Override
public void onEmptyAppend() {
- mAppendWorkerState = DONE_FETCHING;
+ mLoadStateManager.setState(LoadType.END, LoadState.DONE, null);
}
@MainThread
diff --git a/paging/common/src/main/java/androidx/paging/DataSource.java b/paging/common/src/main/java/androidx/paging/DataSource.java
index 60f3cfe..fe4f126 100644
--- a/paging/common/src/main/java/androidx/paging/DataSource.java
+++ b/paging/common/src/main/java/androidx/paging/DataSource.java
@@ -17,6 +17,7 @@
package androidx.paging;
import androidx.annotation.AnyThread;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
@@ -277,6 +278,8 @@
// mSignalLock protects mPostExecutor, and mHasSignalled
private final Object mSignalLock = new Object();
private Executor mPostExecutor = null;
+
+ @GuardedBy("mSignalLock")
private boolean mHasSignalled = false;
LoadCallbackHelper(@NonNull DataSource dataSource, @PageResult.ResultType int resultType,
@@ -298,6 +301,7 @@
*
* @return true if DataSource was invalid, and invalid result dispatched
*/
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
boolean dispatchInvalidResultIfInvalid() {
if (mDataSource.isInvalid()) {
dispatchResultToReceiver(PageResult.<T>getInvalidResult());
@@ -306,12 +310,21 @@
return false;
}
- void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
+ void dispatchResultToReceiver(@NonNull PageResult<T> result) {
+ dispatchToReceiver(result, null, false);
+ }
+
+ void dispatchErrorToReceiver(@NonNull Throwable error, boolean retryable) {
+ dispatchToReceiver(null, error, retryable);
+ }
+
+ private void dispatchToReceiver(final @Nullable PageResult<T> result,
+ final @Nullable Throwable error, final boolean retryable) {
Executor executor;
synchronized (mSignalLock) {
if (mHasSignalled) {
throw new IllegalStateException(
- "callback.onResult already called, cannot call again.");
+ "callback.onResult/onError already called, cannot call again.");
}
mHasSignalled = true;
executor = mPostExecutor;
@@ -321,11 +334,21 @@
executor.execute(new Runnable() {
@Override
public void run() {
- mReceiver.onPageResult(mResultType, result);
+ dispatchOnCurrentThread(result, error, retryable);
}
});
} else {
+ dispatchOnCurrentThread(result, error, retryable);
+ }
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ void dispatchOnCurrentThread(@Nullable PageResult<T> result,
+ @Nullable Throwable error, boolean retryable) {
+ if (result != null) {
mReceiver.onPageResult(mResultType, result);
+ } else {
+ mReceiver.onPageError(mResultType, error, retryable);
}
}
}
diff --git a/paging/common/src/main/java/androidx/paging/ItemKeyedDataSource.java b/paging/common/src/main/java/androidx/paging/ItemKeyedDataSource.java
index 2e89ba6..493aba7 100644
--- a/paging/common/src/main/java/androidx/paging/ItemKeyedDataSource.java
+++ b/paging/common/src/main/java/androidx/paging/ItemKeyedDataSource.java
@@ -153,7 +153,6 @@
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.
@@ -183,6 +182,38 @@
* @param data List of items loaded from the ItemKeyedDataSource.
*/
public abstract void onResult(@NonNull List<Value> data);
+
+ /**
+ * Called to report a non-retryable error from a DataSource.
+ * <p>
+ * Call this method to report a non-retryable error from
+ * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
+ * {@link #loadBefore(LoadParams, LoadCallback)}, or
+ * {@link #loadAfter(LoadParams, LoadCallback)} methods.
+ *
+ * @param error The error that occurred during loading.
+ */
+ public void onError(@NonNull Throwable error) {
+ // TODO: remove default implementation in 3.0
+ throw new IllegalStateException(
+ "You must implement onError if implementing your own load callback");
+ }
+
+ /**
+ * Called to report a retryable error from a DataSource.
+ * <p>
+ * Call this method to report a retryable error from
+ * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
+ * {@link #loadBefore(LoadParams, LoadCallback)}, or
+ * {@link #loadAfter(LoadParams, LoadCallback)} methods.
+ *
+ * @param error The error that occurred during loading.
+ */
+ public void onRetryableError(@NonNull Throwable error) {
+ // TODO: remove default implementation in 3.0
+ throw new IllegalStateException(
+ "You must implement onRetryableError if implementing your own load callback");
+ }
}
static class LoadInitialCallbackImpl<Value> extends LoadInitialCallback<Value> {
@@ -215,6 +246,16 @@
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
}
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ mCallbackHelper.dispatchErrorToReceiver(error, false);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ mCallbackHelper.dispatchErrorToReceiver(error, true);
+ }
}
static class LoadCallbackImpl<Value> extends LoadCallback<Value> {
@@ -233,6 +274,16 @@
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
}
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ mCallbackHelper.dispatchErrorToReceiver(error, false);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ mCallbackHelper.dispatchErrorToReceiver(error, true);
+ }
}
@Nullable
diff --git a/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java b/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java
index 076d791..81e8626 100644
--- a/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java
+++ b/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java
@@ -205,6 +205,34 @@
*/
public abstract void onResult(@NonNull List<Value> data, @Nullable Key previousPageKey,
@Nullable Key nextPageKey);
+
+ /**
+ * Called to report a non-retryable error from a DataSource.
+ * <p>
+ * Call this method to report a non-retryable error from
+ * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
+ *
+ * @param error The error that occurred during loading.
+ */
+ public void onError(@NonNull Throwable error) {
+ // TODO: remove default implementation in 3.0
+ throw new IllegalStateException(
+ "You must implement onError if implementing your own load callback");
+ }
+
+ /**
+ * Called to report a retryable error from a DataSource.
+ * <p>
+ * Call this method to report an error from
+ * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
+ *
+ * @param error The error that occurred during loading.
+ */
+ public void onRetryableError(@NonNull Throwable error) {
+ // TODO: remove default implementation in 3.0
+ throw new IllegalStateException(
+ "You must implement onRetryableError if implementing your own load callback");
+ }
}
/**
@@ -244,6 +272,36 @@
* no more pages to load in the current load direction.
*/
public abstract void onResult(@NonNull List<Value> data, @Nullable Key adjacentPageKey);
+
+ /**
+ * Called to report a non-retryable error from a DataSource.
+ * <p>
+ * Call this method to report a non-retryable error from your PageKeyedDataSource's
+ * {@link #loadBefore(LoadParams, LoadCallback)} and
+ * {@link #loadAfter(LoadParams, LoadCallback)} methods.
+ *
+ * @param error The error that occurred during loading.
+ */
+ public void onError(@NonNull Throwable error) {
+ // TODO: remove default implementation in 3.0
+ throw new IllegalStateException(
+ "You must implement onError if implementing your own load callback");
+ }
+
+ /**
+ * Called to report a retryable error from a DataSource.
+ * <p>
+ * Call this method to report an error from your PageKeyedDataSource's
+ * {@link #loadBefore(LoadParams, LoadCallback)} and
+ * {@link #loadAfter(LoadParams, LoadCallback)} methods.
+ *
+ * @param error The error that occurred during loading.
+ */
+ public void onRetryableError(@NonNull Throwable error) {
+ // TODO: remove default implementation in 3.0
+ throw new IllegalStateException(
+ "You must implement onRetryableError if implementing your own load callback");
+ }
}
static class LoadInitialCallbackImpl<Key, Value> extends LoadInitialCallback<Key, Value> {
@@ -285,6 +343,16 @@
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
}
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ mCallbackHelper.dispatchErrorToReceiver(error, false);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ mCallbackHelper.dispatchErrorToReceiver(error, true);
+ }
}
static class LoadCallbackImpl<Key, Value> extends LoadCallback<Key, Value> {
@@ -309,6 +377,16 @@
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
}
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ mCallbackHelper.dispatchErrorToReceiver(error, false);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ mCallbackHelper.dispatchErrorToReceiver(error, true);
+ }
}
@Nullable
diff --git a/paging/common/src/main/java/androidx/paging/PageResult.java b/paging/common/src/main/java/androidx/paging/PageResult.java
index e654a73..d231d2b 100644
--- a/paging/common/src/main/java/androidx/paging/PageResult.java
+++ b/paging/common/src/main/java/androidx/paging/PageResult.java
@@ -101,5 +101,9 @@
abstract static class Receiver<T> {
@MainThread
public abstract void onPageResult(@ResultType int type, @NonNull PageResult<T> pageResult);
+
+ @MainThread
+ public abstract void onPageError(@ResultType int type,
+ @NonNull Throwable error, boolean retryable);
}
}
diff --git a/paging/common/src/main/java/androidx/paging/PagedList.java b/paging/common/src/main/java/androidx/paging/PagedList.java
index 1d4a184..9070301 100644
--- a/paging/common/src/main/java/androidx/paging/PagedList.java
+++ b/paging/common/src/main/java/androidx/paging/PagedList.java
@@ -113,6 +113,204 @@
*/
public abstract class PagedList<T> extends AbstractList<T> {
+ /**
+ * Type of load a PagedList can perform.
+ * <p>
+ * You can use a {@link LoadStateListener} to observe {@link LoadState} of
+ * any {@link LoadType}. For UI purposes (swipe refresh, loading spinner, retry button), this
+ * is typically done by registering a Listener with the {@code PagedListAdapter} or
+ * {@code AsyncPagedListDiffer}.
+ *
+ * @see LoadState
+ */
+ public enum LoadType {
+ /**
+ * PagedList content being reloaded, may contain content updates.
+ */
+ REFRESH,
+
+ /**
+ * Load at the start of the PagedList.
+ */
+ START,
+
+ /**
+ * Load at the end of the PagedList.
+ */
+ END
+ }
+
+ /**
+ * State of a PagedList load - associated with a {@code LoadType}
+ * <p>
+ * You can use a {@link LoadStateListener} to observe {@link LoadState} of
+ * any {@link LoadType}. For UI purposes (swipe refresh, loading spinner, retry button), this
+ * is typically done by registering a Listener with the {@code PagedListAdapter} or
+ * {@code AsyncPagedListDiffer}.
+ */
+ public enum LoadState {
+ /**
+ * Indicates the PagedList is not currently loading, and no error currently observed.
+ */
+ IDLE,
+
+ /**
+ * Loading is in progress.
+ */
+ LOADING,
+
+ /**
+ * Loading is complete.
+ */
+ DONE,
+
+ /**
+ * Loading hit a non-retryable error.
+ */
+ ERROR,
+
+ /**
+ * Loading hit a retryable error.
+ *
+ * @see #retry()
+ */
+ RETRYABLE_ERROR,
+ }
+
+
+
+
+ /**
+ * Listener for changes to loading state - whether the refresh, prepend, or append is idle,
+ * loading, or has an error.
+ * <p>
+ * Can be used to observe the {@link LoadState} of any {@link LoadType} (REFRESH/START/END).
+ * For UI purposes (swipe refresh, loading spinner, retry button), this is typically done by
+ * registering a Listener with the {@code PagedListAdapter} or {@code AsyncPagedListDiffer}.
+ * <p>
+ * These calls will be dispatched on the executor defined by
+ * {@link Builder#setNotifyExecutor(Executor)}, which is generally the main/UI thread.
+ *
+ * @see LoadType
+ * @see LoadState
+ */
+ public interface LoadStateListener {
+ /**
+ * Called when the LoadState has changed - whether the refresh, prepend, or append is
+ * idle, loading, or has an error.
+ * <p>
+ * REFRESH events can be used to drive a {@code SwipeRefreshLayout}, or START/END events
+ * can be used to drive loading spinner items in your {@code RecyclerView}.
+ *
+ * @param type Type of load - START, END, or REFRESH.
+ * @param state State of load - IDLE, LOADING, DONE, ERROR, or RETRYABLE_ERROR
+ * @param error Error, if in an error state, null otherwise.
+ *
+ * @see #retry()
+ */
+ void onLoadStateChanged(@NonNull LoadType type,
+ @NonNull LoadState state, @Nullable Throwable error);
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ static boolean equalsHelper(@Nullable Object a, @Nullable Object b) {
+ // Because Objects.equals() is API 19+
+ return a == b || (a != null && a.equals(b));
+ }
+
+ abstract static class LoadStateManager {
+ @NonNull
+ private LoadState mRefresh = LoadState.IDLE;
+ @Nullable
+ private Throwable mRefreshError = null;
+ @NonNull
+ private LoadState mStart = LoadState.IDLE;
+ @Nullable
+ private Throwable mStartError = null;
+ @NonNull
+ private LoadState mEnd = LoadState.IDLE;
+ @Nullable
+ private Throwable mEndError = null;
+
+ @NonNull
+ public LoadState getRefresh() {
+ return mRefresh;
+ }
+
+ @NonNull
+ public LoadState getStart() {
+ return mStart;
+ }
+
+ @NonNull
+ public LoadState getEnd() {
+ return mEnd;
+ }
+
+ @Nullable
+ public Throwable getRefreshError() {
+ return mRefreshError;
+ }
+
+ @Nullable
+ public Throwable getStartError() {
+ return mStartError;
+ }
+
+ @Nullable
+ public Throwable getEndError() {
+ return mEndError;
+ }
+
+ void setState(@NonNull LoadType type, @NonNull LoadState state, @Nullable Throwable error) {
+ boolean expectError = state == LoadState.RETRYABLE_ERROR || state == LoadState.ERROR;
+ boolean hasError = error != null;
+ if (expectError != hasError) {
+ throw new IllegalArgumentException(
+ "Error states must be accompanied by a throwable, other states must not");
+ }
+
+ // deduplicate signals
+ switch (type) {
+ case REFRESH:
+ if (mRefresh.equals(state) && equalsHelper(mRefreshError, error)) return;
+ mRefresh = state;
+ mRefreshError = error;
+ break;
+ case START:
+ if (mStart.equals(state) && equalsHelper(mStartError, error)) return;
+ mStart = state;
+ mStartError = error;
+ break;
+ case END:
+ if (mEnd.equals(state) && equalsHelper(mEndError, error)) return;
+ mEnd = state;
+ mEndError = error;
+ break;
+ }
+ onStateChanged(type, state, error);
+ }
+
+ protected abstract void onStateChanged(@NonNull LoadType type,
+ @NonNull LoadState state, @Nullable Throwable error);
+ }
+
+ /**
+ * Retry any retryable errors associated with this PagedList.
+ * <p>
+ * If for example a network DataSource append timed out, calling this method will retry the
+ * failed append load. Note that your DataSource will need to pass {@code true} to
+ * {@code onError()} to signify the error as retryable.
+ * <p>
+ * You can observe loading state via {@link #addWeakLoadStateListener(LoadStateListener)},
+ * though generally this is done through the {@link PagedListAdapter} or
+ * {@link AsyncPagedListDiffer}.
+ *
+ * @see #addWeakLoadStateListener(LoadStateListener)
+ * @see #removeWeakLoadStateListener(LoadStateListener)
+ */
+ public void retry() {}
+
// Notes on threading:
//
// PagedList and its subclasses are passed and accessed on multiple threads, but are always
@@ -164,6 +362,31 @@
private final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final ArrayList<WeakReference<LoadStateListener>> mListeners = new ArrayList<>();
+
+ final LoadStateManager mLoadStateManager = new LoadStateManager() {
+ @Override
+ protected void onStateChanged(@NonNull final LoadType type, @NonNull final LoadState state,
+ @Nullable final Throwable error) {
+ // new state, dispatch to listeners
+ // Post, since UI will want to react immediately
+ mMainThreadExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ final LoadStateListener currentListener = mListeners.get(i).get();
+ if (currentListener == null) {
+ mListeners.remove(i);
+ } else {
+ currentListener.onLoadStateChanged(type, state, error);
+ }
+ }
+ }
+ });
+ }
+ };
+
PagedList(@NonNull PagedStorage<T> storage,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@@ -680,6 +903,49 @@
return mStorage.getPositionOffset();
}
+
+ /**
+ * Add a LoadStateListener to observe the loading state of the PagedList.
+ *
+ * @param listener Listener to receive updates.
+ *
+ * @see #removeWeakLoadStateListener(LoadStateListener)
+ */
+ public void addWeakLoadStateListener(@NonNull LoadStateListener listener) {
+ // first, clean up any empty weak refs
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ final LoadStateListener currentListener = mListeners.get(i).get();
+ if (currentListener == null) {
+ mListeners.remove(i);
+ }
+ }
+
+ // then add the new one
+ mListeners.add(new WeakReference<>(listener));
+ listener.onLoadStateChanged(PagedList.LoadType.REFRESH, mLoadStateManager.getRefresh(),
+ mLoadStateManager.getRefreshError());
+ listener.onLoadStateChanged(PagedList.LoadType.START, mLoadStateManager.getStart(),
+ mLoadStateManager.getStartError());
+ listener.onLoadStateChanged(PagedList.LoadType.END, mLoadStateManager.getEnd(),
+ mLoadStateManager.getEndError());
+ }
+
+ /**
+ * Remove a previously registered LoadStateListener.
+ *
+ * @param listener Previously registered listener.
+ * @see #addWeakLoadStateListener(LoadStateListener)
+ */
+ public void removeWeakLoadStateListener(@NonNull LoadStateListener listener) {
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ final LoadStateListener currentListener = mListeners.get(i).get();
+ if (currentListener == null || currentListener == listener) {
+ // found Listener, or empty weak ref
+ mListeners.remove(i);
+ }
+ }
+ }
+
/**
* Adds a callback, and issues updates since the previousSnapshot was created.
* <p>
diff --git a/paging/common/src/main/java/androidx/paging/PositionalDataSource.java b/paging/common/src/main/java/androidx/paging/PositionalDataSource.java
index 158aa1e..0be0a0a 100644
--- a/paging/common/src/main/java/androidx/paging/PositionalDataSource.java
+++ b/paging/common/src/main/java/androidx/paging/PositionalDataSource.java
@@ -103,7 +103,7 @@
@SuppressWarnings("WeakerAccess")
public static class LoadRangeParams {
/**
- * Start position of data to load.
+ * START position of data to load.
* <p>
* Returned data must start at this position.
*/
@@ -173,6 +173,34 @@
* pass {@code N}.
*/
public abstract void onResult(@NonNull List<T> data, int position);
+
+ /**
+ * Called to report a non-retryable error from a DataSource.
+ * <p>
+ * Call this method to report a non-retryable error from
+ * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
+ *
+ * @param error The error that occurred during loading.
+ */
+ public void onError(@NonNull Throwable error) {
+ // TODO: remove default implementation in 3.0
+ throw new IllegalStateException(
+ "You must implement onError if implementing your own load callback");
+ }
+
+ /**
+ * Called to report a retryable error from a DataSource.
+ * <p>
+ * Call this method to report a retryable error from
+ * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
+ *
+ * @param error The error that occurred during loading.
+ */
+ public void onRetryableError(@NonNull Throwable error) {
+ // TODO: remove default implementation in 3.0
+ throw new IllegalStateException(
+ "You must implement onRetryableError if implementing your own load callback");
+ }
}
/**
@@ -195,6 +223,34 @@
* unless at end of list.
*/
public abstract void onResult(@NonNull List<T> data);
+
+ /**
+ * Called to report a non-retryable error from a DataSource.
+ * <p>
+ * Call this method to report a non-retryable error from
+ * {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
+ *
+ * @param error The error that occurred during loading.
+ */
+ public void onError(@NonNull Throwable error) {
+ // TODO: remove default implementation in 3.0
+ throw new IllegalStateException(
+ "You must implement onError if implementing your own load callback");
+ }
+
+ /**
+ * Called to report a retryable error from a DataSource.
+ * <p>
+ * Call this method to report a retryable error from
+ * {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
+ *
+ * @param error The error that occurred during loading.
+ */
+ public void onRetryableError(@NonNull Throwable error) {
+ // TODO: remove default implementation in 3.0
+ throw new IllegalStateException(
+ "You must implement onRetryableError if implementing your own load callback");
+ }
}
static class LoadInitialCallbackImpl<T> extends LoadInitialCallback<T> {
@@ -253,6 +309,16 @@
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
}
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ mCallbackHelper.dispatchErrorToReceiver(error, false);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ mCallbackHelper.dispatchErrorToReceiver(error, true);
+ }
}
static class LoadRangeCallbackImpl<T> extends LoadRangeCallback<T> {
@@ -273,6 +339,16 @@
data, 0, 0, mPositionOffset));
}
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ mCallbackHelper.dispatchErrorToReceiver(error, false);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ mCallbackHelper.dispatchErrorToReceiver(error, true);
+ }
}
final void dispatchLoadInitial(boolean acceptCount,
diff --git a/paging/common/src/main/java/androidx/paging/TiledPagedList.java b/paging/common/src/main/java/androidx/paging/TiledPagedList.java
index e68869e..c01fa83 100644
--- a/paging/common/src/main/java/androidx/paging/TiledPagedList.java
+++ b/paging/common/src/main/java/androidx/paging/TiledPagedList.java
@@ -79,6 +79,11 @@
deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
}
}
+
+ @Override
+ public void onPageError(int type, @NonNull Throwable error, boolean retryable) {
+ throw new IllegalStateException("Tiled error handling not yet implemented");
+ }
};
@WorkerThread
diff --git a/paging/common/src/main/java/androidx/paging/WrapperItemKeyedDataSource.java b/paging/common/src/main/java/androidx/paging/WrapperItemKeyedDataSource.java
index dc9f908..dc9e3b7 100644
--- a/paging/common/src/main/java/androidx/paging/WrapperItemKeyedDataSource.java
+++ b/paging/common/src/main/java/androidx/paging/WrapperItemKeyedDataSource.java
@@ -81,6 +81,16 @@
public void onResult(@NonNull List<A> data) {
callback.onResult(convertWithStashedKeys(data));
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ callback.onError(error);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ callback.onRetryableError(error);
+ }
});
}
@@ -92,6 +102,16 @@
public void onResult(@NonNull List<A> data) {
callback.onResult(convertWithStashedKeys(data));
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ callback.onError(error);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ callback.onRetryableError(error);
+ }
});
}
@@ -103,6 +123,16 @@
public void onResult(@NonNull List<A> data) {
callback.onResult(convertWithStashedKeys(data));
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ callback.onError(error);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ callback.onRetryableError(error);
+ }
});
}
diff --git a/paging/common/src/main/java/androidx/paging/WrapperPageKeyedDataSource.java b/paging/common/src/main/java/androidx/paging/WrapperPageKeyedDataSource.java
index 6658df1..0010df5 100644
--- a/paging/common/src/main/java/androidx/paging/WrapperPageKeyedDataSource.java
+++ b/paging/common/src/main/java/androidx/paging/WrapperPageKeyedDataSource.java
@@ -69,6 +69,16 @@
@Nullable K nextPageKey) {
callback.onResult(convert(mListFunction, data), previousPageKey, nextPageKey);
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ callback.onError(error);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ callback.onRetryableError(error);
+ }
});
}
@@ -80,6 +90,16 @@
public void onResult(@NonNull List<A> data, @Nullable K adjacentPageKey) {
callback.onResult(convert(mListFunction, data), adjacentPageKey);
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ callback.onError(error);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ callback.onRetryableError(error);
+ }
});
}
@@ -91,6 +111,16 @@
public void onResult(@NonNull List<A> data, @Nullable K adjacentPageKey) {
callback.onResult(convert(mListFunction, data), adjacentPageKey);
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ callback.onError(error);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ callback.onRetryableError(error);
+ }
});
}
}
diff --git a/paging/common/src/main/java/androidx/paging/WrapperPositionalDataSource.java b/paging/common/src/main/java/androidx/paging/WrapperPositionalDataSource.java
index 3a265d6..012af75 100644
--- a/paging/common/src/main/java/androidx/paging/WrapperPositionalDataSource.java
+++ b/paging/common/src/main/java/androidx/paging/WrapperPositionalDataSource.java
@@ -65,6 +65,16 @@
public void onResult(@NonNull List<A> data, int position) {
callback.onResult(convert(mListFunction, data), position);
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ callback.onError(error);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ callback.onRetryableError(error);
+ }
});
}
@@ -76,6 +86,16 @@
public void onResult(@NonNull List<A> data) {
callback.onResult(convert(mListFunction, data));
}
+
+ @Override
+ public void onError(@NonNull Throwable error) {
+ callback.onError(error);
+ }
+
+ @Override
+ public void onRetryableError(@NonNull Throwable error) {
+ callback.onRetryableError(error);
+ }
});
}
}
diff --git a/paging/common/src/test/java/androidx/paging/ContiguousPagedListTest.kt b/paging/common/src/test/java/androidx/paging/ContiguousPagedListTest.kt
index d0d596c..5099839 100644
--- a/paging/common/src/test/java/androidx/paging/ContiguousPagedListTest.kt
+++ b/paging/common/src/test/java/androidx/paging/ContiguousPagedListTest.kt
@@ -17,6 +17,9 @@
package androidx.paging
import androidx.arch.core.util.Function
+import androidx.paging.PagedList.LoadState.LOADING
+import androidx.paging.PagedList.LoadState.IDLE
+import androidx.paging.PagedList.LoadState.RETRYABLE_ERROR
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -33,7 +36,7 @@
import java.util.concurrent.Executor
@RunWith(Parameterized::class)
-class ContiguousPagedListTest(private val mCounted: Boolean) {
+class ContiguousPagedListTest(private val placeholdersEnabled: Boolean) {
private val mMainThread = TestExecutor()
private val mBackgroundThread = TestExecutor()
@@ -58,15 +61,23 @@
val convertPosition = key ?: 0
val position = Math.max(0, (convertPosition - initialLoadSize / 2))
val data = getClampedRange(position, position + initialLoadSize)
- val trailingUnloadedCount = listData.size - position - data.size
+ if (data != null) {
+ val trailingUnloadedCount = listData.size - position - data.size
- if (enablePlaceholders && mCounted) {
- receiver.onPageResult(PageResult.INIT,
- PageResult(data, position, trailingUnloadedCount, 0))
+ if (enablePlaceholders && placeholdersEnabled) {
+ receiver.onPageResult(
+ PageResult.INIT,
+ PageResult(data, position, trailingUnloadedCount, 0)
+ )
+ } else {
+ // still must pass offset, even if not counted
+ receiver.onPageResult(
+ PageResult.INIT,
+ PageResult(data, position)
+ )
+ }
} else {
- // still must pass offset, even if not counted
- receiver.onPageResult(PageResult.INIT,
- PageResult(data, position))
+ receiver.onPageError(PageResult.INIT, Exception(), true)
}
}
@@ -81,7 +92,11 @@
val data = getClampedRange(startIndex, startIndex + pageSize)
mainThreadExecutor.execute {
- receiver.onPageResult(PageResult.APPEND, PageResult(data, 0, 0, 0))
+ if (data != null) {
+ receiver.onPageResult(PageResult.APPEND, PageResult(data, 0, 0, 0))
+ } else {
+ receiver.onPageError(PageResult.APPEND, Exception(), true)
+ }
}
}
@@ -97,7 +112,11 @@
val data = getClampedRange(startIndex - pageSize + 1, startIndex + 1)
mainThreadExecutor.execute {
- receiver.onPageResult(PageResult.PREPEND, PageResult(data, 0, 0, 0))
+ if (data != null) {
+ receiver.onPageResult(PageResult.PREPEND, PageResult(data, 0, 0, 0))
+ } else {
+ receiver.onPageError(PageResult.PREPEND, Exception(), true)
+ }
}
}
@@ -105,7 +124,13 @@
return 0
}
- private fun getClampedRange(startInc: Int, endExc: Int): List<Item> {
+ private fun getClampedRange(startInc: Int, endExc: Int): List<Item>? {
+ val matching = errorIndices.filter { it in startInc..(endExc - 1) }
+ if (matching.isNotEmpty()) {
+ // found indices with errors enqueued - fail to load them
+ errorIndices.removeAll(matching)
+ return null
+ }
return listData.subList(Math.max(0, startInc), Math.min(listData.size, endExc))
}
@@ -118,10 +143,37 @@
DataSource<Int, ToValue> {
throw UnsupportedOperationException()
}
+
+ fun enqueueErrorForIndex(index: Int) {
+ errorIndices.add(index)
+ }
+
+ val errorIndices = mutableListOf<Int>()
+ }
+
+ private fun DataSource<*, Item>.enqueueErrorForIndex(index: Int) {
+ (this as TestSource).enqueueErrorForIndex(index)
+ }
+
+ private fun <E> MutableList<E>.getAllAndClear(): List<E> {
+ val data = this.toList()
+ this.clear()
+ return data
+ }
+
+ private fun <E> PagedList<E>.addLoadStateCapture(desiredType: PagedList.LoadType):
+ MutableList<PagedList.LoadState> {
+ val list = mutableListOf<PagedList.LoadState>()
+ this.addWeakLoadStateListener { type, state, _ ->
+ if (type == desiredType) {
+ list.add(state)
+ }
+ }
+ return list
}
private fun verifyRange(start: Int, count: Int, actual: PagedStorage<Item>) {
- if (mCounted) {
+ if (placeholdersEnabled) {
// assert nulls + content
val expected = arrayOfNulls<Item>(ITEMS.size)
System.arraycopy(ITEMS.toTypedArray(), start, expected, start, count)
@@ -160,15 +212,18 @@
maxSize: Int = PagedList.Config.MAX_SIZE_UNBOUNDED
): ContiguousPagedList<Int, Item> {
return ContiguousPagedList(
- TestSource(listData), mMainThread, mBackgroundThread, boundaryCallback,
- PagedList.Config.Builder()
- .setPageSize(pageSize)
- .setInitialLoadSizeHint(initLoadSize)
- .setPrefetchDistance(prefetchDistance)
- .setMaxSize(maxSize)
- .build(),
- initialPosition,
- lastLoad)
+ TestSource(listData),
+ mMainThread,
+ mBackgroundThread,
+ boundaryCallback,
+ PagedList.Config.Builder()
+ .setPageSize(pageSize)
+ .setInitialLoadSizeHint(initLoadSize)
+ .setPrefetchDistance(prefetchDistance)
+ .setMaxSize(maxSize)
+ .build(),
+ initialPosition,
+ lastLoad)
}
@Test
@@ -204,7 +259,7 @@
countedPosition: Int,
uncountedPosition: Int
) {
- if (mCounted) {
+ if (placeholdersEnabled) {
verify(callback).onChanged(countedPosition, 20)
} else {
verify(callback).onInserted(uncountedPosition, 20)
@@ -220,7 +275,7 @@
countedPosition: Int,
uncountedPosition: Int
) {
- if (mCounted) {
+ if (placeholdersEnabled) {
verify(callback).onChanged(countedPosition, 20)
} else {
verify(callback).onRemoved(uncountedPosition, 20)
@@ -255,7 +310,7 @@
verifyRange(60, 40, pagedList)
verifyZeroInteractions(callback)
- pagedList.loadAround(if (mCounted) 65 else 5)
+ pagedList.loadAround(if (placeholdersEnabled) 65 else 5)
drain()
verifyRange(40, 60, pagedList)
@@ -271,14 +326,14 @@
verifyRange(30, 40, pagedList)
verifyZeroInteractions(callback)
- pagedList.loadAround(if (mCounted) 65 else 35)
+ pagedList.loadAround(if (placeholdersEnabled) 65 else 35)
drain()
verifyRange(30, 60, pagedList)
verifyCallback(callback, 70, 40)
verifyNoMoreInteractions(callback)
- pagedList.loadAround(if (mCounted) 35 else 5)
+ pagedList.loadAround(if (placeholdersEnabled) 35 else 5)
drain()
verifyRange(10, 80, pagedList)
@@ -312,12 +367,12 @@
verifyRange(40, 20, pagedList)
// access adjacent to front, shouldn't trigger prefetch
- pagedList.loadAround(if (mCounted) 41 else 1)
+ pagedList.loadAround(if (placeholdersEnabled) 41 else 1)
drain()
verifyRange(40, 20, pagedList)
// access front item, should trigger prefetch
- pagedList.loadAround(if (mCounted) 40 else 0)
+ pagedList.loadAround(if (placeholdersEnabled) 40 else 0)
drain()
verifyRange(20, 40, pagedList)
}
@@ -332,12 +387,12 @@
verifyRange(40, 20, pagedList)
// access adjacent from end, shouldn't trigger prefetch
- pagedList.loadAround(if (mCounted) 58 else 18)
+ pagedList.loadAround(if (placeholdersEnabled) 58 else 18)
drain()
verifyRange(40, 20, pagedList)
// access end item, should trigger prefetch
- pagedList.loadAround(if (mCounted) 59 else 19)
+ pagedList.loadAround(if (placeholdersEnabled) 59 else 19)
drain()
verifyRange(40, 40, pagedList)
}
@@ -392,7 +447,7 @@
verifyZeroInteractions(callback)
// load 4th page
- pagedList.loadAround(if (mCounted) 80 else 0)
+ pagedList.loadAround(if (placeholdersEnabled) 80 else 0)
drain()
verifyRange(60, 40, pagedList)
verifyCallback(callback, 60, 0)
@@ -400,7 +455,7 @@
reset(callback)
// load 3rd page
- pagedList.loadAround(if (mCounted) 60 else 0)
+ pagedList.loadAround(if (placeholdersEnabled) 60 else 0)
drain()
verifyRange(40, 60, pagedList)
verifyCallback(callback, 40, 0)
@@ -408,7 +463,7 @@
reset(callback)
// load 2nd page, drop 5th
- pagedList.loadAround(if (mCounted) 40 else 0)
+ pagedList.loadAround(if (placeholdersEnabled) 40 else 0)
drain()
verifyRange(20, 60, pagedList)
verifyCallback(callback, 20, 0)
@@ -427,27 +482,28 @@
maxSize = 3)
// load 3 pages - 2nd, 3rd, 4th
- pagedList.loadAround(if (mCounted) 2 else 0)
+ pagedList.loadAround(if (placeholdersEnabled) 2 else 0)
drain()
+ verifyRange(1, 3, pagedList)
val callback = mock(PagedList.Callback::class.java)
pagedList.addWeakCallback(null, callback)
// start a load at the beginning...
- pagedList.loadAround(if (mCounted) 1 else 0)
+ pagedList.loadAround(if (placeholdersEnabled) 1 else 0)
mBackgroundThread.executeAll()
// but before page received, access near end of list
- pagedList.loadAround(if (mCounted) 3 else 2)
+ pagedList.loadAround(if (placeholdersEnabled) 3 else 2)
verifyZeroInteractions(callback)
mMainThread.executeAll()
- // and the load at the end is dropped without signaling callback
+ // and the load at the beginning is dropped without signaling callback
verifyNoMoreInteractions(callback)
verifyRange(1, 3, pagedList)
drain()
- if (mCounted) {
+ if (placeholdersEnabled) {
verify(callback).onChanged(4, 1)
verify(callback).onChanged(1, 1)
} else {
@@ -468,19 +524,19 @@
maxSize = 3)
// load 3 pages - 2nd, 3rd, 4th
- pagedList.loadAround(if (mCounted) 2 else 0)
+ pagedList.loadAround(if (placeholdersEnabled) 2 else 0)
drain()
val callback = mock(PagedList.Callback::class.java)
pagedList.addWeakCallback(null, callback)
// start a load at the end...
- pagedList.loadAround(if (mCounted) 3 else 2)
+ pagedList.loadAround(if (placeholdersEnabled) 3 else 2)
mBackgroundThread.executeAll()
// but before page received, access near front of list
- pagedList.loadAround(if (mCounted) 1 else 0)
+ pagedList.loadAround(if (placeholdersEnabled) 1 else 0)
verifyZeroInteractions(callback)
mMainThread.executeAll()
// and the load at the end is dropped without signaling callback
@@ -488,7 +544,7 @@
verifyRange(1, 3, pagedList)
drain()
- if (mCounted) {
+ if (placeholdersEnabled) {
verify(callback).onChanged(0, 1)
verify(callback).onChanged(3, 1)
} else {
@@ -499,6 +555,130 @@
}
@Test
+ fun loadingListenerAppend() {
+ val pagedList = createCountedPagedList(0)
+ val states = pagedList.addLoadStateCapture(PagedList.LoadType.END)
+
+ // No loading going on currently
+ assertEquals(listOf(IDLE), states.getAllAndClear())
+ verifyRange(0, 40, pagedList)
+
+ // trigger load
+ pagedList.loadAround(35)
+ mMainThread.executeAll()
+ assertEquals(listOf(LOADING), states.getAllAndClear())
+ verifyRange(0, 40, pagedList)
+
+ // load finishes
+ drain()
+ assertEquals(listOf(IDLE), states.getAllAndClear())
+ verifyRange(0, 60, pagedList)
+
+ pagedList.dataSource.enqueueErrorForIndex(65)
+
+ // trigger load which will error
+ pagedList.loadAround(55)
+ mMainThread.executeAll()
+ assertEquals(listOf(LOADING), states.getAllAndClear())
+ verifyRange(0, 60, pagedList)
+
+ // load now in error state
+ drain()
+ assertEquals(listOf(RETRYABLE_ERROR), states.getAllAndClear())
+ verifyRange(0, 60, pagedList)
+
+ // retry
+ pagedList.retry()
+ mMainThread.executeAll()
+ assertEquals(listOf(LOADING), states.getAllAndClear())
+
+ // load finishes
+ drain()
+ assertEquals(listOf(IDLE), states.getAllAndClear())
+ verifyRange(0, 80, pagedList)
+ }
+
+ @Test
+ fun pageDropCancelPrependError() {
+ // verify a prepend in error state can be dropped
+ val pagedList = createCountedPagedList(
+ initialPosition = 2,
+ pageSize = 1,
+ initLoadSize = 1,
+ prefetchDistance = 1,
+ maxSize = 3)
+ val states = pagedList.addLoadStateCapture(PagedList.LoadType.START)
+
+ // load 3 pages - 2nd, 3rd, 4th
+ pagedList.loadAround(if (placeholdersEnabled) 2 else 0)
+ drain()
+ verifyRange(1, 3, pagedList)
+ assertEquals(listOf(IDLE, LOADING, IDLE), states.getAllAndClear())
+
+ // start a load at the beginning, which will fail
+ pagedList.dataSource.enqueueErrorForIndex(0)
+ pagedList.loadAround(if (placeholdersEnabled) 1 else 0)
+ drain()
+ verifyRange(1, 3, pagedList)
+ assertEquals(listOf(LOADING, RETRYABLE_ERROR), states.getAllAndClear())
+
+ // but without that failure being retried, access near end of list, which drops the error
+ pagedList.loadAround(if (placeholdersEnabled) 3 else 2)
+ drain()
+ assertEquals(listOf(IDLE), states.getAllAndClear())
+ verifyRange(2, 3, pagedList)
+ }
+
+ @Test
+ fun pageDropCancelAppendError() {
+ // verify an append in error state can be dropped
+ val pagedList = createCountedPagedList(
+ initialPosition = 2,
+ pageSize = 1,
+ initLoadSize = 1,
+ prefetchDistance = 1,
+ maxSize = 3)
+ val states = pagedList.addLoadStateCapture(PagedList.LoadType.END)
+
+ // load 3 pages - 2nd, 3rd, 4th
+ pagedList.loadAround(if (placeholdersEnabled) 2 else 0)
+ drain()
+ verifyRange(1, 3, pagedList)
+ assertEquals(listOf(IDLE, LOADING, IDLE), states.getAllAndClear())
+
+ // start a load at the end, which will fail
+ pagedList.dataSource.enqueueErrorForIndex(4)
+ pagedList.loadAround(if (placeholdersEnabled) 3 else 2)
+ drain()
+ verifyRange(1, 3, pagedList)
+ assertEquals(listOf(LOADING, RETRYABLE_ERROR), states.getAllAndClear())
+
+ // but without that failure being retried, access near start of list, which drops the error
+ pagedList.loadAround(if (placeholdersEnabled) 1 else 0)
+ drain()
+ assertEquals(listOf(IDLE), states.getAllAndClear())
+ verifyRange(0, 3, pagedList)
+ }
+
+ @Test
+ fun errorIntoDrop() {
+ // have an error, move loading range, error goes away
+ val pagedList = createCountedPagedList(0)
+ val states = mutableListOf<PagedList.LoadState>()
+ pagedList.addWeakLoadStateListener { type, state, _ ->
+ if (type == PagedList.LoadType.END) {
+ states.add(state)
+ }
+ }
+
+ pagedList.dataSource.enqueueErrorForIndex(45)
+ pagedList.loadAround(35)
+ drain()
+ assertEquals(listOf(IDLE, LOADING, RETRYABLE_ERROR), states.getAllAndClear())
+ verifyRange(0, 40, pagedList)
+ }
+
+ @Test
fun distantPrefetch() {
val pagedList = createCountedPagedList(0,
initLoadSize = 10, pageSize = 10, prefetchDistance = 30)
@@ -550,7 +730,7 @@
val pagedList = createCountedPagedList(80)
verifyRange(60, 40, pagedList)
- pagedList.loadAround(if (mCounted) 65 else 5)
+ pagedList.loadAround(if (placeholdersEnabled) 65 else 5)
drain()
verifyRange(40, 60, pagedList)
@@ -558,7 +738,7 @@
val snapshot = pagedList.snapshot() as PagedList<Item>
verifyRange(40, 60, snapshot)
- pagedList.loadAround(if (mCounted) 45 else 5)
+ pagedList.loadAround(if (placeholdersEnabled) 45 else 5)
drain()
verifyRange(20, 80, pagedList)
verifyRange(40, 60, snapshot)
@@ -707,24 +887,24 @@
verifyZeroInteractions(boundaryCallback)
// loading around last item causes onItemAtEndLoaded
- pagedList.loadAround(if (mCounted) 99 else 19)
+ pagedList.loadAround(if (placeholdersEnabled) 99 else 19)
drain()
verifyRange(80, 20, pagedList)
verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
verifyNoMoreInteractions(boundaryCallback)
// prepending doesn't trigger callback...
- pagedList.loadAround(if (mCounted) 80 else 0)
+ pagedList.loadAround(if (placeholdersEnabled) 80 else 0)
drain()
verifyRange(60, 40, pagedList)
verifyZeroInteractions(boundaryCallback)
// ...load rest of data, still no dispatch...
- pagedList.loadAround(if (mCounted) 60 else 0)
+ pagedList.loadAround(if (placeholdersEnabled) 60 else 0)
drain()
- pagedList.loadAround(if (mCounted) 40 else 0)
+ pagedList.loadAround(if (placeholdersEnabled) 40 else 0)
drain()
- pagedList.loadAround(if (mCounted) 20 else 0)
+ pagedList.loadAround(if (placeholdersEnabled) 20 else 0)
drain()
verifyRange(0, 100, pagedList)
verifyZeroInteractions(boundaryCallback)
diff --git a/paging/common/src/test/java/androidx/paging/ItemKeyedDataSourceTest.kt b/paging/common/src/test/java/androidx/paging/ItemKeyedDataSourceTest.kt
index bd03782..3f6efdbf 100644
--- a/paging/common/src/test/java/androidx/paging/ItemKeyedDataSourceTest.kt
+++ b/paging/common/src/test/java/androidx/paging/ItemKeyedDataSourceTest.kt
@@ -34,8 +34,12 @@
// ----- STANDARD -----
- private fun loadInitial(dataSource: ItemDataSource, key: Key?, initialLoadSize: Int,
- enablePlaceholders: Boolean): PageResult<Item> {
+ private fun loadInitial(
+ dataSource: ItemDataSource,
+ key: Key?,
+ initialLoadSize: Int,
+ enablePlaceholders: Boolean
+ ): PageResult<Item> {
@Suppress("UNCHECKED_CAST")
val receiver = mock(PageResult.Receiver::class.java) as PageResult.Receiver<Item>
@Suppress("UNCHECKED_CAST")
@@ -204,15 +208,28 @@
internal data class Key(val name: String, val id: Int)
internal data class Item(
- val name: String, val id: Int, val balance: Double, val address: String)
+ val name: String,
+ val id: Int,
+ val balance: Double,
+ val address: String
+ )
- internal class ItemDataSource(private val counted: Boolean = true,
- private val items: List<Item> = ITEMS_BY_NAME_ID)
- : ItemKeyedDataSource<Key, Item>() {
+ internal class ItemDataSource(
+ private val counted: Boolean = true,
+ private val items: List<Item> = ITEMS_BY_NAME_ID
+ ) : ItemKeyedDataSource<Key, Item>() {
+ private var error = false
override fun loadInitial(
- params: LoadInitialParams<Key>,
- callback: LoadInitialCallback<Item>) {
+ params: LoadInitialParams<Key>,
+ callback: LoadInitialCallback<Item>
+ ) {
+ if (error) {
+ callback.onError(EXCEPTION)
+ error = false
+ return
+ }
+
val key = params.requestedInitialKey ?: Key("", Integer.MAX_VALUE)
val start = Math.max(0, findFirstIndexAfter(key) - params.requestedLoadSize / 2)
val endExclusive = Math.min(start + params.requestedLoadSize, items.size)
@@ -225,6 +242,12 @@
}
override fun loadAfter(params: LoadParams<Key>, callback: LoadCallback<Item>) {
+ if (error) {
+ callback.onError(EXCEPTION)
+ error = false
+ return
+ }
+
val start = findFirstIndexAfter(params.key)
val endExclusive = Math.min(start + params.requestedLoadSize, items.size)
@@ -232,6 +255,12 @@
}
override fun loadBefore(params: LoadParams<Key>, callback: LoadCallback<Item>) {
+ if (error) {
+ callback.onError(EXCEPTION)
+ error = false
+ return
+ }
+
val firstIndexBefore = findFirstIndexBefore(params.key)
val endExclusive = Math.max(0, firstIndexBefore + 1)
val start = Math.max(0, firstIndexBefore - params.requestedLoadSize + 1)
@@ -254,19 +283,25 @@
KEY_COMPARATOR.compare(key, getKey(items[it])) > 0
} ?: -1
}
+
+ fun enqueueError() {
+ error = true
+ }
}
private fun performLoadInitial(
- invalidateDataSource: Boolean = false,
- callbackInvoker: (callback: ItemKeyedDataSource.LoadInitialCallback<String>) -> Unit) {
+ invalidateDataSource: Boolean = false,
+ callbackInvoker: (callback: ItemKeyedDataSource.LoadInitialCallback<String>) -> Unit
+ ) {
val dataSource = object : ItemKeyedDataSource<String, String>() {
override fun getKey(item: String): String {
return ""
}
override fun loadInitial(
- params: LoadInitialParams<String>,
- callback: LoadInitialCallback<String>) {
+ params: LoadInitialParams<String>,
+ callback: LoadInitialCallback<String>
+ ) {
if (invalidateDataSource) {
// invalidate data source so it's invalid when onResult() called
invalidate()
@@ -301,7 +336,7 @@
@Test
fun loadInitialCallbackNotPageSizeMultiple() = performLoadInitial {
// Keyed LoadInitialCallback *can* accept result that's not a multiple of page size
- val elevenLetterList = List(11) { "" + 'a' + it }
+ val elevenLetterList = List(11) { index -> "" + ('a' + index) }
it.onResult(elevenLetterList, 0, 12)
}
@@ -362,6 +397,14 @@
override fun onResult(data: MutableList<A>) {
callback.onResult(convert(data))
}
+
+ override fun onError(error: Throwable) {
+ callback.onError(error)
+ }
+
+ override fun onRetryableError(error: Throwable) {
+ callback.onRetryableError(error)
+ }
})
}
@@ -370,6 +413,14 @@
override fun onResult(data: MutableList<A>) {
callback.onResult(convert(data))
}
+
+ override fun onError(error: Throwable) {
+ callback.onError(error)
+ }
+
+ override fun onRetryableError(error: Throwable) {
+ callback.onRetryableError(error)
+ }
})
}
@@ -378,6 +429,14 @@
override fun onResult(data: MutableList<A>) {
callback.onResult(convert(data))
}
+
+ override fun onError(error: Throwable) {
+ callback.onError(error)
+ }
+
+ override fun onRetryableError(error: Throwable) {
+ callback.onRetryableError(error)
+ }
})
}
@@ -397,36 +456,53 @@
}
}
- private fun verifyWrappedDataSource(createWrapper:
- (ItemKeyedDataSource<Key, Item>) -> ItemKeyedDataSource<Key, DecoratedItem>) {
+ private fun verifyWrappedDataSource(
+ createWrapper: (ItemKeyedDataSource<Key, Item>) -> ItemKeyedDataSource<Key, DecoratedItem>
+ ) {
// verify that it's possible to wrap an ItemKeyedDataSource, and add info to its data
val orig = ItemDataSource(items = ITEMS_BY_NAME_ID)
val wrapper = createWrapper(orig)
- // load initial
+ // load initial - success
@Suppress("UNCHECKED_CAST")
val loadInitialCallback = mock(ItemKeyedDataSource.LoadInitialCallback::class.java)
as ItemKeyedDataSource.LoadInitialCallback<DecoratedItem>
val initKey = orig.getKey(ITEMS_BY_NAME_ID.first())
- wrapper.loadInitial(ItemKeyedDataSource.LoadInitialParams(initKey, 10, false),
+ val initParams = ItemKeyedDataSource.LoadInitialParams(initKey, 10, false)
+ wrapper.loadInitial(initParams,
loadInitialCallback)
verify(loadInitialCallback).onResult(
ITEMS_BY_NAME_ID.subList(0, 10).map { DecoratedItem(it) })
+ // error
+ orig.enqueueError()
+ wrapper.loadInitial(initParams, loadInitialCallback)
+ verify(loadInitialCallback).onError(EXCEPTION)
verifyNoMoreInteractions(loadInitialCallback)
- @Suppress("UNCHECKED_CAST")
- val loadCallback = mock(ItemKeyedDataSource.LoadCallback::class.java)
- as ItemKeyedDataSource.LoadCallback<DecoratedItem>
val key = orig.getKey(ITEMS_BY_NAME_ID[20])
+ @Suppress("UNCHECKED_CAST")
+ var loadCallback = mock(ItemKeyedDataSource.LoadCallback::class.java)
+ as ItemKeyedDataSource.LoadCallback<DecoratedItem>
// load after
wrapper.loadAfter(ItemKeyedDataSource.LoadParams(key, 10), loadCallback)
verify(loadCallback).onResult(ITEMS_BY_NAME_ID.subList(21, 31).map { DecoratedItem(it) })
+ // load after - error
+ orig.enqueueError()
+ wrapper.loadAfter(ItemKeyedDataSource.LoadParams(key, 10), loadCallback)
+ verify(loadCallback).onError(EXCEPTION)
verifyNoMoreInteractions(loadCallback)
// load before
+ @Suppress("UNCHECKED_CAST")
+ loadCallback = mock(ItemKeyedDataSource.LoadCallback::class.java)
+ as ItemKeyedDataSource.LoadCallback<DecoratedItem>
wrapper.loadBefore(ItemKeyedDataSource.LoadParams(key, 10), loadCallback)
verify(loadCallback).onResult(ITEMS_BY_NAME_ID.subList(10, 20).map { DecoratedItem(it) })
+ // load before - error
+ orig.enqueueError()
+ wrapper.loadBefore(ItemKeyedDataSource.LoadParams(key, 10), loadCallback)
+ verify(loadCallback).onError(EXCEPTION)
verifyNoMoreInteractions(loadCallback)
// verify invalidation
@@ -440,13 +516,13 @@
}
@Test
- fun testListConverterWrappedDataSource() = verifyWrappedDataSource {
- it.mapByPage { it.map { DecoratedItem(it) } }
+ fun testListConverterWrappedDataSource() = verifyWrappedDataSource { dataSource ->
+ dataSource.mapByPage { page -> page.map { DecoratedItem(it) } }
}
@Test
- fun testItemConverterWrappedDataSource() = verifyWrappedDataSource {
- it.map { DecoratedItem(it) }
+ fun testItemConverterWrappedDataSource() = verifyWrappedDataSource { dataSource ->
+ dataSource.map { DecoratedItem(it) }
}
@Test
@@ -468,15 +544,17 @@
}
companion object {
- private val ITEM_COMPARATOR = compareBy<Item>({ it.name }).thenByDescending({ it.id })
- private val KEY_COMPARATOR = compareBy<Key>({ it.name }).thenByDescending({ it.id })
+ private val ITEM_COMPARATOR = compareBy<Item> { it.name }.thenByDescending { it.id }
+ private val KEY_COMPARATOR = compareBy<Key> { it.name }.thenByDescending { it.id }
private val ITEMS_BY_NAME_ID = List(100) {
- val names = Array(10) { "f" + ('a' + it) }
+ val names = Array(10) { index -> "f" + ('a' + index) }
Item(names[it % 10],
it,
Math.random() * 1000,
(Math.random() * 200).toInt().toString() + " fake st.")
}.sortedWith(ITEM_COMPARATOR)
+
+ private val EXCEPTION = Exception()
}
}
diff --git a/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt b/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt
index e6f0caa5..b7cafe7 100644
--- a/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt
+++ b/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt
@@ -39,6 +39,7 @@
internal class ItemDataSource(val data: Map<String, Page> = PAGE_MAP)
: PageKeyedDataSource<String, Item>() {
+ private var error = false
private fun getPage(key: String): Page = data[key]!!
@@ -46,19 +47,41 @@
params: LoadInitialParams<String>,
callback: LoadInitialCallback<String, Item>
) {
+ if (error) {
+ callback.onRetryableError(EXCEPTION)
+ error = false
+ return
+ }
+
val page = getPage(INIT_KEY)
callback.onResult(page.data, page.prev, page.next)
}
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<String, Item>) {
+ if (error) {
+ callback.onRetryableError(EXCEPTION)
+ error = false
+ return
+ }
+
val page = getPage(params.key)
callback.onResult(page.data, page.prev)
}
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String, Item>) {
+ if (error) {
+ callback.onRetryableError(EXCEPTION)
+ error = false
+ return
+ }
+
val page = getPage(params.key)
callback.onResult(page.data, page.next)
}
+
+ fun enqueueError() {
+ error = true
+ }
}
@Test
@@ -133,7 +156,7 @@
@Test
fun loadInitialCallbackNotPageSizeMultiple() = performLoadInitial {
// Keyed LoadInitialCallback *can* accept result that's not a multiple of page size
- val elevenLetterList = List(11) { "" + 'a' + it }
+ val elevenLetterList = List(11) { index -> "" + ('a' + index) }
it.onResult(elevenLetterList, 0, 12, null, null)
}
@@ -172,6 +195,7 @@
assertFalse(ItemDataSource().supportsPageDropping())
}
+ @Test
fun testBoundaryCallback() {
val dataSource = object : PageKeyedDataSource<String, String>() {
override fun loadInitial(
@@ -313,6 +337,14 @@
override fun onResult(data: MutableList<A>, previousPageKey: K?, nextPageKey: K?) {
callback.onResult(convert(data), previousPageKey, nextPageKey)
}
+
+ override fun onError(error: Throwable) {
+ callback.onError(error)
+ }
+
+ override fun onRetryableError(error: Throwable) {
+ callback.onRetryableError(error)
+ }
})
}
@@ -321,6 +353,14 @@
override fun onResult(data: List<A>, adjacentPageKey: K?) {
callback.onResult(convert(data), adjacentPageKey)
}
+
+ override fun onError(error: Throwable) {
+ callback.onError(error)
+ }
+
+ override fun onRetryableError(error: Throwable) {
+ callback.onRetryableError(error)
+ }
})
}
@@ -329,6 +369,14 @@
override fun onResult(data: List<A>, adjacentPageKey: K?) {
callback.onResult(convert(data), adjacentPageKey)
}
+
+ override fun onError(error: Throwable) {
+ callback.onError(error)
+ }
+
+ override fun onRetryableError(error: Throwable) {
+ callback.onRetryableError(error)
+ }
})
}
@@ -354,28 +402,39 @@
val loadInitialCallback = mock(PageKeyedDataSource.LoadInitialCallback::class.java)
as PageKeyedDataSource.LoadInitialCallback<String, String>
- wrapper.loadInitial(PageKeyedDataSource.LoadInitialParams<String>(4, true),
- loadInitialCallback)
- val expectedInitial = PAGE_MAP.get(INIT_KEY)!!
+ val initParams = PageKeyedDataSource.LoadInitialParams<String>(4, true)
+ wrapper.loadInitial(initParams, loadInitialCallback)
+ val expectedInitial = PAGE_MAP[INIT_KEY]!!
verify(loadInitialCallback).onResult(expectedInitial.data.map { it.toString() },
expectedInitial.prev, expectedInitial.next)
verifyNoMoreInteractions(loadInitialCallback)
@Suppress("UNCHECKED_CAST")
- val loadCallback = mock(PageKeyedDataSource.LoadCallback::class.java)
- as PageKeyedDataSource.LoadCallback<String, String>
// load after
+ var loadCallback = mock(PageKeyedDataSource.LoadCallback::class.java)
+ as PageKeyedDataSource.LoadCallback<String, String>
wrapper.loadAfter(PageKeyedDataSource.LoadParams(expectedInitial.next!!, 4), loadCallback)
- val expectedAfter = PAGE_MAP.get(expectedInitial.next)!!
- verify(loadCallback).onResult(expectedAfter.data.map { it.toString() },
- expectedAfter.next)
+ val expectedAfter = PAGE_MAP[expectedInitial.next]!!
+ verify(loadCallback).onResult(expectedAfter.data.map { it.toString() }, expectedAfter.next)
+ // load after - error
+ orig.enqueueError()
+ wrapper.loadAfter(PageKeyedDataSource.LoadParams(expectedInitial.next, 4), loadCallback)
+ verify(loadCallback).onRetryableError(EXCEPTION)
verifyNoMoreInteractions(loadCallback)
// load before
+ @Suppress("UNCHECKED_CAST")
+ loadCallback = mock(PageKeyedDataSource.LoadCallback::class.java)
+ as PageKeyedDataSource.LoadCallback<String, String>
wrapper.loadBefore(PageKeyedDataSource.LoadParams(expectedAfter.prev!!, 4), loadCallback)
verify(loadCallback).onResult(expectedInitial.data.map { it.toString() },
expectedInitial.prev)
verifyNoMoreInteractions(loadCallback)
+ // load before - error
+ orig.enqueueError()
+ wrapper.loadBefore(PageKeyedDataSource.LoadParams(expectedAfter.prev, 4), loadCallback)
+ verify(loadCallback).onRetryableError(EXCEPTION)
+ verifyNoMoreInteractions(loadCallback)
// verify invalidation
orig.invalidate()
@@ -388,13 +447,13 @@
}
@Test
- fun testListConverterWrappedDataSource() = verifyWrappedDataSource {
- it.mapByPage { it.map { it.toString() } }
+ fun testListConverterWrappedDataSource() = verifyWrappedDataSource { dataSource ->
+ dataSource.mapByPage { page -> page.map { it.toString() } }
}
@Test
- fun testItemConverterWrappedDataSource() = verifyWrappedDataSource {
- it.map { it.toString() }
+ fun testItemConverterWrappedDataSource() = verifyWrappedDataSource { dataSource ->
+ dataSource.map { it.toString() }
}
@Test
@@ -420,6 +479,7 @@
private val INIT_KEY: String = "key 2"
private val PAGE_MAP: Map<String, Page>
private val ITEM_LIST: List<Item>
+ private val EXCEPTION = Exception()
init {
val map = HashMap<String, Page>()
@@ -432,7 +492,7 @@
val key = "key $i"
val prev = if (i > 1) ("key " + (i - 1)) else null
val next = if (i < pageCount) ("key " + (i + 1)) else null
- map.put(key, Page(prev, data, next))
+ map[key] = Page(prev, data, next)
}
PAGE_MAP = map
ITEM_LIST = list
diff --git a/paging/common/src/test/java/androidx/paging/PositionalDataSourceTest.kt b/paging/common/src/test/java/androidx/paging/PositionalDataSourceTest.kt
index f18b41a..308d378 100644
--- a/paging/common/src/test/java/androidx/paging/PositionalDataSourceTest.kt
+++ b/paging/common/src/test/java/androidx/paging/PositionalDataSourceTest.kt
@@ -29,12 +29,14 @@
@RunWith(JUnit4::class)
class PositionalDataSourceTest {
private fun computeInitialLoadPos(
- requestedStartPosition: Int,
- requestedLoadSize: Int,
- pageSize: Int,
- totalCount: Int): Int {
+ requestedStartPosition: Int,
+ requestedLoadSize: Int,
+ pageSize: Int,
+ totalCount: Int
+ ): Int {
val params = PositionalDataSource.LoadInitialParams(
- requestedStartPosition, requestedLoadSize, pageSize, true)
+ requestedStartPosition, requestedLoadSize, pageSize, true
+ )
return PositionalDataSource.computeInitialLoadPosition(params, totalCount)
}
@@ -113,13 +115,15 @@
}
private fun performLoadInitial(
- enablePlaceholders: Boolean = true,
- invalidateDataSource: Boolean = false,
- callbackInvoker: (callback: PositionalDataSource.LoadInitialCallback<String>) -> Unit) {
+ enablePlaceholders: Boolean = true,
+ invalidateDataSource: Boolean = false,
+ callbackInvoker: (callback: PositionalDataSource.LoadInitialCallback<String>) -> Unit
+ ) {
val dataSource = object : PositionalDataSource<String>() {
override fun loadInitial(
- params: LoadInitialParams,
- callback: LoadInitialCallback<String>) {
+ params: LoadInitialParams,
+ callback: LoadInitialCallback<String>
+ ) {
if (invalidateDataSource) {
// invalidate data source so it's invalid when onResult() called
invalidate()
@@ -154,7 +158,7 @@
@Test(expected = IllegalArgumentException::class)
fun initialLoadCallbackNotPageSizeMultiple() = performLoadInitial {
// Positional LoadInitialCallback can't accept result that's not a multiple of page size
- val elevenLetterList = List(11) { "" + 'a' + it }
+ val elevenLetterList = List(11) { index -> "" + ('a' + index) }
it.onResult(elevenLetterList, 0, 12)
}
@@ -245,6 +249,14 @@
override fun onResult(data: List<A>, position: Int) {
callback.onResult(convert(data), position)
}
+
+ override fun onError(error: Throwable) {
+ callback.onError(error)
+ }
+
+ override fun onRetryableError(error: Throwable) {
+ callback.onRetryableError(error)
+ }
})
}
@@ -253,6 +265,14 @@
override fun onResult(data: List<A>) {
callback.onResult(convert(data))
}
+
+ override fun onError(error: Throwable) {
+ callback.onError(error)
+ }
+
+ override fun onRetryableError(error: Throwable) {
+ callback.onRetryableError(error)
+ }
})
}
@@ -266,8 +286,54 @@
}
}
+ class ListDataSource<T>(val list: List<T>) : PositionalDataSource<T>() {
+ private var error = false
+
+ override fun loadInitial(
+ params: PositionalDataSource.LoadInitialParams,
+ callback: PositionalDataSource.LoadInitialCallback<T>
+ ) {
+ if (error) {
+ callback.onError(ERROR)
+ error = false
+ return
+ }
+ val totalCount = list.size
+
+ val position = PositionalDataSource.computeInitialLoadPosition(params, totalCount)
+ val loadSize = PositionalDataSource.computeInitialLoadSize(params, position, totalCount)
+
+ // for simplicity, we could return everything immediately,
+ // but we tile here since it's expected behavior
+ val sublist = list.subList(position, position + loadSize)
+ callback.onResult(sublist, position, totalCount)
+ }
+
+ override fun loadRange(
+ params: PositionalDataSource.LoadRangeParams,
+ callback: PositionalDataSource.LoadRangeCallback<T>
+ ) {
+ if (error) {
+ callback.onError(ERROR)
+ error = false
+ return
+ }
+ callback.onResult(
+ list.subList(
+ params.startPosition,
+ params.startPosition + params.loadSize
+ )
+ )
+ }
+
+ fun enqueueError() {
+ error = true
+ }
+ }
+
private fun verifyWrappedDataSource(
- createWrapper: (PositionalDataSource<Int>) -> PositionalDataSource<String>) {
+ createWrapper: (PositionalDataSource<Int>) -> PositionalDataSource<String>
+ ) {
val orig = ListDataSource(listOf(0, 5, 4, 8, 12))
val wrapper = createWrapper(orig)
@@ -275,19 +341,25 @@
@Suppress("UNCHECKED_CAST")
val loadInitialCallback = mock(PositionalDataSource.LoadInitialCallback::class.java)
as PositionalDataSource.LoadInitialCallback<String>
-
- wrapper.loadInitial(PositionalDataSource.LoadInitialParams(0, 2, 1, true),
- loadInitialCallback)
+ val initParams = PositionalDataSource.LoadInitialParams(0, 2, 1, true)
+ wrapper.loadInitial(initParams, loadInitialCallback)
verify(loadInitialCallback).onResult(listOf("0", "5"), 0, 5)
+ // load initial - error
+ orig.enqueueError()
+ wrapper.loadInitial(initParams, loadInitialCallback)
+ verify(loadInitialCallback).onError(ERROR)
verifyNoMoreInteractions(loadInitialCallback)
// load range
@Suppress("UNCHECKED_CAST")
val loadRangeCallback = mock(PositionalDataSource.LoadRangeCallback::class.java)
as PositionalDataSource.LoadRangeCallback<String>
-
wrapper.loadRange(PositionalDataSource.LoadRangeParams(2, 3), loadRangeCallback)
verify(loadRangeCallback).onResult(listOf("4", "8", "12"))
+ // load range - error
+ orig.enqueueError()
+ wrapper.loadRange(PositionalDataSource.LoadRangeParams(2, 3), loadRangeCallback)
+ verify(loadRangeCallback).onError(ERROR)
verifyNoMoreInteractions(loadRangeCallback)
// check invalidation behavior
@@ -308,13 +380,13 @@
}
@Test
- fun testListConverterWrappedDataSource() = verifyWrappedDataSource {
- it.mapByPage { it.map { it.toString() } }
+ fun testListConverterWrappedDataSource() = verifyWrappedDataSource { dataSource ->
+ dataSource.mapByPage { page -> page.map { it.toString() } }
}
@Test
- fun testItemConverterWrappedDataSource() = verifyWrappedDataSource {
- it.map { it.toString() }
+ fun testItemConverterWrappedDataSource() = verifyWrappedDataSource { dataSource ->
+ dataSource.map { it.toString() }
}
@Test
@@ -352,4 +424,8 @@
wrapper.invalidate()
assertTrue(orig.isInvalid)
}
+
+ companion object {
+ private val ERROR = Exception()
+ }
}
diff --git a/paging/runtime/api/2.2.0-alpha01.txt b/paging/runtime/api/2.2.0-alpha01.txt
new file mode 100644
index 0000000..889c9bf
--- /dev/null
+++ b/paging/runtime/api/2.2.0-alpha01.txt
@@ -0,0 +1,47 @@
+// Signature format: 2.0
+package androidx.paging {
+
+ public class AsyncPagedListDiffer<T> {
+ ctor public AsyncPagedListDiffer(androidx.recyclerview.widget.RecyclerView.Adapter, androidx.recyclerview.widget.DiffUtil.ItemCallback<T>);
+ ctor public AsyncPagedListDiffer(androidx.recyclerview.widget.ListUpdateCallback, androidx.recyclerview.widget.AsyncDifferConfig<T>);
+ method public void addLoadStateListener(androidx.paging.PagedList.LoadStateListener);
+ method public void addPagedListListener(androidx.paging.AsyncPagedListDiffer.PagedListListener<T>);
+ method public androidx.paging.PagedList<T>? getCurrentList();
+ method public T? getItem(int);
+ method public int getItemCount();
+ method public void removeLoadStateListListener(androidx.paging.PagedList.LoadStateListener);
+ method public void removePagedListListener(androidx.paging.AsyncPagedListDiffer.PagedListListener<T>);
+ method public void submitList(androidx.paging.PagedList<T>?);
+ method public void submitList(androidx.paging.PagedList<T>?, Runnable?);
+ }
+
+ public static interface AsyncPagedListDiffer.PagedListListener<T> {
+ method public void onCurrentListChanged(androidx.paging.PagedList<T>?, androidx.paging.PagedList<T>?);
+ }
+
+ public final class LivePagedListBuilder<Key, Value> {
+ ctor public LivePagedListBuilder(androidx.paging.DataSource.Factory<Key,Value>, androidx.paging.PagedList.Config);
+ ctor public LivePagedListBuilder(androidx.paging.DataSource.Factory<Key,Value>, int);
+ method public androidx.lifecycle.LiveData<androidx.paging.PagedList<Value>> build();
+ method public androidx.paging.LivePagedListBuilder<Key,Value> setBoundaryCallback(androidx.paging.PagedList.BoundaryCallback<Value>?);
+ method public androidx.paging.LivePagedListBuilder<Key,Value> setFetchExecutor(java.util.concurrent.Executor);
+ method public androidx.paging.LivePagedListBuilder<Key,Value> setInitialLoadKey(Key?);
+ }
+
+ public abstract class PagedListAdapter<T, VH extends androidx.recyclerview.widget.RecyclerView.ViewHolder> extends androidx.recyclerview.widget.RecyclerView.Adapter<VH> {
+ ctor protected PagedListAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>);
+ ctor protected PagedListAdapter(androidx.recyclerview.widget.AsyncDifferConfig<T>);
+ method public void addLoadStateListener(androidx.paging.PagedList.LoadStateListener!);
+ method public androidx.paging.PagedList<T>? getCurrentList();
+ method protected T? getItem(int);
+ method public int getItemCount();
+ method @Deprecated public void onCurrentListChanged(androidx.paging.PagedList<T>?);
+ method public void onCurrentListChanged(androidx.paging.PagedList<T>?, androidx.paging.PagedList<T>?);
+ method public void onLoadStateChanged(androidx.paging.PagedList.LoadType, androidx.paging.PagedList.LoadState, Throwable?);
+ method public void removeLoadStateListener(androidx.paging.PagedList.LoadStateListener!);
+ method public void submitList(androidx.paging.PagedList<T>?);
+ method public void submitList(androidx.paging.PagedList<T>?, Runnable?);
+ }
+
+}
+
diff --git a/paging/runtime/api/current.txt b/paging/runtime/api/current.txt
index 0f658df..889c9bf 100644
--- a/paging/runtime/api/current.txt
+++ b/paging/runtime/api/current.txt
@@ -4,10 +4,12 @@
public class AsyncPagedListDiffer<T> {
ctor public AsyncPagedListDiffer(androidx.recyclerview.widget.RecyclerView.Adapter, androidx.recyclerview.widget.DiffUtil.ItemCallback<T>);
ctor public AsyncPagedListDiffer(androidx.recyclerview.widget.ListUpdateCallback, androidx.recyclerview.widget.AsyncDifferConfig<T>);
+ method public void addLoadStateListener(androidx.paging.PagedList.LoadStateListener);
method public void addPagedListListener(androidx.paging.AsyncPagedListDiffer.PagedListListener<T>);
method public androidx.paging.PagedList<T>? getCurrentList();
method public T? getItem(int);
method public int getItemCount();
+ method public void removeLoadStateListListener(androidx.paging.PagedList.LoadStateListener);
method public void removePagedListListener(androidx.paging.AsyncPagedListDiffer.PagedListListener<T>);
method public void submitList(androidx.paging.PagedList<T>?);
method public void submitList(androidx.paging.PagedList<T>?, Runnable?);
@@ -29,11 +31,14 @@
public abstract class PagedListAdapter<T, VH extends androidx.recyclerview.widget.RecyclerView.ViewHolder> extends androidx.recyclerview.widget.RecyclerView.Adapter<VH> {
ctor protected PagedListAdapter(androidx.recyclerview.widget.DiffUtil.ItemCallback<T>);
ctor protected PagedListAdapter(androidx.recyclerview.widget.AsyncDifferConfig<T>);
+ method public void addLoadStateListener(androidx.paging.PagedList.LoadStateListener!);
method public androidx.paging.PagedList<T>? getCurrentList();
method protected T? getItem(int);
method public int getItemCount();
method @Deprecated public void onCurrentListChanged(androidx.paging.PagedList<T>?);
method public void onCurrentListChanged(androidx.paging.PagedList<T>?, androidx.paging.PagedList<T>?);
+ method public void onLoadStateChanged(androidx.paging.PagedList.LoadType, androidx.paging.PagedList.LoadState, Throwable?);
+ method public void removeLoadStateListener(androidx.paging.PagedList.LoadStateListener!);
method public void submitList(androidx.paging.PagedList<T>?);
method public void submitList(androidx.paging.PagedList<T>?, Runnable?);
}
diff --git a/paging/runtime/ktx/api/2.2.0-alpha01.txt b/paging/runtime/ktx/api/2.2.0-alpha01.txt
new file mode 100644
index 0000000..b7750be
--- /dev/null
+++ b/paging/runtime/ktx/api/2.2.0-alpha01.txt
@@ -0,0 +1,11 @@
+// Signature format: 2.0
+package androidx.paging {
+
+ public final class LivePagedListKt {
+ ctor public LivePagedListKt();
+ method public static <Key, Value> androidx.lifecycle.LiveData<androidx.paging.PagedList<Value>> toLiveData(androidx.paging.DataSource.Factory<Key,Value>, androidx.paging.PagedList.Config config, Key? initialLoadKey = null, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, java.util.concurrent.Executor fetchExecutor = ArchTaskExecutor.getIOThreadExecutor());
+ method public static <Key, Value> androidx.lifecycle.LiveData<androidx.paging.PagedList<Value>> toLiveData(androidx.paging.DataSource.Factory<Key,Value>, int pageSize, Key? initialLoadKey = null, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, java.util.concurrent.Executor fetchExecutor = ArchTaskExecutor.getIOThreadExecutor());
+ }
+
+}
+
diff --git a/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java b/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
index 6640dc0..c456965 100644
--- a/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
+++ b/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
@@ -153,6 +153,30 @@
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mMaxScheduledGeneration;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final PagedList.LoadStateManager mLoadStateManager = new PagedList.LoadStateManager() {
+ @Override
+ protected void onStateChanged(@NonNull PagedList.LoadType type,
+ @NonNull PagedList.LoadState state, @Nullable Throwable error) {
+ // Don't need to post - PagedList will already have done that
+ for (PagedList.LoadStateListener listener : mLoadStateListeners) {
+ listener.onLoadStateChanged(type, state, error);
+ }
+ }
+ };
+ @SuppressWarnings("WeakerAccess") // synthetic access
+ PagedList.LoadStateListener mLoadStateListener = new PagedList.LoadStateListener() {
+ @Override
+ public void onLoadStateChanged(@NonNull PagedList.LoadType type,
+ @NonNull PagedList.LoadState state, @Nullable Throwable error) {
+ mLoadStateManager.onStateChanged(type, state, error);
+ }
+ };
+
+ @SuppressWarnings("WeakerAccess") // synthetic access
+ final List<PagedList.LoadStateListener> mLoadStateListeners =
+ new CopyOnWriteArrayList<>();
+
/**
* Convenience for {@code AsyncPagedListDiffer(new AdapterListUpdateCallback(adapter),
* new AsyncDifferConfig.Builder<T>(diffCallback).build();}
@@ -293,6 +317,7 @@
int removedCount = getItemCount();
if (mPagedList != null) {
mPagedList.removeWeakCallback(mPagedListCallback);
+ mPagedList.removeWeakLoadStateListener(mLoadStateListener);
mPagedList = null;
} else if (mSnapshot != null) {
mSnapshot = null;
@@ -306,6 +331,7 @@
if (mPagedList == null && mSnapshot == null) {
// fast simple first insert
mPagedList = pagedList;
+ mPagedList.addWeakLoadStateListener(mLoadStateListener);
pagedList.addWeakCallback(null, mPagedListCallback);
// dispatch update callback after updating mPagedList/mSnapshot
@@ -319,6 +345,8 @@
// 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);
+ mPagedList.removeWeakLoadStateListener(mLoadStateListener);
+
mSnapshot = (PagedList<T>) mPagedList.snapshot();
mPagedList = null;
}
@@ -364,6 +392,7 @@
PagedList<T> previousSnapshot = mSnapshot;
mPagedList = newList;
+ mPagedList.addWeakLoadStateListener(mLoadStateListener);
mSnapshot = null;
// dispatch update callback after updating mPagedList/mSnapshot
@@ -428,6 +457,44 @@
}
/**
+ * Add a LoadStateListener to observe the loading state of the current PagedList.
+ *
+ * As new PagedLists are submitted and displayed, the listener will be notified to reflect
+ * current REFRESH, START, and END states.
+ *
+ * @param listener Listener to receive updates.
+ *
+ * @see #removeLoadStateListListener(PagedList.LoadStateListener)
+ */
+ public void addLoadStateListener(@NonNull PagedList.LoadStateListener listener) {
+ if (mPagedList != null) {
+ mPagedList.addWeakLoadStateListener(listener);
+ } else {
+ listener.onLoadStateChanged(PagedList.LoadType.REFRESH, mLoadStateManager.getRefresh(),
+ mLoadStateManager.getRefreshError());
+ listener.onLoadStateChanged(PagedList.LoadType.START, mLoadStateManager.getStart(),
+ mLoadStateManager.getStartError());
+ listener.onLoadStateChanged(PagedList.LoadType.END, mLoadStateManager.getEnd(),
+ mLoadStateManager.getEndError());
+ }
+ mLoadStateListeners.add(listener);
+ }
+
+ /**
+ * Remove a previously registered PagedListListener.
+ *
+ * @param listener Previously registered listener.
+ * @see #getCurrentList()
+ * @see #addPagedListListener(PagedListListener)
+ */
+ public void removeLoadStateListListener(@NonNull PagedList.LoadStateListener listener) {
+ mLoadStateListeners.remove(listener);
+ if (mPagedList != null) {
+ mPagedList.removeWeakLoadStateListener(listener);
+ }
+ }
+
+ /**
* Returns the PagedList currently being displayed by the differ.
* <p>
* This is not necessarily the most recent list passed to {@link #submitList(PagedList)},
diff --git a/paging/runtime/src/main/java/androidx/paging/PagedListAdapter.java b/paging/runtime/src/main/java/androidx/paging/PagedListAdapter.java
index cfc8aa5..9f1d4dd 100644
--- a/paging/runtime/src/main/java/androidx/paging/PagedListAdapter.java
+++ b/paging/runtime/src/main/java/androidx/paging/PagedListAdapter.java
@@ -119,6 +119,14 @@
PagedListAdapter.this.onCurrentListChanged(previousList, currentList);
}
};
+ private final PagedList.LoadStateListener mLoadStateListener =
+ new PagedList.LoadStateListener() {
+ @Override
+ public void onLoadStateChanged(@NonNull PagedList.LoadType type,
+ @NonNull PagedList.LoadState state, @Nullable Throwable error) {
+ PagedListAdapter.this.onLoadStateChanged(type, state, error);
+ }
+ };
/**
* Creates a PagedListAdapter with default threading and
@@ -133,11 +141,13 @@
protected PagedListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
mDiffer.addPagedListListener(mListener);
+ mDiffer.addLoadStateListener(mLoadStateListener);
}
protected PagedListAdapter(@NonNull AsyncDifferConfig<T> config) {
mDiffer = new AsyncPagedListDiffer<>(new AdapterListUpdateCallback(this), config);
mDiffer.addPagedListListener(mListener);
+ mDiffer.addLoadStateListener(mLoadStateListener);
}
/**
@@ -241,4 +251,44 @@
public void onCurrentListChanged(
@Nullable PagedList<T> previousList, @Nullable PagedList<T> currentList) {
}
+
+
+ /**
+ * Called when the LoadState for a particular type of load (START, END, REFRESH) has
+ * changed.
+ * <p>
+ * REFRESH events can be used to drive a {@code SwipeRefreshLayout}, or START/END events
+ * can be used to drive loading spinner items in the Adapter.
+ *
+ * @param type Type of load - START, END, or REFRESH.
+ * @param state State of load - IDLE, LOADING, DONE, ERROR, or RETRYABLE_ERROR
+ * @param error Error, if in an error state, null otherwise.
+ */
+ public void onLoadStateChanged(@NonNull PagedList.LoadType type,
+ @NonNull PagedList.LoadState state, @Nullable Throwable error) {
+ }
+
+ /**
+ * Add a LoadStateListener to observe the loading state of the current PagedList.
+ *
+ * As new PagedLists are submitted and displayed, the listener will be notified to reflect
+ * current REFRESH, START, and END states.
+ *
+ * @param listener Listener to receive updates.
+ *
+ * @see #removeLoadStateListener(PagedList.LoadStateListener)
+ */
+ public void addLoadStateListener(PagedList.LoadStateListener listener) {
+ mDiffer.addLoadStateListener(listener);
+ }
+
+ /**
+ * Remove a previously registered LoadStateListener.
+ *
+ * @param listener Previously registered listener.
+ * @see #addLoadStateListener(PagedList.LoadStateListener)
+ */
+ public void removeLoadStateListener(PagedList.LoadStateListener listener) {
+ mDiffer.removeLoadStateListListener(listener);
+ }
}
diff --git a/paging/rxjava2/api/2.2.0-alpha01.txt b/paging/rxjava2/api/2.2.0-alpha01.txt
new file mode 100644
index 0000000..067f422
--- /dev/null
+++ b/paging/rxjava2/api/2.2.0-alpha01.txt
@@ -0,0 +1,16 @@
+// Signature format: 2.0
+package androidx.paging {
+
+ public final class RxPagedListBuilder<Key, Value> {
+ ctor public RxPagedListBuilder(androidx.paging.DataSource.Factory<Key,Value>, androidx.paging.PagedList.Config);
+ ctor public RxPagedListBuilder(androidx.paging.DataSource.Factory<Key,Value>, int);
+ method public io.reactivex.Flowable<androidx.paging.PagedList<Value>> buildFlowable(io.reactivex.BackpressureStrategy);
+ method public io.reactivex.Observable<androidx.paging.PagedList<Value>> buildObservable();
+ method public androidx.paging.RxPagedListBuilder<Key,Value> setBoundaryCallback(androidx.paging.PagedList.BoundaryCallback<Value>?);
+ method public androidx.paging.RxPagedListBuilder<Key,Value> setFetchScheduler(io.reactivex.Scheduler);
+ method public androidx.paging.RxPagedListBuilder<Key,Value> setInitialLoadKey(Key?);
+ method public androidx.paging.RxPagedListBuilder<Key,Value> setNotifyScheduler(io.reactivex.Scheduler);
+ }
+
+}
+
diff --git a/paging/rxjava2/ktx/api/2.2.0-alpha01.txt b/paging/rxjava2/ktx/api/2.2.0-alpha01.txt
new file mode 100644
index 0000000..6b573b4
--- /dev/null
+++ b/paging/rxjava2/ktx/api/2.2.0-alpha01.txt
@@ -0,0 +1,13 @@
+// Signature format: 2.0
+package androidx.paging {
+
+ public final class RxPagedListKt {
+ ctor public RxPagedListKt();
+ method public static <Key, Value> io.reactivex.Flowable<androidx.paging.PagedList<Value>> toFlowable(androidx.paging.DataSource.Factory<Key,Value>, androidx.paging.PagedList.Config config, Key? initialLoadKey = null, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, io.reactivex.Scheduler? fetchScheduler = null, io.reactivex.Scheduler? notifyScheduler = null, io.reactivex.BackpressureStrategy backpressureStrategy = io.reactivex.BackpressureStrategy.LATEST);
+ method public static <Key, Value> io.reactivex.Flowable<androidx.paging.PagedList<Value>> toFlowable(androidx.paging.DataSource.Factory<Key,Value>, int pageSize, Key? initialLoadKey = null, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, io.reactivex.Scheduler? fetchScheduler = null, io.reactivex.Scheduler? notifyScheduler = null, io.reactivex.BackpressureStrategy backpressureStrategy = io.reactivex.BackpressureStrategy.LATEST);
+ method public static <Key, Value> io.reactivex.Observable<androidx.paging.PagedList<Value>> toObservable(androidx.paging.DataSource.Factory<Key,Value>, androidx.paging.PagedList.Config config, Key? initialLoadKey = null, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, io.reactivex.Scheduler? fetchScheduler = null, io.reactivex.Scheduler? notifyScheduler = null);
+ method public static <Key, Value> io.reactivex.Observable<androidx.paging.PagedList<Value>> toObservable(androidx.paging.DataSource.Factory<Key,Value>, int pageSize, Key? initialLoadKey = null, androidx.paging.PagedList.BoundaryCallback<Value>? boundaryCallback = null, io.reactivex.Scheduler? fetchScheduler = null, io.reactivex.Scheduler? notifyScheduler = null);
+ }
+
+}
+