All Downloads are FREE. Search and download functionalities are using the official Maven repository.

android.arch.paging.DataSource Maven / Gradle / Ivy

/*
 * Copyright 2016-2019 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 android.arch.paging;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.arch.core.util.Function;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Base class for loading pages of snapshot data into a {@link PagedList}.
 * 

* DataSource is queried to load pages of content into a {@link PagedList}. A PagedList can grow as * it loads more data, but the data loaded cannot be updated. If the underlying data set is * modified, a new PagedList / DataSource pair must be created to represent the new data. *

Loading Pages

* PagedList queries data from its DataSource in response to loading hints. {@link PagedListAdapter} * calls {@link PagedList#loadAround(int)} to load content as the user scrolls in a RecyclerView. *

* To control how and when a PagedList queries data from its DataSource, see * {@link PagedList.Config}. The Config object defines things like load sizes and prefetch distance. *

Updating Paged Data

* A PagedList / DataSource pair are a snapshot of the data set. A new pair of * PagedList / DataSource must be created if an update occurs, such as a reorder, insert, delete, or * content update occurs. A DataSource must detect that it cannot continue loading its * snapshot (for instance, when Database query notices a table being invalidated), and call * {@link #invalidate()}. Then a new PagedList / DataSource pair would be created to load data from * the new state of the Database query. *

* To page in data that doesn't update, you can create a single DataSource, and pass it to a single * PagedList. For example, loading from network when the network's paging API doesn't provide * updates. *

* To page in data from a source that does provide updates, you can create a * {@link DataSource.Factory}, where each DataSource created is invalidated when an update to the * data set occurs that makes the current snapshot invalid. For example, when paging a query from * the Database, and the table being queried inserts or removes items. You can also use a * DataSource.Factory to provide multiple versions of network-paged lists. If reloading all content * (e.g. in response to an action like swipe-to-refresh) is required to get a new version of data, * you can connect an explicit refresh signal to call {@link #invalidate()} on the current * DataSource. *

* If you have more granular update signals, such as a network API signaling an update to a single * item in the list, it's recommended to load data from network into memory. Then present that * data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory * copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the * snapshot can be created. *

Implementing a DataSource

* To implement, extend one of the subclasses: {@link PageKeyedDataSource}, * {@link ItemKeyedDataSource}, or {@link PositionalDataSource}. *

* Use {@link PageKeyedDataSource} if pages you load embed keys for loading adjacent pages. For * example a network response that returns some items, and a next/previous page links. *

* Use {@link ItemKeyedDataSource} if you need to use data from item {@code N-1} to load item * {@code N}. For example, if requesting the backend for the next comments in the list * requires the ID or timestamp of the most recent loaded comment, or if querying the next users * from a name-sorted database query requires the name and unique ID of the previous. *

* Use {@link PositionalDataSource} if you can load pages of a requested size at arbitrary * positions, and provide a fixed item count. PositionalDataSource supports querying pages at * arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that * PositionalDataSource is required to respect page size for efficient tiling. If you want to * override page size (e.g. when network page size constraints are only known at runtime), use one * of the other DataSource classes. *

* Because a {@code null} item indicates a placeholder in {@link PagedList}, DataSource may not * return {@code null} items in lists that it loads. This is so that users of the PagedList * can differentiate unloaded placeholder items from content that has been paged in. * * @param Input used to trigger initial load from the DataSource. Often an Integer position. * @param Value type loaded by the DataSource. */ @SuppressWarnings("unused") // suppress warning to remove Key/Value, needed for subclass type safety public abstract class DataSource { /** * Factory for DataSources. *

* Data-loading systems of an application or library can implement this interface to allow * {@code LiveData}s to be created. For example, Room can provide a * DataSource.Factory for a given SQL query: * *

     * {@literal @}Dao
     * interface UserDao {
     *    {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
     *    public abstract DataSource.Factory<Integer, User> usersByLastName();
     * }
     * 
* In the above sample, {@code Integer} is used because it is the {@code Key} type of * PositionalDataSource. Currently, Room uses the {@code LIMIT}/{@code OFFSET} SQL keywords to * page a large query with a PositionalDataSource. * * @param Key identifying items in DataSource. * @param Type of items in the list loaded by the DataSources. */ public abstract static class Factory { /** * Create a DataSource. *

* The DataSource should invalidate itself if the snapshot is no longer valid. If a * DataSource becomes invalid, the only way to query more data is to create a new DataSource * from the Factory. *

* {@link LivePagedListBuilder} for example will construct a new PagedList and DataSource * when the current DataSource is invalidated, and pass the new PagedList through the * {@code LiveData} to observers. * * @return the new DataSource. */ public abstract DataSource create(); /** * Applies the given function to each value emitted by DataSources produced by this Factory. *

* Same as {@link #mapByPage(Function)}, but operates on individual items. * * @param function Function that runs on each loaded item, returning items of a potentially * new type. * @param Type of items produced by the new DataSource, from the passed function. * * @return A new DataSource.Factory, which transforms items using the given function. * * @see #mapByPage(Function) * @see DataSource#map(Function) * @see DataSource#mapByPage(Function) */ @NonNull public DataSource.Factory map( @NonNull Function function) { return mapByPage(createListFunction(function)); } /** * Applies the given function to each value emitted by DataSources produced by this Factory. *

* Same as {@link #map(Function)}, but allows for batch conversions. * * @param function Function that runs on each loaded page, returning items of a potentially * new type. * @param Type of items produced by the new DataSource, from the passed function. * * @return A new DataSource.Factory, which transforms items using the given function. * * @see #map(Function) * @see DataSource#map(Function) * @see DataSource#mapByPage(Function) */ @NonNull public DataSource.Factory mapByPage( @NonNull final Function, List> function) { return new Factory() { @Override public DataSource create() { return Factory.this.create().mapByPage(function); } }; } } @NonNull static Function, List> createListFunction( final @NonNull Function innerFunc) { return new Function, List>() { @Override public List apply(@NonNull List source) { List out = new ArrayList<>(source.size()); for (int i = 0; i < source.size(); i++) { out.add(innerFunc.apply(source.get(i))); } return out; } }; } static List convert(Function, List> function, List source) { List dest = function.apply(source); if (dest.size() != source.size()) { throw new IllegalStateException("Invalid Function " + function + " changed return size. This is not supported."); } return dest; } // Since we currently rely on implementation details of two implementations, // prevent external subclassing, except through exposed subclasses DataSource() { } /** * Applies the given function to each value emitted by the DataSource. *

* Same as {@link #map(Function)}, but allows for batch conversions. * * @param function Function that runs on each loaded page, returning items of a potentially * new type. * @param Type of items produced by the new DataSource, from the passed function. * * @return A new DataSource, which transforms items using the given function. * * @see #map(Function) * @see DataSource.Factory#map(Function) * @see DataSource.Factory#mapByPage(Function) */ @NonNull public abstract DataSource mapByPage( @NonNull Function, List> function); /** * Applies the given function to each value emitted by the DataSource. *

* Same as {@link #mapByPage(Function)}, but operates on individual items. * * @param function Function that runs on each loaded item, returning items of a potentially * new type. * @param Type of items produced by the new DataSource, from the passed function. * * @return A new DataSource, which transforms items using the given function. * * @see #mapByPage(Function) * @see DataSource.Factory#map(Function) * @see DataSource.Factory#mapByPage(Function) */ @NonNull public abstract DataSource map( @NonNull Function function); /** * Returns true if the data source guaranteed to produce a contiguous set of items, * never producing gaps. */ abstract boolean isContiguous(); static class LoadCallbackHelper { static void validateInitialLoadParams(@NonNull List data, int position, int totalCount) { if (position < 0) { throw new IllegalArgumentException("Position must be non-negative"); } if (data.size() + position > totalCount) { throw new IllegalArgumentException( "List size + position too large, last item in list beyond totalCount."); } if (data.size() == 0 && totalCount > 0) { throw new IllegalArgumentException( "Initial result cannot be empty if items are present in data set."); } } @PageResult.ResultType final int mResultType; private final DataSource mDataSource; private final PageResult.Receiver mReceiver; // mSignalLock protects mPostExecutor, and mHasSignalled private final Object mSignalLock = new Object(); private Executor mPostExecutor = null; private boolean mHasSignalled = false; LoadCallbackHelper(@NonNull DataSource dataSource, @PageResult.ResultType int resultType, @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) { mDataSource = dataSource; mResultType = resultType; mPostExecutor = mainThreadExecutor; mReceiver = receiver; } void setPostExecutor(Executor postExecutor) { synchronized (mSignalLock) { mPostExecutor = postExecutor; } } /** * Call before verifying args, or dispatching actul results * * @return true if DataSource was invalid, and invalid result dispatched */ boolean dispatchInvalidResultIfInvalid() { if (mDataSource.isInvalid()) { dispatchResultToReceiver(PageResult.getInvalidResult()); return true; } return false; } void dispatchResultToReceiver(final @NonNull PageResult result) { Executor executor; synchronized (mSignalLock) { if (mHasSignalled) { throw new IllegalStateException( "callback.onResult already called, cannot call again."); } mHasSignalled = true; executor = mPostExecutor; } if (executor != null) { executor.execute(new Runnable() { @Override public void run() { mReceiver.onPageResult(mResultType, result); } }); } else { mReceiver.onPageResult(mResultType, result); } } } /** * Invalidation callback for DataSource. *

* Used to signal when a DataSource a data source has become invalid, and that a new data source * is needed to continue loading data. */ public interface InvalidatedCallback { /** * Called when the data backing the list has become invalid. This callback is typically used * to signal that a new data source is needed. *

* This callback will be invoked on the thread that calls {@link #invalidate()}. It is valid * for the data source to invalidate itself during its load methods, or for an outside * source to invalidate it. */ void onInvalidated(); } private AtomicBoolean mInvalid = new AtomicBoolean(false); private CopyOnWriteArrayList mOnInvalidatedCallbacks = new CopyOnWriteArrayList<>(); /** * Add a callback to invoke when the DataSource is first invalidated. *

* Once invalidated, a data source will not become valid again. *

* A data source will only invoke its callbacks once - the first time {@link #invalidate()} * is called, on that thread. * * @param onInvalidatedCallback The callback, will be invoked on thread that * {@link #invalidate()} is called on. */ @SuppressWarnings("WeakerAccess") public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { mOnInvalidatedCallbacks.add(onInvalidatedCallback); } /** * Remove a previously added invalidate callback. * * @param onInvalidatedCallback The previously added callback. */ @SuppressWarnings("WeakerAccess") public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { mOnInvalidatedCallbacks.remove(onInvalidatedCallback); } /** * Signal the data source to stop loading, and notify its callback. *

* If invalidate has already been called, this method does nothing. */ public void invalidate() { if (mInvalid.compareAndSet(false, true)) { for (InvalidatedCallback callback : mOnInvalidatedCallbacks) { callback.onInvalidated(); } } } /** * Returns true if the data source is invalid, and can no longer be queried for data. * * @return True if the data source is invalid, and can no longer return data. */ @WorkerThread public boolean isInvalid() { return mInvalid.get(); } }