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