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.compose
18 
19 import androidx.compose.foundation.lazy.LazyListScope
20 import androidx.compose.runtime.Composable
21 import androidx.compose.runtime.LaunchedEffect
22 import androidx.compose.runtime.getValue
23 import androidx.compose.runtime.mutableStateOf
24 import androidx.compose.runtime.remember
25 import androidx.compose.runtime.setValue
26 import androidx.compose.ui.platform.AndroidUiDispatcher
27 import androidx.paging.CombinedLoadStates
28 import androidx.paging.ItemSnapshotList
29 import androidx.paging.LoadState
30 import androidx.paging.LoadStates
31 import androidx.paging.PagingData
32 import androidx.paging.PagingDataEvent
33 import androidx.paging.PagingDataPresenter
34 import androidx.paging.PagingSource
35 import androidx.paging.RemoteMediator
36 import kotlin.coroutines.CoroutineContext
37 import kotlin.coroutines.EmptyCoroutineContext
38 import kotlinx.coroutines.flow.Flow
39 import kotlinx.coroutines.flow.SharedFlow
40 import kotlinx.coroutines.flow.collectLatest
41 import kotlinx.coroutines.flow.filterNotNull
42 import kotlinx.coroutines.withContext
43 
44 /**
45  * The class responsible for accessing the data from a [Flow] of [PagingData]. In order to obtain an
46  * instance of [LazyPagingItems] use the [collectAsLazyPagingItems] extension method of [Flow] with
47  * [PagingData]. This instance can be used for Lazy foundations such as [LazyListScope.items] to
48  * display data received from the [Flow] of [PagingData].
49  *
50  * Previewing [LazyPagingItems] is supported on a list of mock data. See sample for how to preview
51  * mock data.
52  *
53  * @sample androidx.paging.compose.samples.PagingPreview
54  * @param T the type of value used by [PagingData].
55  */
56 public class LazyPagingItems<T : Any>
57 internal constructor(
58     /** the [Flow] object which contains a stream of [PagingData] elements. */
59     private val flow: Flow<PagingData<T>>
60 ) {
61     private val mainDispatcher = AndroidUiDispatcher.Main
62 
63     /**
64      * If the [flow] is a SharedFlow, it is expected to be the flow returned by from
65      * pager.flow.cachedIn(scope) which could contain a cached PagingData. We pass the cached
66      * PagingData to the presenter so that if the PagingData contains cached data, the presenter can
67      * be initialized with the data prior to collection on pager.
68      */
69     private val pagingDataPresenter =
70         object :
71             PagingDataPresenter<T>(
72                 mainContext = mainDispatcher,
73                 cachedPagingData =
74                     if (flow is SharedFlow<PagingData<T>>) flow.replayCache.firstOrNull() else null
75             ) {
presentPagingDataEventnull76             override suspend fun presentPagingDataEvent(
77                 event: PagingDataEvent<T>,
78             ) {
79                 updateItemSnapshotList()
80             }
81         }
82 
83     /**
84      * Contains the immutable [ItemSnapshotList] of currently presented items, including any
85      * placeholders if they are enabled. Note that similarly to [peek] accessing the items in a list
86      * will not trigger any loads. Use [get] to achieve such behavior.
87      */
88     var itemSnapshotList by mutableStateOf(pagingDataPresenter.snapshot())
89         private set
90 
91     /** The number of items which can be accessed. */
92     val itemCount: Int
93         get() = itemSnapshotList.size
94 
updateItemSnapshotListnull95     private fun updateItemSnapshotList() {
96         itemSnapshotList = pagingDataPresenter.snapshot()
97     }
98 
99     /**
100      * Returns the presented item at the specified position, notifying Paging of the item access to
101      * trigger any loads necessary to fulfill prefetchDistance.
102      *
103      * @see peek
104      */
getnull105     operator fun get(index: Int): T? {
106         pagingDataPresenter[index] // this registers the value load
107         return itemSnapshotList[index]
108     }
109 
110     /**
111      * Returns the presented item at the specified position, without notifying Paging of the item
112      * access that would normally trigger page loads.
113      *
114      * @param index Index of the presented item to return, including placeholders.
115      * @return The presented item at position [index], `null` if it is a placeholder
116      */
peeknull117     fun peek(index: Int): T? {
118         return itemSnapshotList[index]
119     }
120 
121     /**
122      * Retry any failed load requests that would result in a [LoadState.Error] update to this
123      * [LazyPagingItems].
124      *
125      * Unlike [refresh], this does not invalidate [PagingSource], it only retries failed loads
126      * within the same generation of [PagingData].
127      *
128      * [LoadState.Error] can be generated from two types of load requests:
129      * * [PagingSource.load] returning [PagingSource.LoadResult.Error]
130      * * [RemoteMediator.load] returning [RemoteMediator.MediatorResult.Error]
131      */
retrynull132     fun retry() {
133         pagingDataPresenter.retry()
134     }
135 
136     /**
137      * Refresh the data presented by this [LazyPagingItems].
138      *
139      * [refresh] triggers the creation of a new [PagingData] with a new instance of [PagingSource]
140      * to represent an updated snapshot of the backing dataset. If a [RemoteMediator] is set,
141      * calling [refresh] will also trigger a call to [RemoteMediator.load] with [LoadType] [REFRESH]
142      * to allow [RemoteMediator] to check for updates to the dataset backing [PagingSource].
143      *
144      * Note: This API is intended for UI-driven refresh signals, such as swipe-to-refresh.
145      * Invalidation due repository-layer signals, such as DB-updates, should instead use
146      * [PagingSource.invalidate].
147      *
148      * @see PagingSource.invalidate
149      */
refreshnull150     fun refresh() {
151         pagingDataPresenter.refresh()
152     }
153 
154     /** A [CombinedLoadStates] object which represents the current loading state. */
155     public var loadState: CombinedLoadStates by
156         mutableStateOf(
157             pagingDataPresenter.loadStateFlow.value
158                 ?: CombinedLoadStates(
159                     refresh = InitialLoadStates.refresh,
160                     prepend = InitialLoadStates.prepend,
161                     append = InitialLoadStates.append,
162                     source = InitialLoadStates
163                 )
164         )
165         private set
166 
collectLoadStatenull167     internal suspend fun collectLoadState() {
168         pagingDataPresenter.loadStateFlow.filterNotNull().collect { loadState = it }
169     }
170 
collectPagingDatanull171     internal suspend fun collectPagingData() {
172         flow.collectLatest { pagingDataPresenter.collectFrom(it) }
173     }
174 }
175 
176 private val IncompleteLoadState = LoadState.NotLoading(false)
177 private val InitialLoadStates =
178     LoadStates(LoadState.Loading, IncompleteLoadState, IncompleteLoadState)
179 
180 /**
181  * Collects values from this [Flow] of [PagingData] and represents them inside a [LazyPagingItems]
182  * instance. The [LazyPagingItems] instance can be used for lazy foundations such as
183  * [LazyListScope.items] in order to display the data obtained from a [Flow] of [PagingData].
184  *
185  * @sample androidx.paging.compose.samples.PagingBackendSample
186  * @param context the [CoroutineContext] to perform the collection of [PagingData] and
187  *   [CombinedLoadStates].
188  */
189 @Composable
collectAsLazyPagingItemsnull190 public fun <T : Any> Flow<PagingData<T>>.collectAsLazyPagingItems(
191     context: CoroutineContext = EmptyCoroutineContext
192 ): LazyPagingItems<T> {
193 
194     val lazyPagingItems = remember(this) { LazyPagingItems(this) }
195 
196     LaunchedEffect(lazyPagingItems) {
197         if (context == EmptyCoroutineContext) {
198             lazyPagingItems.collectPagingData()
199         } else {
200             withContext(context) { lazyPagingItems.collectPagingData() }
201         }
202     }
203 
204     LaunchedEffect(lazyPagingItems) {
205         if (context == EmptyCoroutineContext) {
206             lazyPagingItems.collectLoadState()
207         } else {
208             withContext(context) { lazyPagingItems.collectLoadState() }
209         }
210     }
211 
212     return lazyPagingItems
213 }
214