1 /*
<lambda>null2  * Copyright 2019 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.internal.appendMediatorStatesIfNotNull
23 
24 /**
25  * Events in the stream from paging fetch logic to UI.
26  *
27  * Every event sent to the UI is a PageEvent, and will be processed atomically.
28  */
29 internal sealed class PageEvent<T : Any> {
30     /**
31      * Represents a fully-terminal, static list of data.
32      *
33      * This event should always be the first and only emission in a Flow<PageEvent> within a
34      * generation.
35      *
36      * @param sourceLoadStates source [LoadStates] to emit if non-null, ignored otherwise, allowing
37      *   the presenter receiving this event to maintain the previous state.
38      * @param mediatorLoadStates mediator [LoadStates] to emit if non-null, ignored otherwise,
39      *   allowing the presenter receiving this event to maintain its previous state.
40      */
41     data class StaticList<T : Any>(
42         val data: List<T>,
43         val sourceLoadStates: LoadStates? = null,
44         val mediatorLoadStates: LoadStates? = null
45     ) : PageEvent<T>() {
46         override suspend fun <R : Any> map(transform: suspend (T) -> R): PageEvent<R> {
47             return StaticList(
48                 data = data.map { transform(it) },
49                 sourceLoadStates = sourceLoadStates,
50                 mediatorLoadStates = mediatorLoadStates,
51             )
52         }
53 
54         override suspend fun <R : Any> flatMap(
55             transform: suspend (T) -> Iterable<R>
56         ): PageEvent<R> {
57             return StaticList(
58                 data = data.flatMap { transform(it) },
59                 sourceLoadStates = sourceLoadStates,
60                 mediatorLoadStates = mediatorLoadStates,
61             )
62         }
63 
64         override suspend fun filter(predicate: suspend (T) -> Boolean): PageEvent<T> {
65             return StaticList(
66                 data = data.filter { predicate(it) },
67                 sourceLoadStates = sourceLoadStates,
68                 mediatorLoadStates = mediatorLoadStates,
69             )
70         }
71 
72         override fun toString(): String {
73             return appendMediatorStatesIfNotNull(mediatorLoadStates) {
74                 """PageEvent.StaticList with ${data.size} items (
75                     |   first item: ${data.firstOrNull()}
76                     |   last item: ${data.lastOrNull()}
77                     |   sourceLoadStates: $sourceLoadStates
78                     """
79             }
80         }
81     }
82 
83     // Intentional to prefer Refresh, Prepend, Append constructors from Companion.
84     @Suppress("DATA_CLASS_COPY_VISIBILITY_WILL_BE_CHANGED_WARNING")
85     data class Insert<T : Any>
86     private constructor(
87         val loadType: LoadType,
88         val pages: List<TransformablePage<T>>,
89         val placeholdersBefore: Int,
90         val placeholdersAfter: Int,
91         val sourceLoadStates: LoadStates,
92         val mediatorLoadStates: LoadStates? = null
93     ) : PageEvent<T>() {
94         init {
95             require(loadType == APPEND || placeholdersBefore >= 0) {
96                 "Prepend insert defining placeholdersBefore must be > 0, but was" +
97                     " $placeholdersBefore"
98             }
99             require(loadType == PREPEND || placeholdersAfter >= 0) {
100                 "Append insert defining placeholdersAfter must be > 0, but was" +
101                     " $placeholdersAfter"
102             }
103             require(loadType != REFRESH || pages.isNotEmpty()) {
104                 "Cannot create a REFRESH Insert event with no TransformablePages as this could " +
105                     "permanently stall pagination. Note that this check does not prevent empty " +
106                     "LoadResults and is instead usually an indication of an internal error in " +
107                     "Paging itself."
108             }
109         }
110 
111         private inline fun <R : Any> mapPages(
112             transform: (TransformablePage<T>) -> TransformablePage<R>
113         ) = transformPages { it.map(transform) }
114 
115         internal inline fun <R : Any> transformPages(
116             transform: (List<TransformablePage<T>>) -> List<TransformablePage<R>>
117         ): Insert<R> =
118             Insert(
119                 loadType = loadType,
120                 pages = transform(pages),
121                 placeholdersBefore = placeholdersBefore,
122                 placeholdersAfter = placeholdersAfter,
123                 sourceLoadStates = sourceLoadStates,
124                 mediatorLoadStates = mediatorLoadStates,
125             )
126 
127         override suspend fun <R : Any> map(transform: suspend (T) -> R): PageEvent<R> = mapPages {
128             TransformablePage(
129                 originalPageOffsets = it.originalPageOffsets,
130                 data = it.data.map { item -> transform(item) },
131                 hintOriginalPageOffset = it.hintOriginalPageOffset,
132                 hintOriginalIndices = it.hintOriginalIndices
133             )
134         }
135 
136         override suspend fun <R : Any> flatMap(
137             transform: suspend (T) -> Iterable<R>
138         ): PageEvent<R> = mapPages {
139             val data = mutableListOf<R>()
140             val originalIndices = mutableListOf<Int>()
141             it.data.forEachIndexed { index, t ->
142                 data += transform(t)
143                 val indexToStore = it.hintOriginalIndices?.get(index) ?: index
144                 while (originalIndices.size < data.size) {
145                     originalIndices.add(indexToStore)
146                 }
147             }
148             TransformablePage(
149                 originalPageOffsets = it.originalPageOffsets,
150                 data = data,
151                 hintOriginalPageOffset = it.hintOriginalPageOffset,
152                 hintOriginalIndices = originalIndices
153             )
154         }
155 
156         override suspend fun filter(predicate: suspend (T) -> Boolean): PageEvent<T> = mapPages {
157             val data = mutableListOf<T>()
158             val originalIndices = mutableListOf<Int>()
159             it.data.forEachIndexed { index, t ->
160                 if (predicate(t)) {
161                     data.add(t)
162                     originalIndices.add(it.hintOriginalIndices?.get(index) ?: index)
163                 }
164             }
165             TransformablePage(
166                 originalPageOffsets = it.originalPageOffsets,
167                 data = data,
168                 hintOriginalPageOffset = it.hintOriginalPageOffset,
169                 hintOriginalIndices = originalIndices
170             )
171         }
172 
173         companion object {
174             fun <T : Any> Refresh(
175                 pages: List<TransformablePage<T>>,
176                 placeholdersBefore: Int,
177                 placeholdersAfter: Int,
178                 sourceLoadStates: LoadStates,
179                 mediatorLoadStates: LoadStates? = null
180             ) =
181                 Insert(
182                     REFRESH,
183                     pages,
184                     placeholdersBefore,
185                     placeholdersAfter,
186                     sourceLoadStates,
187                     mediatorLoadStates,
188                 )
189 
190             fun <T : Any> Prepend(
191                 pages: List<TransformablePage<T>>,
192                 placeholdersBefore: Int,
193                 sourceLoadStates: LoadStates,
194                 mediatorLoadStates: LoadStates? = null
195             ) =
196                 Insert(
197                     PREPEND,
198                     pages,
199                     placeholdersBefore,
200                     -1,
201                     sourceLoadStates,
202                     mediatorLoadStates,
203                 )
204 
205             fun <T : Any> Append(
206                 pages: List<TransformablePage<T>>,
207                 placeholdersAfter: Int,
208                 sourceLoadStates: LoadStates,
209                 mediatorLoadStates: LoadStates? = null
210             ) =
211                 Insert(
212                     APPEND,
213                     pages,
214                     -1,
215                     placeholdersAfter,
216                     sourceLoadStates,
217                     mediatorLoadStates,
218                 )
219 
220             /**
221              * Empty refresh, used to convey initial state.
222              *
223              * Note - has no remote state, so remote state may be added over time
224              */
225             val EMPTY_REFRESH_LOCAL: Insert<Any> =
226                 Refresh(
227                     pages = listOf(TransformablePage.EMPTY_INITIAL_PAGE),
228                     placeholdersBefore = 0,
229                     placeholdersAfter = 0,
230                     sourceLoadStates =
231                         LoadStates(
232                             refresh = LoadState.NotLoading.Incomplete,
233                             prepend = LoadState.NotLoading.Complete,
234                             append = LoadState.NotLoading.Complete,
235                         ),
236                 )
237         }
238 
239         override fun toString(): String {
240             val itemCount = pages.fold(0) { total, page -> total + page.data.size }
241             val placeholdersBefore = if (placeholdersBefore != -1) "$placeholdersBefore" else "none"
242             val placeholdersAfter = if (placeholdersAfter != -1) "$placeholdersAfter" else "none"
243             return appendMediatorStatesIfNotNull(mediatorLoadStates) {
244                 """PageEvent.Insert for $loadType, with $itemCount items (
245                     |   first item: ${pages.firstOrNull()?.data?.firstOrNull()}
246                     |   last item: ${pages.lastOrNull()?.data?.lastOrNull()}
247                     |   placeholdersBefore: $placeholdersBefore
248                     |   placeholdersAfter: $placeholdersAfter
249                     |   sourceLoadStates: $sourceLoadStates
250                     """
251             }
252         }
253     }
254 
255     // TODO: b/195658070 consider refactoring Drop events to carry full source/mediator states.
256     data class Drop<T : Any>(
257         val loadType: LoadType,
258         /** Smallest [TransformablePage.originalPageOffsets] to drop; inclusive. */
259         val minPageOffset: Int,
260         /** Largest [TransformablePage.originalPageOffsets] to drop; inclusive */
261         val maxPageOffset: Int,
262         val placeholdersRemaining: Int
263     ) : PageEvent<T>() {
264 
265         init {
266             require(loadType != REFRESH) { "Drop load type must be PREPEND or APPEND" }
267             require(pageCount > 0) { "Drop count must be > 0, but was $pageCount" }
268             require(placeholdersRemaining >= 0) {
269                 "Invalid placeholdersRemaining $placeholdersRemaining"
270             }
271         }
272 
273         val pageCount
274             get() = maxPageOffset - minPageOffset + 1
275 
276         override fun toString(): String {
277             val direction =
278                 when (loadType) {
279                     APPEND -> "end"
280                     PREPEND -> "front"
281                     else ->
282                         throw IllegalArgumentException("Drop load type must be PREPEND or APPEND")
283                 }
284             return """PageEvent.Drop from the $direction (
285                     |   minPageOffset: $minPageOffset
286                     |   maxPageOffset: $maxPageOffset
287                     |   placeholdersRemaining: $placeholdersRemaining
288                     |)"""
289                 .trimMargin()
290         }
291     }
292 
293     /**
294      * A [PageEvent] to notify presenter layer of changes in local and remote LoadState.
295      *
296      * Uses two LoadStates objects instead of CombinedLoadStates so that consumers like
297      * PagingDataPresenter can define behavior of convenience properties
298      */
299     data class LoadStateUpdate<T : Any>(
300         val source: LoadStates,
301         val mediator: LoadStates? = null,
302     ) : PageEvent<T>() {
303 
304         override fun toString(): String {
305             return appendMediatorStatesIfNotNull(mediator) {
306                 """PageEvent.LoadStateUpdate (
307                     |   sourceLoadStates: $source
308                     """
309             }
310         }
311     }
312 
313     @Suppress("UNCHECKED_CAST")
314     open suspend fun <R : Any> map(transform: suspend (T) -> R): PageEvent<R> = this as PageEvent<R>
315 
316     @Suppress("UNCHECKED_CAST")
317     open suspend fun <R : Any> flatMap(transform: suspend (T) -> Iterable<R>): PageEvent<R> {
318         return this as PageEvent<R>
319     }
320 
321     open suspend fun filter(predicate: suspend (T) -> Boolean): PageEvent<T> = this
322 }
323