/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.paging; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.arch.core.util.Function; import java.util.List; import java.util.concurrent.Executor; /** * Incremental data loader for paging keyed content, where loaded content uses previously loaded * items as input to future loads. *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

*

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

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

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

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

* If your key is more complex, such as when you're sorting by name, then resolving collisions * with integer ID, you'll need to return both. In such a case you would use a wrapper class, * such as {@code Pair} or, in Kotlin, * {@code data class Key(val name: String, val id: Int)} * * @param item Item to get the key from. * @return Key associated with given item. */ @NonNull public abstract Key getKey(@NonNull Value item); @NonNull @Override public final ItemKeyedDataSource mapByPage( @NonNull Function, List> function) { return new WrapperItemKeyedDataSource<>(this, function); } @NonNull @Override public final ItemKeyedDataSource map( @NonNull Function function) { return mapByPage(createListFunction(function)); } }