Skip to content

Commit

Permalink
Try out pausing presenters
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes committed May 22, 2024
1 parent dba5686 commit b18689a
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 171 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuit.foundation

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.CircuitUiState
import com.slack.circuit.runtime.InternalCircuitApi
Expand Down Expand Up @@ -139,7 +148,9 @@ public fun <UiState : CircuitUiState> CircuitContent(
onDispose { eventListener.onDisposePresent() }
}

val state = presenter.present()
val pauseablePresenter = remember(presenter) { presenter.toPauseablePresenter() }

val state = pauseablePresenter.present()

// TODO not sure why stateFlow + LaunchedEffect + distinctUntilChanged doesn't work here
SideEffect { eventListener.onState(state) }
Expand All @@ -148,7 +159,15 @@ public fun <UiState : CircuitUiState> CircuitContent(

onDispose { eventListener.onDisposeContent() }
}
ui.Content(state, modifier)

Box {
ui.Content(state, modifier)

if (LocalLifecycle.current.isPaused) {
// Just for debugging. Easier to see if presenters are enabled or not
Spacer(Modifier.matchParentSize().background(Color.Magenta.copy(alpha = 0.25f)))
}
}
}

/**
Expand Down Expand Up @@ -218,3 +237,17 @@ public inline fun rememberUi(
eventListener.onAfterCreateUi(screen, ui, context)
}
}

public interface Lifecycle {
public val isPaused: Boolean
}

internal class LifecycleImpl : Lifecycle {
internal var _isPaused by mutableStateOf(false)
override val isPaused: Boolean
get() = _isPaused
}

public val LocalLifecycle: ProvidableCompositionLocal<Lifecycle> = staticCompositionLocalOf {
error("")
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.currentCompositeKeyHash
import androidx.compose.runtime.getValue
Expand Down Expand Up @@ -58,7 +60,8 @@ public fun <R : Record> NavigableCircuitContent(
circuit.onUnavailableContent,
) {
val activeContentProviders =
backStack.buildCircuitContentProviders(
buildCircuitContentProviders(
backStack = backStack,
navigator = navigator,
circuit = circuit,
unavailableRoute = unavailableRoute,
Expand Down Expand Up @@ -101,14 +104,17 @@ public fun <R : Record> NavigableCircuitContent(
val outerKey = "_navigable_registry_${currentCompositeKeyHash.toString(MaxSupportedRadix)}"
val outerRegistry = rememberRetained(key = outerKey) { RetainedStateRegistry() }

println("Composing NavigableCircuitContent for ${backStack.toList()}")

CompositionLocalProvider(LocalRetainedStateRegistry provides outerRegistry) {
decoration.DecoratedContent(activeContentProviders, backStack.size, modifier) { provider ->
// We retain the record's retained state registry for as long as the back stack
// contains the record
println("--> NavigableCircuitContent content called for ${provider.record}")

val record = provider.record
val recordInBackStackRetainChecker =
remember(backStack, record) {
CanRetainChecker { backStack.containsRecord(record, includeSaved = true) }
.also { println("Creating CanRetainChecker for $record") }
}

CompositionLocalProvider(LocalCanRetainChecker provides recordInBackStackRetainChecker) {
Expand All @@ -122,15 +128,32 @@ public fun <R : Record> NavigableCircuitContent(
// maintain the lifetime), and the other provided values
val recordRetainedStateRegistry =
rememberRetained(key = record.registryKey) { RetainedStateRegistry() }

val lifecycle =
remember { LifecycleImpl().also { println("Created LifecycleImpl $it for $record") } }
.apply {
_isPaused = backStack.topRecord != record
println("Set LifecycleImpl.isPaused to ${_isPaused} for $record. $this")
}

CompositionLocalProvider(
LocalRetainedStateRegistry provides recordRetainedStateRegistry,
LocalCanRetainChecker provides CanRetainChecker.Always,
LocalBackStack provides backStack,
LocalLifecycle provides lifecycle,
*providedLocals,
) {
provider.content(record)
}
}

DisposableEffect(Unit) {
println("NavigableCircuitContent for ${provider.record} added")

onDispose { println("NavigableCircuitContent for ${provider.record} removed") }
}

SideEffect { println("--> NavigableCircuitContent content finished for ${provider.record}") }
}
}
}
Expand Down Expand Up @@ -163,7 +186,8 @@ public class RecordContentProvider<R : Record>(
}

@Composable
private fun <R : Record> BackStack<R>.buildCircuitContentProviders(
private fun <R : Record> buildCircuitContentProviders(
backStack: BackStack<R>,
navigator: Navigator,
circuit: Circuit,
unavailableRoute: @Composable (screen: Screen, modifier: Modifier) -> Unit,
Expand All @@ -174,7 +198,8 @@ private fun <R : Record> BackStack<R>.buildCircuitContentProviders(
val lastCircuit by rememberUpdatedState(circuit)
val lastUnavailableRoute by rememberUpdatedState(unavailableRoute)

return iterator()
return backStack
.iterator()
.asSequence()
.map { record ->
// Query the previous content providers map, so that we use the same
Expand All @@ -186,7 +211,6 @@ private fun <R : Record> BackStack<R>.buildCircuitContentProviders(
movableContentOf { record ->
CircuitContent(
screen = record.screen,
modifier = Modifier,
navigator = lastNavigator,
circuit = lastCircuit,
unavailableContent = lastUnavailableRoute,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuit.foundation

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.slack.circuit.runtime.CircuitUiState
import com.slack.circuit.runtime.presenter.Presenter

public abstract class PauseablePresenter<UiState : CircuitUiState>() : Presenter<UiState> {

private var lastState by mutableStateOf<UiState?>(null)

@Composable
override fun present(): UiState {
val isPaused = LocalLifecycle.current.isPaused

if (!isPaused || lastState == null) {
lastState = _present()
}

println("PauseablePresenter. isPaused: $isPaused. state: $lastState")

return lastState!!
}

@Composable protected abstract fun _present(): UiState
}

public fun <UiState : CircuitUiState> Presenter<UiState>.toPauseablePresenter():
PauseablePresenter<UiState> {
if (this is PauseablePresenter<UiState>) return this
// Else we wrap the presenter
return object : PauseablePresenter<UiState>() {
@Composable override fun _present(): UiState = this@toPauseablePresenter.present()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
Expand All @@ -43,12 +44,12 @@ public actual fun GestureNavigationDecoration(
onBackInvoked: () -> Unit,
): NavDecoration =
when {
Build.VERSION.SDK_INT >= 34 -> AndroidPredictiveNavigationDecoration(onBackInvoked)
Build.VERSION.SDK_INT >= 34 -> AndroidPredictiveBackNavigationDecoration(onBackInvoked)
else -> fallback
}

@RequiresApi(34)
private class AndroidPredictiveNavigationDecoration(private val onBackInvoked: () -> Unit) :
public class AndroidPredictiveBackNavigationDecoration(private val onBackInvoked: () -> Unit) :
NavDecoration {
@Composable
override fun <T> DecoratedContent(
Expand All @@ -70,15 +71,31 @@ private class AndroidPredictiveNavigationDecoration(private val onBackInvoked: (
label = "GestureNavDecoration",
)

if (previous != null && !transition.isStateBeingAnimated { it.record == previous }) {
var addPreviousAsOptionalLayout by remember { mutableStateOf(false) }
SideEffect {
addPreviousAsOptionalLayout =
previous != null &&
!transition.isPending &&
!transition.isStateBeingAnimated { it.record == previous }
}

if (previous != null && addPreviousAsOptionalLayout) {
// We display the 'previous' item in the back stack for when the user performs a gesture
// to go back.
// We only display it here if the transition is not running. When the transition is
// running, the record's movable content will still be attached to the
// AnimatedContent below. If we call it here too, we will invoke a new copy of
// the content (and thus dropping all state). The if statement above keeps the states
// exclusive, so that the movable content is only used once at a time.
OptionalLayout(shouldLayout = { showPrevious }) { content(previous) }
OptionalLayout(shouldLayout = { showPrevious }) {
content(previous)

DisposableEffect(previous) {
println("OptionalLayout for $previous added")

onDispose { println("OptionalLayout for $previous removed") }
}
}
}

LaunchedEffect(transition.currentState) {
Expand Down

This file was deleted.

Loading

0 comments on commit b18689a

Please sign in to comment.