1 /* <lambda>null2 * 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 18 19 import androidx.paging.LoadState.Error 20 import androidx.paging.LoadState.Loading 21 import androidx.paging.LoadState.NotLoading 22 import androidx.paging.internal.CopyOnWriteArrayList 23 import kotlinx.coroutines.flow.MutableStateFlow 24 import kotlinx.coroutines.flow.StateFlow 25 import kotlinx.coroutines.flow.asStateFlow 26 import kotlinx.coroutines.flow.update 27 28 /** 29 * Helper to construct [CombinedLoadStates] that accounts for previous state to set the convenience 30 * properties correctly. 31 * 32 * This class exposes a [StateFlow] and handles dispatches to tracked [listeners] intended for use 33 * with presenter APIs, which has the nuance of filtering out the initial value and dispatching to 34 * listeners immediately as they get added. 35 */ 36 internal class MutableCombinedLoadStateCollection { 37 38 private val listeners = CopyOnWriteArrayList<(CombinedLoadStates) -> Unit>() 39 private val _stateFlow = MutableStateFlow<CombinedLoadStates?>(null) 40 public val stateFlow = _stateFlow.asStateFlow() 41 42 // load states are de-duplicated 43 fun set(sourceLoadStates: LoadStates, remoteLoadStates: LoadStates?) = 44 dispatchNewState { currState -> 45 computeNewState(currState, sourceLoadStates, remoteLoadStates) 46 } 47 48 // load states are de-duplicated 49 fun set(type: LoadType, remote: Boolean, state: LoadState) = dispatchNewState { currState -> 50 var source = currState?.source ?: LoadStates.IDLE 51 var mediator = currState?.mediator 52 53 if (remote) { 54 mediator = LoadStates.IDLE.modifyState(type, state) 55 } else { 56 source = source.modifyState(type, state) 57 } 58 computeNewState(currState, source, mediator) 59 } 60 61 fun get(type: LoadType, remote: Boolean): LoadState? { 62 val state = _stateFlow.value 63 return (if (remote) state?.mediator else state?.source)?.get(type) 64 } 65 66 /** 67 * When a new listener is added, it will be immediately called with the current 68 * [CombinedLoadStates] unless no state has been set yet, and thus has no valid state to emit. 69 */ 70 fun addListener(listener: (CombinedLoadStates) -> Unit) { 71 // Note: Important to add the listener first before sending off events, in case the 72 // callback triggers removal, which could lead to a leak if the listener is added 73 // afterwards. 74 listeners.add(listener) 75 _stateFlow.value?.also { listener(it) } 76 } 77 78 fun removeListener(listener: (CombinedLoadStates) -> Unit) { 79 listeners.remove(listener) 80 } 81 82 /** 83 * Computes and dispatches the new CombinedLoadStates. No-op if new value is same as previous 84 * value. 85 * 86 * We manually de-duplicate emissions to StateFlow and to listeners even though 87 * [MutableStateFlow.update] de-duplicates automatically in that duplicated values are set but 88 * not sent to collectors. However it doesn't indicate whether the new value is indeed a 89 * duplicate or not, so we still need to manually compare previous/updated values before sending 90 * to listeners. Because of that, we manually de-dupe both stateFlow and listener emissions to 91 * ensure they are in sync. 92 */ 93 private fun dispatchNewState( 94 computeNewState: (currState: CombinedLoadStates?) -> CombinedLoadStates 95 ) { 96 var newState: CombinedLoadStates? = null 97 _stateFlow.update { currState -> 98 val computed = computeNewState(currState) 99 if (currState != computed) { 100 newState = computed 101 computed 102 } else { 103 // no-op, doesn't dispatch 104 return 105 } 106 } 107 newState?.apply { listeners.forEach { it(this) } } 108 } 109 110 private fun computeNewState( 111 previousState: CombinedLoadStates?, 112 newSource: LoadStates, 113 newRemote: LoadStates? 114 ): CombinedLoadStates { 115 val refresh = 116 computeHelperState( 117 previousState = previousState?.refresh ?: NotLoading.Incomplete, 118 sourceRefreshState = newSource.refresh, 119 sourceState = newSource.refresh, 120 remoteState = newRemote?.refresh 121 ) 122 val prepend = 123 computeHelperState( 124 previousState = previousState?.prepend ?: NotLoading.Incomplete, 125 sourceRefreshState = newSource.refresh, 126 sourceState = newSource.prepend, 127 remoteState = newRemote?.prepend 128 ) 129 val append = 130 computeHelperState( 131 previousState = previousState?.append ?: NotLoading.Incomplete, 132 sourceRefreshState = newSource.refresh, 133 sourceState = newSource.append, 134 remoteState = newRemote?.append 135 ) 136 137 return CombinedLoadStates( 138 refresh = refresh, 139 prepend = prepend, 140 append = append, 141 source = newSource, 142 mediator = newRemote, 143 ) 144 } 145 146 /** 147 * Computes the next value for the convenience helpers in [CombinedLoadStates], which generally 148 * defers to remote state, but waits for both source and remote states to become [NotLoading] 149 * before moving to that state. This provides a reasonable default for the common use-case where 150 * you generally want to wait for both RemoteMediator to return and for the update to get 151 * applied before signaling to UI that a network fetch has "finished". 152 */ 153 private fun computeHelperState( 154 previousState: LoadState, 155 sourceRefreshState: LoadState, 156 sourceState: LoadState, 157 remoteState: LoadState? 158 ): LoadState { 159 if (remoteState == null) return sourceState 160 161 return when (previousState) { 162 is Loading -> 163 when { 164 sourceRefreshState is NotLoading && remoteState is NotLoading -> remoteState 165 remoteState is Error -> remoteState 166 else -> previousState 167 } 168 else -> remoteState 169 } 170 } 171 } 172