| /* |
| * Copyright 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 androidx.paging |
| |
| import androidx.annotation.CheckResult |
| import kotlinx.coroutines.flow.Flow |
| import kotlinx.coroutines.flow.flowOf |
| import kotlinx.coroutines.flow.map |
| |
| /** |
| * Container for Paged data from a single generation of loads. |
| * |
| * Each refresh of data (generally either pushed by local storage, or pulled from the network) |
| * will have a separate corresponding [PagingData]. |
| */ |
| class PagingData<T : Any> internal constructor( |
| internal val flow: Flow<PageEvent<T>>, |
| internal val receiver: UiReceiver |
| ) { |
| /** |
| * Returns a [PagingData] containing the result of applying the given [transform] to each |
| * element, as it is loaded. |
| * |
| * @see map |
| */ |
| @JvmName("map") |
| @CheckResult |
| fun <R : Any> mapSync(transform: (T) -> R): PagingData<R> = transform { event -> |
| event.map { transform(it) } |
| } |
| |
| /** |
| * Returns a [PagingData] of all elements returned from applying the given [transform] |
| * to each element, as it is loaded. |
| * |
| * @see flatMap |
| */ |
| @JvmName("flatMap") |
| @CheckResult |
| fun <R : Any> flatMapSync(transform: (T) -> Iterable<R>): PagingData<R> = transform { event -> |
| event.flatMap { transform(it) } |
| } |
| |
| /** |
| * Returns a [PagingData] containing only elements matching the given [predicate] |
| * |
| * @see filter |
| */ |
| @JvmName("filter") |
| @CheckResult |
| fun filterSync(predicate: (T) -> Boolean): PagingData<T> = transform { event -> |
| event.filter { predicate(it) } |
| } |
| |
| /** |
| * Returns a [PagingData] containing each original element, with the passed header [item] added |
| * to the start of the list. |
| * |
| * The header [item] is added to a loaded page which marks the end of the data stream in the |
| * prepend direction by returning null in [PagingSource.LoadResult.Page.prevKey]. It will be |
| * removed if the first page in the list is dropped, which can happen in the case of loaded |
| * pages exceeding [PagedList.Config.maxSize]. |
| * |
| * Note: This operation is not idempotent, calling it multiple times will continually add |
| * more headers to the start of the list, which can be useful if multiple header items are |
| * required. |
| * |
| * @see [insertFooterItem] |
| */ |
| @CheckResult |
| fun insertHeaderItem(item: T) = insertSeparators { before, _ -> |
| if (before == null) item else null |
| } |
| |
| /** |
| * Returns a [PagingData] containing each original element, with the passed footer [item] added |
| * to the end of the list. |
| * |
| * The footer [item] is added to a loaded page which marks the end of the data stream in the |
| * append direction, either by returning null in [PagingSource.LoadResult.Page.nextKey]. It |
| * will be removed if the first page in the list is dropped, which can happen in the case of |
| * loaded* pages exceeding [PagedList.Config.maxSize]. |
| * |
| * Note: This operation is not idempotent, calling it multiple times will continually add |
| * more footers to the end of the list, which can be useful if multiple footer items are |
| * required. |
| * |
| * @see [insertHeaderItem] |
| */ |
| @CheckResult |
| fun insertFooterItem(item: T) = insertSeparators { _, after -> |
| if (after == null) item else null |
| } |
| |
| companion object { |
| internal val NOOP_RECEIVER = object : UiReceiver { |
| override fun addHint(hint: ViewportHint) {} |
| |
| override fun retry() {} |
| |
| override fun refresh() {} |
| } |
| |
| @Suppress("MemberVisibilityCanBePrivate") // synthetic access |
| internal val EMPTY = PagingData<Any>( |
| flow = flowOf(PageEvent.Insert.EMPTY_REFRESH_LOCAL), |
| receiver = NOOP_RECEIVER |
| ) |
| |
| /** |
| * Create a [PagingData] that immediately displays an empty list of items when submitted to |
| * [AsyncPagingDataAdapter][androidx.paging.AsyncPagingDataAdapter]. |
| */ |
| @Suppress("UNCHECKED_CAST") |
| @JvmStatic // Convenience for Java developers. |
| fun <T : Any> empty() = EMPTY as PagingData<T> |
| |
| /** |
| * Create a [PagingData] that immediately displays a static list of items when submitted to |
| * [AsyncPagingDataAdapter][androidx.paging.AsyncPagingDataAdapter]. |
| * |
| * @param data Static list of [T] to display. |
| */ |
| @JvmStatic // Convenience for Java developers. |
| fun <T : Any> from(data: List<T>) = PagingData( |
| flow = flowOf( |
| PageEvent.Insert.Refresh( |
| pages = listOf(TransformablePage(originalPageOffset = 0, data = data)), |
| placeholdersBefore = 0, |
| placeholdersAfter = 0, |
| combinedLoadStates = CombinedLoadStates( |
| source = LoadStates( |
| refresh = LoadState.NotLoading.Incomplete, |
| prepend = LoadState.NotLoading.Complete, |
| append = LoadState.NotLoading.Complete |
| ) |
| ) |
| |
| ) |
| ), |
| receiver = NOOP_RECEIVER |
| ) |
| |
| // NOTE: samples in the doc below are manually imported from Java code in the samples |
| // project, since Java cannot be linked with @sample. |
| // DO NOT CHANGE THE BELOW COMMENT WITHOUT MAKING THE CORRESPONDING CHANGE IN `samples/` |
| /** |
| * Returns a [PagingData] containing each original element, with an optional separator |
| * generated by [generator], given the elements before and after (or null, in boundary |
| * conditions). |
| * |
| * Note that this transform is applied asynchronously, as pages are loaded. Potential |
| * separators between pages are only computed once both pages are loaded. |
| * |
| * **Kotlin callers should instead use the extension function [insertSeparators]** |
| * |
| * ``` |
| * /* |
| * * Create letter separators in an alphabetically sorted list. |
| * * |
| * * For example, if the input is: |
| * * "apple", "apricot", "banana", "carrot" |
| * * |
| * * The operator would output: |
| * * "A", "apple", "apricot", "B", "banana", "C", "carrot" |
| * */ |
| * pagingDataStream.map((pagingData) -> |
| * // map outer stream, so we can perform transformations on each paging generation |
| * PagingData.insertSeparators(pagingData, |
| * (@Nullable String before, @Nullable String after) -> { |
| * if (after != null && (before == null |
| * || before.charAt(0) != after.charAt(0))) { |
| * // separator - after is first item that starts with its first letter |
| * return Character.toString(Character.toUpperCase(after.charAt(0))); |
| * } else { |
| * // no separator - either end of list, or first |
| * // letters of items are the same |
| * return null; |
| * } |
| * })); |
| * |
| * /* |
| * * Create letter separators in an alphabetically sorted list of Items, with UiModel objects. |
| * * |
| * * For example, if the input is (each an `Item`): |
| * * "apple", "apricot", "banana", "carrot" |
| * * |
| * * The operator would output a list of UiModels corresponding to: |
| * * "A", "apple", "apricot", "B", "banana", "C", "carrot" |
| * */ |
| * pagingDataStream.map((itemPagingData) -> { |
| * // map outer stream, so we can perform transformations on each paging generation |
| * |
| * // first convert items in stream to UiModel.Item |
| * PagingData<UiModel.ItemModel> itemModelPagingData = |
| * itemPagingData.map(UiModel.ItemModel::new); |
| * |
| * // Now insert UiModel.Separators, which makes the PagingData of generic type UiModel |
| * return PagingData.insertSeparators( |
| * itemModelPagingData, |
| * (@Nullable UiModel.ItemModel before, @Nullable UiModel.ItemModel after) -> { |
| * if (after != null && (before == null |
| * || before.item.label.charAt(0) != after.item.label.charAt(0))) { |
| * // separator - after is first item that starts with its first letter |
| * return new UiModel.SeparatorModel( |
| * Character.toUpperCase(after.item.label.charAt(0))); |
| * } else { |
| * // no separator - either end of list, or first |
| * // letters of items are the same |
| * return null; |
| * } |
| * }); |
| * }); |
| * |
| * public class UiModel { |
| * static class ItemModel extends UiModel { |
| * public Item item; |
| * ItemModel(Item item) { |
| * this.item = item; |
| * } |
| * } |
| * static class SeparatorModel extends UiModel { |
| * public char character; |
| * SeparatorModel(char character) { |
| * this.character = character; |
| * } |
| * } |
| * } |
| */ |
| @JvmStatic |
| @CheckResult |
| fun <T : R, R : Any> insertSeparators( |
| pagingData: PagingData<T>, |
| generator: (T?, T?) -> R? |
| ): PagingData<R> { |
| return pagingData.insertSeparators { before, after -> generator(before, after) } |
| } |
| } |
| } |
| |
| private inline fun <T : Any, R : Any> PagingData<T>.transform( |
| crossinline transform: suspend (PageEvent<T>) -> PageEvent<R> |
| ) = PagingData( |
| flow = flow.map { transform(it) }, |
| receiver = receiver |
| ) |
| |
| /** |
| * Returns a [PagingData] containing the result of applying the given [transform] to each |
| * element, as it is loaded. |
| */ |
| @CheckResult |
| @JvmSynthetic |
| fun <T : Any, R : Any> PagingData<T>.map( |
| transform: suspend (T) -> R |
| ): PagingData<R> = transform { it.map(transform) } |
| |
| /** |
| * Returns a [PagingData] of all elements returned from applying the given [transform] |
| * to each element, as it is loaded. |
| */ |
| @CheckResult |
| @JvmSynthetic |
| fun <T : Any, R : Any> PagingData<T>.flatMap( |
| transform: suspend (T) -> Iterable<R> |
| ): PagingData<R> = transform { it.flatMap(transform) } |
| |
| /** |
| * Returns a [PagingData] containing only elements matching the given [predicate] |
| */ |
| @CheckResult |
| @JvmSynthetic |
| fun <T : Any> PagingData<T>.filter( |
| predicate: suspend (T) -> Boolean |
| ): PagingData<T> = transform { it.filter(predicate) } |
| |
| /** |
| * Returns a [PagingData] containing each original element, with an optional separator |
| * generated by [generator], given the elements before and after (or null, in boundary |
| * conditions). |
| * |
| * Note that this transform is applied asynchronously, as pages are loaded. Potential |
| * separators between pages are only computed once both pages are loaded. |
| * |
| * @sample androidx.paging.samples.insertSeparatorsSample |
| * @sample androidx.paging.samples.insertSeparatorsUiModelSample |
| */ |
| @CheckResult |
| @JvmSynthetic |
| fun <T : R, R : Any> PagingData<T>.insertSeparators( |
| generator: suspend (T?, T?) -> R? |
| ): PagingData<R> { |
| // This function must be an extension method, as it indirectly imposes a constraint on |
| // the type of T (because T extends R). Ideally it would be declared not be an |
| // extension, to make this method discoverable for Java callers, but we need to support |
| // the common UI model pattern for separators: |
| // class UiModel |
| // class ItemModel: UiModel |
| // class SeparatorModel: UiModel |
| return PagingData( |
| flow = flow.insertEventSeparators(generator), |
| receiver = receiver |
| ) |
| } |
| |
| internal interface UiReceiver { |
| fun addHint(hint: ViewportHint) |
| fun retry() |
| fun refresh() |
| } |