Skip to content
This repository was archived by the owner on Jan 10, 2025. It is now read-only.

Add a sample showing how to synchronously await for remote loads to be applied #987

Merged
merged 4 commits into from
May 21, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import androidx.paging.LoadState
import com.android.example.paging.pagingwithnetwork.GlideApp
import com.android.example.paging.pagingwithnetwork.databinding.ActivityRedditBinding
import com.android.example.paging.pagingwithnetwork.reddit.ServiceLocator
import com.android.example.paging.pagingwithnetwork.reddit.paging.asSynchronousRemoteStates
import com.android.example.paging.pagingwithnetwork.reddit.repository.RedditPostRepository
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
Expand Down Expand Up @@ -85,7 +86,7 @@ class RedditActivity : AppCompatActivity() {

lifecycleScope.launchWhenCreated {
adapter.loadStateFlow.collectLatest { loadStates ->
binding.swipeRefresh.isRefreshing = loadStates.refresh is LoadState.Loading
binding.swipeRefresh.isRefreshing = loadStates.mediator?.refresh is LoadState.Loading
}
}

Expand All @@ -97,10 +98,15 @@ class RedditActivity : AppCompatActivity() {

lifecycleScope.launchWhenCreated {
adapter.loadStateFlow
// Only emit when REFRESH LoadState for RemoteMediator changes.
// Use a state-machine to track LoadStates such that we only transition to
// NotLoading from a RemoteMediator load if it was also presented to UI.
.asSynchronousRemoteStates()
// Only emit when REFRESH changes, as we only want to react on loads replacing the
// list.
.distinctUntilChangedBy { it.refresh }
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
// Only react to cases where REFRESH completes i.e., NotLoading.
.filter { it.refresh is LoadState.NotLoading }
// Scroll to top is synchronous with UI updates, even if remote load was triggered.
.collect { binding.list.scrollToPosition(0) }
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.android.example.paging.pagingwithnetwork.reddit.paging

import androidx.paging.CombinedLoadStates
import androidx.paging.LoadState
import androidx.paging.LoadState.*
import androidx.paging.LoadStates
import com.android.example.paging.pagingwithnetwork.reddit.paging.SynchronousRemoteState.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.scan
import kotlin.Error

/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you have tests from when you were experimenting with this in the paging codebase?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do but it might take some time to adapt: https://android-review.googlesource.com/c/platform/frameworks/support/+/1591652/7/paging/common/src/main/kotlin/androidx/paging/MutableLoadStateCollection.kt

Let me leave this as a follow-up? I have a couple things on the queue I should probably do before migrating those tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM

* Track the combined [LoadState] of [RemoteMediator] and [PagingSource], so that each load type
* is only set to [NotLoading] when [RemoteMediator] load is applied on presenter-side.
*/
private class SynchronousRemoteStates {
var refresh: LoadState = NotLoading(endOfPaginationReached = false)
private set
var prepend: LoadState = NotLoading(endOfPaginationReached = false)
private set
var append: LoadState = NotLoading(endOfPaginationReached = false)
private set
private var refreshState: SynchronousRemoteState = NOT_LOADING
private var prependState: SynchronousRemoteState = NOT_LOADING
private var appendState: SynchronousRemoteState = NOT_LOADING

fun toLoadStates() = LoadStates(
refresh = refresh,
prepend = prepend,
append = append
)

internal fun updateFromCombinedLoadStates(combinedLoadStates: CombinedLoadStates) {
computeSynchronousRemoteStates(
sourceRefreshState = combinedLoadStates.source.refresh,
sourceState = combinedLoadStates.source.refresh,
remoteState = combinedLoadStates.mediator?.refresh,
synchronousRemoteState = refreshState,
).also {
refresh = it.first
refreshState = it.second
}
computeSynchronousRemoteStates(
sourceRefreshState = combinedLoadStates.source.refresh,
sourceState = combinedLoadStates.source.prepend,
remoteState = combinedLoadStates.mediator?.prepend,
synchronousRemoteState = prependState,
).also {
prepend = it.first
prependState = it.second
}
computeSynchronousRemoteStates(
sourceRefreshState = combinedLoadStates.source.refresh,
sourceState = combinedLoadStates.source.append,
remoteState = combinedLoadStates.mediator?.append,
synchronousRemoteState = appendState,
).also {
append = it.first
appendState = it.second
}
}

private fun computeSynchronousRemoteStates(
sourceRefreshState: LoadState,
sourceState: LoadState,
remoteState: LoadState?,
synchronousRemoteState: SynchronousRemoteState,
): Pair<LoadState, SynchronousRemoteState> {
if (remoteState == null) return sourceState to NOT_LOADING

return when (synchronousRemoteState) {
NOT_LOADING -> when (remoteState) {
is Loading -> Loading to REMOTE_STARTED
is Error -> remoteState to REMOTE_ERROR
else -> NotLoading(remoteState.endOfPaginationReached) to NOT_LOADING
}
REMOTE_STARTED -> when {
remoteState is Error -> remoteState to REMOTE_ERROR
sourceRefreshState is Loading -> Loading to SOURCE_LOADING
else -> Loading to REMOTE_STARTED
}
REMOTE_ERROR -> when (remoteState) {
is Error -> remoteState to REMOTE_ERROR
else -> Loading to REMOTE_STARTED
}
SOURCE_LOADING -> when {
sourceRefreshState is Error -> sourceRefreshState to SOURCE_ERROR
remoteState is Error -> remoteState to REMOTE_ERROR
sourceRefreshState is NotLoading -> {
NotLoading(remoteState.endOfPaginationReached) to NOT_LOADING
}
else -> Loading to SOURCE_LOADING
}
SOURCE_ERROR -> when (sourceRefreshState) {
is Error -> sourceRefreshState to SOURCE_ERROR
else -> sourceRefreshState to SOURCE_LOADING
}
}
}
}

@OptIn(ExperimentalCoroutinesApi::class)
fun Flow<CombinedLoadStates>.asSynchronousRemoteStates(): Flow<LoadStates> {
val syncRemoteState = SynchronousRemoteStates()
return scan(syncRemoteState.toLoadStates()) { _, combinedLoadStates ->
syncRemoteState.updateFromCombinedLoadStates(combinedLoadStates)
syncRemoteState.toLoadStates()
}
}

/**
* State machine used to compute [LoadState] values in [SynchronousRemoteStates].
*/
enum class SynchronousRemoteState {
/**
* Idle state; defer to remote state for endOfPaginationReached.
*/
NOT_LOADING,

/**
* Remote load triggered; start listening for source refresh.
*/
REMOTE_STARTED,

/**
* Waiting for remote in error state to get retried
*/
REMOTE_ERROR,

/**
* Source refresh triggered by remote invalidation, once this completes we can be sure
* the next generation was loaded.
*/
SOURCE_LOADING,

/**
* Remote load completed, but waiting for source refresh in error state to get retried.
*/
SOURCE_ERROR,
}