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