1
1
package com.android.example.paging.pagingwithnetwork.reddit.paging
2
2
3
+ import androidx.annotation.VisibleForTesting
3
4
import androidx.paging.CombinedLoadStates
4
5
import androidx.paging.LoadState
5
- import androidx.paging.LoadState.*
6
+ import androidx.paging.LoadState.NotLoading
7
+ import androidx.paging.LoadState.Loading
6
8
import androidx.paging.LoadStates
7
- import com.android.example.paging.pagingwithnetwork.reddit.paging.MergedState.*
8
9
import kotlinx.coroutines.ExperimentalCoroutinesApi
9
10
import kotlinx.coroutines.flow.Flow
10
11
import kotlinx.coroutines.flow.scan
11
12
import kotlin.Error
13
+ import androidx.paging.PagingDataAdapter
14
+ import androidx.paging.RemoteMediator
15
+ import androidx.paging.PagingSource
16
+ import androidx.paging.LoadType.REFRESH
17
+ import androidx.paging.LoadType
18
+ import com.android.example.paging.pagingwithnetwork.reddit.paging.MergedState.NOT_LOADING
19
+ import com.android.example.paging.pagingwithnetwork.reddit.paging.MergedState.REMOTE_STARTED
20
+ import com.android.example.paging.pagingwithnetwork.reddit.paging.MergedState.REMOTE_ERROR
21
+ import com.android.example.paging.pagingwithnetwork.reddit.paging.MergedState.SOURCE_ERROR
22
+ import com.android.example.paging.pagingwithnetwork.reddit.paging.MergedState.SOURCE_LOADING
12
23
24
+ /* *
25
+ * Converts the raw [CombinedLoadStates] [Flow] from [PagingDataAdapter.loadStateFlow] into a new
26
+ * [Flow] of [CombinedLoadStates] that track [CombinedLoadStates.mediator] states as they are
27
+ * synchronously applied in the UI. Any [Loading] state triggered by [RemoteMediator] will only
28
+ * transition back to [NotLoading] after the fetched items have been synchronously shown in UI by a
29
+ * successful [PagingSource] load of type [REFRESH].
30
+ *
31
+ * Note: This class assumes that the [RemoteMediator] implementation always invalidates
32
+ * [PagingSource] on a successful fetch, even if no data was modified (which Room does by default).
33
+ * Using this class without this guarantee can cause [LoadState] to get indefinitely stuck as
34
+ * [Loading] in cases where invalidation doesn't happen because the fetched network data represents
35
+ * exactly what is already cached in DB.
36
+ */
13
37
@OptIn(ExperimentalCoroutinesApi ::class )
14
38
fun Flow<CombinedLoadStates>.asMergedLoadStates (): Flow <LoadStates > {
15
39
val syncRemoteState = LoadStatesMerger ()
@@ -30,18 +54,25 @@ private class LoadStatesMerger {
30
54
private set
31
55
var append: LoadState = NotLoading (endOfPaginationReached = false )
32
56
private set
33
- private var refreshState: MergedState = NOT_LOADING
34
- private var prependState: MergedState = NOT_LOADING
35
- private var appendState: MergedState = NOT_LOADING
57
+ var refreshState: MergedState = NOT_LOADING
58
+ private set
59
+ var prependState: MergedState = NOT_LOADING
60
+ private set
61
+ var appendState: MergedState = NOT_LOADING
62
+ private set
36
63
37
64
fun toLoadStates () = LoadStates (
38
65
refresh = refresh,
39
66
prepend = prepend,
40
67
append = append
41
68
)
42
69
43
- internal fun updateFromCombinedLoadStates (combinedLoadStates : CombinedLoadStates ) {
44
- computeSynchronousRemoteStates(
70
+ /* *
71
+ * For every new emission of [CombinedLoadStates] from the original [Flow], update the
72
+ * [MergedState] of each [LoadType] and compute the new [LoadState].
73
+ */
74
+ fun updateFromCombinedLoadStates (combinedLoadStates : CombinedLoadStates ) {
75
+ computeNextLoadStateAndMergedState(
45
76
sourceRefreshState = combinedLoadStates.source.refresh,
46
77
sourceState = combinedLoadStates.source.refresh,
47
78
remoteState = combinedLoadStates.mediator?.refresh,
@@ -50,7 +81,7 @@ private class LoadStatesMerger {
50
81
refresh = it.first
51
82
refreshState = it.second
52
83
}
53
- computeSynchronousRemoteStates (
84
+ computeNextLoadStateAndMergedState (
54
85
sourceRefreshState = combinedLoadStates.source.refresh,
55
86
sourceState = combinedLoadStates.source.prepend,
56
87
remoteState = combinedLoadStates.mediator?.prepend,
@@ -59,7 +90,7 @@ private class LoadStatesMerger {
59
90
prepend = it.first
60
91
prependState = it.second
61
92
}
62
- computeSynchronousRemoteStates (
93
+ computeNextLoadStateAndMergedState (
63
94
sourceRefreshState = combinedLoadStates.source.refresh,
64
95
sourceState = combinedLoadStates.source.append,
65
96
remoteState = combinedLoadStates.mediator?.append,
@@ -70,7 +101,11 @@ private class LoadStatesMerger {
70
101
}
71
102
}
72
103
73
- private fun computeSynchronousRemoteStates (
104
+ /* *
105
+ * Compute which [LoadState] and [MergedState] to transition, given the previous and current
106
+ * state for a particular [LoadType].
107
+ */
108
+ private fun computeNextLoadStateAndMergedState (
74
109
sourceRefreshState : LoadState ,
75
110
sourceState : LoadState ,
76
111
remoteState : LoadState ? ,
@@ -111,6 +146,10 @@ private class LoadStatesMerger {
111
146
112
147
/* *
113
148
* State machine used to compute [LoadState] values in [LoadStatesMerger].
149
+ *
150
+ * This allows [LoadStatesMerger] to track whether to block transitioning to [NotLoading] from the
151
+ * [Loading] state if it was triggered by [RemoteMediator], until [PagingSource] invalidates and
152
+ * completes [REFRESH].
114
153
*/
115
154
private enum class MergedState {
116
155
/* *
0 commit comments