Skip to content
This repository has been archived by the owner on Nov 12, 2024. It is now read-only.

Commit

Permalink
Add retained caching for paging flows (#1763)
Browse files Browse the repository at this point in the history
We now use a `rememberRetainedCoroutineScope` to keep a CoroutineScope
running in a retained state. This allows to then keep a retained
`cachedIn` paging flow running too.
  • Loading branch information
chrisbanes authored Apr 29, 2024
1 parent 1ef8d6f commit 0c740e5
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
package app.tivi.common.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import app.cash.paging.CombinedLoadStates
import app.cash.paging.LoadStateError
import app.cash.paging.PagingData
import app.cash.paging.cachedIn
import com.slack.circuit.retained.rememberRetained
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow

Expand All @@ -30,6 +30,6 @@ fun CombinedLoadStates.refreshErrorOrNull(): UiMessage? {
}

@Composable
inline fun <T : Any> Flow<PagingData<T>>.rememberCachedPagingFlow(
scope: CoroutineScope = rememberCoroutineScope(),
): Flow<PagingData<T>> = remember(this, scope) { cachedIn(scope) }
inline fun <T : Any> Flow<PagingData<T>>.rememberRetainedCachedPagingFlow(
scope: CoroutineScope = rememberRetainedCoroutineScope(),
): Flow<PagingData<T>> = rememberRetained(this, scope) { cachedIn(scope) }
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
package app.tivi.common.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import com.slack.circuit.retained.rememberRetained
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel

/**
* Returns a [StableCoroutineScope] around a [androidx.compose.runtime.rememberCoroutineScope].
Expand All @@ -22,3 +27,20 @@ fun rememberCoroutineScope(): StableCoroutineScope {
/** @see rememberCoroutineScope */
@Stable
class StableCoroutineScope(scope: CoroutineScope) : CoroutineScope by scope

@Composable
fun rememberRetainedCoroutineScope(): StableCoroutineScope {
return rememberRetained("coroutine_scope") {
object : RememberObserver {
val scope = StableCoroutineScope(CoroutineScope(Dispatchers.Main + Job()))

override fun onAbandoned() = onForgotten()

override fun onForgotten() {
scope.cancel()
}

override fun onRemembered() = Unit
}
}.scope
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import app.cash.paging.PagingConfig
import app.cash.paging.compose.collectAsLazyPagingItems
import app.tivi.common.compose.UiMessage
import app.tivi.common.compose.UiMessageManager
import app.tivi.common.compose.rememberCachedPagingFlow
import app.tivi.common.compose.rememberCoroutineScope
import app.tivi.common.compose.rememberRetainedCachedPagingFlow
import app.tivi.data.models.SortOption
import app.tivi.data.traktauth.TraktAuthState
import app.tivi.domain.interactors.GetTraktAuthState
Expand All @@ -31,6 +31,7 @@ import app.tivi.settings.TiviPreferences
import app.tivi.util.Logger
import app.tivi.util.onException
import com.slack.circuit.retained.collectAsRetainedState
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
Expand Down Expand Up @@ -71,8 +72,13 @@ class LibraryPresenter(
val scope = rememberCoroutineScope()
val uiMessageManager = remember { UiMessageManager() }

val items = observePagedLibraryShows.value.flow
.rememberCachedPagingFlow(scope)
// Yes, this is gross. We need the same flow instance across Presenter instances. We could
// make the interactor have @ApplicationScope, but that has other consequences if we use the
// same interactor at the same time across UIs. Instead we just retain the instance
val retainedObservePagedLibraryShows = rememberRetained { observePagedLibraryShows.value }

val items = retainedObservePagedLibraryShows.flow
.rememberRetainedCachedPagingFlow()
.collectAsLazyPagingItems()

var filter by remember { mutableStateOf<String?>(null) }
Expand Down Expand Up @@ -144,7 +150,7 @@ class LibraryPresenter(

LaunchedEffect(filter, sort, includeFollowedShows, includeWatchedShows) {
// When the filter and sort options change, update the data source
observePagedLibraryShows.value.invoke(
retainedObservePagedLibraryShows(
ObservePagedLibraryShows.Parameters(
sort = sort,
filter = filter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import app.cash.paging.PagingConfig
import app.cash.paging.compose.collectAsLazyPagingItems
import app.tivi.common.compose.rememberCachedPagingFlow
import app.tivi.common.compose.rememberRetainedCachedPagingFlow
import app.tivi.domain.observers.ObservePagedPopularShows
import app.tivi.screens.PopularShowsScreen
import app.tivi.screens.ShowDetailsScreen
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
Expand Down Expand Up @@ -40,12 +41,17 @@ class PopularShowsPresenter(

@Composable
override fun present(): PopularShowsUiState {
val items = pagingInteractor.value.flow
.rememberCachedPagingFlow()
// Yes, this is gross. We need the same flow instance across Presenter instances. We could
// make the interactor have @ApplicationScope, but that has other consequences if we use the
// same interactor at the same time across UIs. Instead we just retain the instance
val retainedPagingInteractor = rememberRetained { pagingInteractor.value }

val items = retainedPagingInteractor.flow
.rememberRetainedCachedPagingFlow()
.collectAsLazyPagingItems()

LaunchedEffect(Unit) {
pagingInteractor.value.invoke(ObservePagedPopularShows.Params(PAGING_CONFIG))
retainedPagingInteractor(ObservePagedPopularShows.Params(PAGING_CONFIG))
}

fun eventSink(event: PopularShowsUiEvent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import app.cash.paging.PagingConfig
import app.cash.paging.compose.collectAsLazyPagingItems
import app.tivi.common.compose.rememberCachedPagingFlow
import app.tivi.common.compose.rememberRetainedCachedPagingFlow
import app.tivi.domain.observers.ObservePagedRecommendedShows
import app.tivi.screens.RecommendedShowsScreen
import app.tivi.screens.ShowDetailsScreen
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
Expand Down Expand Up @@ -40,12 +41,17 @@ class RecommendedShowsPresenter(

@Composable
override fun present(): RecommendedShowsUiState {
val items = pagingInteractor.value.flow
.rememberCachedPagingFlow()
// Yes, this is gross. We need the same flow instance across Presenter instances. We could
// make the interactor have @ApplicationScope, but that has other consequences if we use the
// same interactor at the same time across UIs. Instead we just retain the instance
val retainedPagingInteractor = rememberRetained { pagingInteractor.value }

val items = retainedPagingInteractor.flow
.rememberRetainedCachedPagingFlow()
.collectAsLazyPagingItems()

LaunchedEffect(Unit) {
pagingInteractor.value.invoke(ObservePagedRecommendedShows.Params(PAGING_CONFIG))
retainedPagingInteractor(ObservePagedRecommendedShows.Params(PAGING_CONFIG))
}

fun eventSink(event: RecommendedShowsUiEvent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import app.cash.paging.PagingConfig
import app.cash.paging.compose.collectAsLazyPagingItems
import app.tivi.common.compose.rememberCachedPagingFlow
import app.tivi.common.compose.rememberRetainedCachedPagingFlow
import app.tivi.domain.observers.ObservePagedTrendingShows
import app.tivi.screens.ShowDetailsScreen
import app.tivi.screens.TrendingShowsScreen
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
Expand Down Expand Up @@ -40,12 +41,17 @@ class TrendingShowsPresenter(

@Composable
override fun present(): TrendingShowsUiState {
val items = pagingInteractor.value.flow
.rememberCachedPagingFlow()
// Yes, this is gross. We need the same flow instance across Presenter instances. We could
// make the interactor have @ApplicationScope, but that has other consequences if we use the
// same interactor at the same time across UIs. Instead we just retain the instance
val retainedPagingInteractor = rememberRetained { pagingInteractor.value }

val items = retainedPagingInteractor.flow
.rememberRetainedCachedPagingFlow()
.collectAsLazyPagingItems()

LaunchedEffect(Unit) {
pagingInteractor.value.invoke(ObservePagedTrendingShows.Params(PAGING_CONFIG))
retainedPagingInteractor(ObservePagedTrendingShows.Params(PAGING_CONFIG))
}

fun eventSink(event: TrendingShowsUiEvent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import app.cash.paging.PagingConfig
import app.cash.paging.compose.collectAsLazyPagingItems
import app.tivi.common.compose.UiMessage
import app.tivi.common.compose.UiMessageManager
import app.tivi.common.compose.rememberCachedPagingFlow
import app.tivi.common.compose.rememberCoroutineScope
import app.tivi.common.compose.rememberRetainedCachedPagingFlow
import app.tivi.data.models.SortOption
import app.tivi.data.traktauth.TraktAuthState
import app.tivi.domain.interactors.GetTraktAuthState
Expand All @@ -30,6 +30,7 @@ import app.tivi.settings.TiviPreferences
import app.tivi.util.Logger
import app.tivi.util.onException
import com.slack.circuit.retained.collectAsRetainedState
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
Expand Down Expand Up @@ -71,8 +72,13 @@ class UpNextPresenter(

val uiMessageManager = remember { UiMessageManager() }

val items = observePagedUpNextShows.value.flow
.rememberCachedPagingFlow(scope)
// Yes, this is gross. We need the same flow instance across Presenter instances. We could
// make the interactor have @ApplicationScope, but that has other consequences if we use the
// same interactor at the same time across UIs. Instead we just retain the instance
val retainedObservePagedUpNextShows = rememberRetained { observePagedUpNextShows.value }

val items = retainedObservePagedUpNextShows.flow
.rememberRetainedCachedPagingFlow()
.collectAsLazyPagingItems()

var sort by remember { mutableStateOf(SortOption.LAST_WATCHED) }
Expand Down Expand Up @@ -131,7 +137,7 @@ class UpNextPresenter(

LaunchedEffect(sort, followedShowsOnly) {
// When the filter and sort options change, update the data source
observePagedUpNextShows.value.invoke(
retainedObservePagedUpNextShows(
ObservePagedUpNextShows.Parameters(
sort = sort,
followedOnly = followedShowsOnly,
Expand Down

0 comments on commit 0c740e5

Please sign in to comment.