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