1 /*
2  * 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.LoadState.Loading
20 import androidx.paging.LoadState.NotLoading
21 import androidx.paging.PagingSource.LoadParams
22 import androidx.paging.internal.AtomicBoolean
23 import kotlinx.coroutines.CoroutineDispatcher
24 import kotlinx.coroutines.CoroutineScope
25 import kotlinx.coroutines.launch
26 
27 internal class LegacyPageFetcher<K : Any, V : Any>(
28     private val pagedListScope: CoroutineScope,
29     @Suppress("DEPRECATION") val config: PagedList.Config,
30     val source: PagingSource<K, V>,
31     private val notifyDispatcher: CoroutineDispatcher,
32     private val fetchDispatcher: CoroutineDispatcher,
33     val pageConsumer: PageConsumer<V>,
34     private val keyProvider: KeyProvider<K>
35 ) {
36     private val detached = AtomicBoolean(false)
37 
38     @Suppress("DEPRECATION")
39     var loadStateManager =
40         object : PagedList.LoadStateManager() {
onStateChangednull41             override fun onStateChanged(type: LoadType, state: LoadState) {
42                 // Don't need to post - PagedList will already have done that
43                 pageConsumer.onStateChanged(type, state)
44             }
45         }
46 
47     val isDetached
48         get() = detached.get()
49 
scheduleLoadnull50     private fun scheduleLoad(type: LoadType, params: LoadParams<K>) {
51         // Listen on the BG thread if the paged source is invalid, since it can be expensive.
52         pagedListScope.launch(fetchDispatcher) {
53             val value = source.load(params)
54 
55             // if invalid, drop result on the floor
56             if (source.invalid) {
57                 detach()
58                 return@launch
59             }
60 
61             // Source has been verified to be valid after producing data, so sent data to UI
62             launch(notifyDispatcher) {
63                 when (value) {
64                     is PagingSource.LoadResult.Page -> onLoadSuccess(type, value)
65                     is PagingSource.LoadResult.Error -> onLoadError(type, value.throwable)
66                     is PagingSource.LoadResult.Invalid -> onLoadInvalid()
67                 }
68             }
69         }
70     }
71 
onLoadSuccessnull72     private fun onLoadSuccess(type: LoadType, value: PagingSource.LoadResult.Page<K, V>) {
73         if (isDetached) return // abort!
74 
75         if (pageConsumer.onPageResult(type, value)) {
76             when (type) {
77                 LoadType.PREPEND -> schedulePrepend()
78                 LoadType.APPEND -> scheduleAppend()
79                 else -> throw IllegalStateException("Can only fetch more during append/prepend")
80             }
81         } else {
82             loadStateManager.setState(
83                 type,
84                 if (value.data.isEmpty()) NotLoading.Complete else NotLoading.Incomplete
85             )
86         }
87     }
88 
onLoadErrornull89     private fun onLoadError(type: LoadType, throwable: Throwable) {
90         if (isDetached) return // abort!
91 
92         val state = LoadState.Error(throwable)
93         loadStateManager.setState(type, state)
94     }
95 
onLoadInvalidnull96     private fun onLoadInvalid() {
97         source.invalidate()
98         detach()
99     }
100 
trySchedulePrependnull101     fun trySchedulePrepend() {
102         val startState = loadStateManager.startState
103         if (startState is NotLoading && !startState.endOfPaginationReached) {
104             schedulePrepend()
105         }
106     }
107 
tryScheduleAppendnull108     fun tryScheduleAppend() {
109         val endState = loadStateManager.endState
110         if (endState is NotLoading && !endState.endOfPaginationReached) {
111             scheduleAppend()
112         }
113     }
114 
schedulePrependnull115     private fun schedulePrepend() {
116         val key = keyProvider.prevKey
117         if (key == null) {
118             onLoadSuccess(LoadType.PREPEND, PagingSource.LoadResult.Page.empty())
119             return
120         }
121 
122         loadStateManager.setState(LoadType.PREPEND, Loading)
123 
124         val loadParams =
125             LoadParams.Prepend(
126                 key,
127                 config.pageSize,
128                 config.enablePlaceholders,
129             )
130         scheduleLoad(LoadType.PREPEND, loadParams)
131     }
132 
scheduleAppendnull133     private fun scheduleAppend() {
134         val key = keyProvider.nextKey
135         if (key == null) {
136             onLoadSuccess(LoadType.APPEND, PagingSource.LoadResult.Page.empty())
137             return
138         }
139 
140         loadStateManager.setState(LoadType.APPEND, Loading)
141         val loadParams =
142             LoadParams.Append(
143                 key,
144                 config.pageSize,
145                 config.enablePlaceholders,
146             )
147         scheduleLoad(LoadType.APPEND, loadParams)
148     }
149 
retrynull150     fun retry() {
151         loadStateManager.startState.run { if (this is LoadState.Error) schedulePrepend() }
152         loadStateManager.endState.run { if (this is LoadState.Error) scheduleAppend() }
153     }
154 
detachnull155     fun detach() {
156         detached.set(true)
157     }
158 
159     internal interface PageConsumer<V : Any> {
160         /** @return `true` if we need to fetch more */
onPageResultnull161         fun onPageResult(type: LoadType, page: PagingSource.LoadResult.Page<*, V>): Boolean
162 
163         fun onStateChanged(type: LoadType, state: LoadState)
164     }
165 
166     internal interface KeyProvider<K : Any> {
167         val prevKey: K?
168         val nextKey: K?
169     }
170 }
171