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