1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.paging
18 
19 import androidx.paging.LoadType.APPEND
20 import androidx.paging.LoadType.PREPEND
21 import androidx.paging.LoadType.REFRESH
22 import androidx.paging.PagingSource.LoadResult
23 import androidx.paging.RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH
24 import kotlin.jvm.JvmName
25 
26 /**
27  * Defines a set of callbacks used to incrementally load data from a remote source into a local
28  * source wrapped by a [PagingSource], e.g., loading data from network into a local db cache.
29  *
30  * A [RemoteMediator] is registered by passing it to [Pager]'s constructor.
31  *
32  * [RemoteMediator] allows hooking into the following events:
33  * * Stream initialization
34  * * [REFRESH] signal driven from UI
35  * * [PagingSource] returns a [LoadResult] which signals a boundary condition, i.e., the most recent
36  *   [LoadResult.Page] in the [PREPEND] or [APPEND] direction has [LoadResult.Page.prevKey] or
37  *   [LoadResult.Page.nextKey] set to `null` respectively.
38  *
39  * @sample androidx.paging.samples.remoteMediatorItemKeyedSample
40  * @sample androidx.paging.samples.remoteMediatorPageKeyedSample
41  */
42 @ExperimentalPagingApi
43 public abstract class RemoteMediator<Key : Any, Value : Any> {
44     /**
45      * Callback triggered when Paging needs to request more data from a remote source due to any of
46      * the following events:
47      *     * Stream initialization if [initialize] returns [LAUNCH_INITIAL_REFRESH]
48      * * [REFRESH] signal driven from UI
49      * * [PagingSource] returns a [LoadResult] which signals a boundary condition, i.e., the most
50      *   recent [LoadResult.Page] in the [PREPEND] or [APPEND] direction has
51      *   [LoadResult.Page.prevKey] or [LoadResult.Page.nextKey] set to `null` respectively.
52      *
53      * It is the responsibility of this method to update the backing dataset and trigger
54      * [PagingSource.invalidate] to allow [androidx.paging.PagingDataAdapter] to pick up new items
55      * found by [load].
56      *
57      * The runtime and result of this method defines the remote [LoadState] behavior sent to the UI
58      * via [CombinedLoadStates].
59      *
60      * This method is never called concurrently *unless* [Pager.flow] has multiple collectors. Note
61      * that Paging might cancel calls to this function if it is currently executing a [PREPEND] or
62      * [APPEND] and a [REFRESH] is requested. In that case, [REFRESH] has higher priority and will
63      * be executed after the previous call is cancelled. If the [load] call with [REFRESH] returns
64      * an error, Paging will call [load] with the previously cancelled [APPEND] or [PREPEND]
65      * request. If [REFRESH] succeeds, it won't make the [APPEND] or [PREPEND] requests unless they
66      * are necessary again after the [REFRESH] is applied to the UI.
67      *
68      * @param loadType [LoadType] of the condition which triggered this callback.
69      *     * [PREPEND] indicates the end of pagination in the [PREPEND] direction was reached. This
70      *       occurs when [PagingSource.load] returns a [LoadResult.Page] with
71      *       [LoadResult.Page.prevKey] == `null`.
72      *     * [APPEND] indicates the end of pagination in the [APPEND] direction was reached. This
73      *       occurs when [PagingSource.load] returns a [LoadResult.Page] with
74      *       [LoadResult.Page.nextKey] == `null`.
75      *     * [REFRESH] indicates this method was triggered due to a requested refresh. Generally,
76      *       this means that a request to load remote data and **replace** all local data was made.
77      *       This can happen when:
78      *         * Stream initialization if [initialize] returns [LAUNCH_INITIAL_REFRESH]
79      *         * An explicit call to refresh driven by the UI
80      *
81      * @param state A copy of the state including the list of pages currently held in memory of the
82      *   currently presented [PagingData] at the time of starting the load. E.g. for load(loadType =
83      *   APPEND), you can use the page or item at the end as input for what to load from the
84      *   network.
85      * @return [MediatorResult] signifying what [LoadState] to be passed to the UI, and whether
86      *   there's more data available.
87      */
loadnull88     public abstract suspend fun load(
89         loadType: LoadType,
90         state: PagingState<Key, Value>
91     ): MediatorResult
92 
93     /**
94      * Callback fired during initialization of a [PagingData] stream, before initial load.
95      *
96      * This function runs to completion before any loading is performed.
97      *
98      * @return [InitializeAction] used to control whether [load] with load type [REFRESH] will be
99      *   immediately dispatched when the first [PagingData] is submitted:
100      *     * [LAUNCH_INITIAL_REFRESH] to immediately dispatch [load] asynchronously with load type
101      *       [REFRESH], to update paginated content when the stream is initialized. Note: This also
102      *       prevents [RemoteMediator] from triggering [PREPEND] or [APPEND] until [REFRESH]
103      *       succeeds.
104      *     * [SKIP_INITIAL_REFRESH] to wait for a refresh request from the UI before dispatching
105      *       [load] asynchronously with load type [REFRESH].
106      */
107     public open suspend fun initialize(): InitializeAction = LAUNCH_INITIAL_REFRESH
108 
109     /** Return type of [load], which determines [LoadState]. */
110     public sealed class MediatorResult {
111         /** Recoverable error that can be retried, sets the [LoadState] to [LoadState.Error]. */
112         public class Error(public val throwable: Throwable) : MediatorResult()
113 
114         /**
115          * Success signaling that [LoadState] should be set to [LoadState.NotLoading] if
116          * [endOfPaginationReached] is `true`, otherwise [LoadState] is kept at [LoadState.Loading]
117          * to await invalidation.
118          *
119          * NOTE: It is the responsibility of [load] to update the backing dataset and trigger
120          * [PagingSource.invalidate] to allow [androidx.paging.PagingDataAdapter] to pick up new
121          * items found by [load].
122          */
123         public class Success(
124             @get:JvmName("endOfPaginationReached") public val endOfPaginationReached: Boolean
125         ) : MediatorResult()
126     }
127 
128     /**
129      * Return type of [initialize], which signals the action to take after [initialize] completes.
130      */
131     public enum class InitializeAction {
132         /**
133          * Immediately dispatch a [load] asynchronously with load type [REFRESH], to update
134          * paginated content when the stream is initialized.
135          *
136          * Note: This also prevents [RemoteMediator] from triggering [PREPEND] or [APPEND] until
137          * [REFRESH] succeeds.
138          */
139         LAUNCH_INITIAL_REFRESH,
140 
141         /**
142          * Wait for a refresh request from the UI before dispatching [load] with load type [REFRESH]
143          */
144         SKIP_INITIAL_REFRESH
145     }
146 }
147