From 95d22727b4f72cdb57b9524fcf1547192f9d7a7d Mon Sep 17 00:00:00 2001 From: Dustin Lam Date: Wed, 21 Apr 2021 21:26:28 -0700 Subject: [PATCH] Add a sample showing how to synchronously await for remote loads to be applied --- .../reddit/ui/RedditActivity.kt | 12 +- .../reddit/paging/CombinedLoadStatesHelper.kt | 141 ++++++++++++++++++ 2 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 PagingWithNetworkSample/lib/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/paging/CombinedLoadStatesHelper.kt diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivity.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivity.kt index 31cae63..8452b6c 100644 --- a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivity.kt +++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivity.kt @@ -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.asHelperStates import com.android.example.paging.pagingwithnetwork.reddit.repository.RedditPostRepository import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest @@ -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 } } @@ -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. + .asHelperStates() + // 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) } } } diff --git a/PagingWithNetworkSample/lib/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/paging/CombinedLoadStatesHelper.kt b/PagingWithNetworkSample/lib/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/paging/CombinedLoadStatesHelper.kt new file mode 100644 index 0000000..fbc372b --- /dev/null +++ b/PagingWithNetworkSample/lib/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/paging/CombinedLoadStatesHelper.kt @@ -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.HelperState.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.scan +import kotlin.Error + +/** + * 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. + */ +class CombinedLoadStatesHelper { + 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: HelperState = NOT_LOADING + private var prependState: HelperState = NOT_LOADING + private var appendState: HelperState = NOT_LOADING + + fun toLoadStates() = LoadStates( + refresh = refresh, + prepend = prepend, + append = append + ) + + internal fun updateFromCombinedLoadStates(combinedLoadStates: CombinedLoadStates) { + computeHelperStates( + sourceRefreshState = combinedLoadStates.source.refresh, + sourceState = combinedLoadStates.source.refresh, + remoteState = combinedLoadStates.mediator?.refresh, + helperState = refreshState, + ).also { + refresh = it.first + refreshState = it.second + } + computeHelperStates( + sourceRefreshState = combinedLoadStates.source.refresh, + sourceState = combinedLoadStates.source.prepend, + remoteState = combinedLoadStates.mediator?.prepend, + helperState = prependState, + ).also { + prepend = it.first + prependState = it.second + } + computeHelperStates( + sourceRefreshState = combinedLoadStates.source.refresh, + sourceState = combinedLoadStates.source.append, + remoteState = combinedLoadStates.mediator?.append, + helperState = appendState, + ).also { + append = it.first + appendState = it.second + } + } + + private fun computeHelperStates( + sourceRefreshState: LoadState, + sourceState: LoadState, + remoteState: LoadState?, + helperState: HelperState, + ): Pair { + if (remoteState == null) return sourceState to NOT_LOADING + + return when (helperState) { + 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.asHelperStates(): Flow { + val helper = CombinedLoadStatesHelper() + return scan(helper.toLoadStates()) { _, combinedLoadStates -> + helper.updateFromCombinedLoadStates(combinedLoadStates) + helper.toLoadStates() + } +} + +/** + * State machine used to compute [LoadState] values in [CombinedLoadStatesHelper]. + */ +enum class HelperState { + /** + * 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, +} -- GitLab