Skip to content

Commit

Permalink
Add pausable presenter class
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes committed Mar 13, 2024
1 parent 4c410ca commit 419bee7
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import androidx.compose.runtime.DisposableEffect
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.ui.Modifier
Expand All @@ -21,6 +20,7 @@ import com.slack.circuit.runtime.CircuitUiState
import com.slack.circuit.runtime.InternalCircuitApi
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.presenter.toPauseablePresenter
import com.slack.circuit.runtime.screen.PopResult
import com.slack.circuit.runtime.screen.Screen
import com.slack.circuit.runtime.ui.Ui
Expand Down Expand Up @@ -88,101 +88,64 @@ public fun CircuitContent(
circuit.onUnavailableContent,
key: Any? = screen,
) {
CircuitContent(
screen = screen,
navigator = navigator,
presenterEnabled = true,
modifier = modifier,
circuit = circuit,
unavailableContent = unavailableContent,
key = key,
)
CircuitContent(screen, navigator, isPaused = false, modifier, circuit, unavailableContent, key)
}

@Composable
internal fun CircuitContent(
screen: Screen,
navigator: Navigator,
presenterEnabled: Boolean,
isPaused: Boolean,
modifier: Modifier = Modifier,
circuit: Circuit = requireNotNull(LocalCircuit.current),
unavailableContent: (@Composable (screen: Screen, modifier: Modifier) -> Unit) =
circuit.onUnavailableContent,
key: Any? = screen,
) {
val parent = LocalCircuitContext.current

@OptIn(InternalCircuitApi::class)
val context =
remember(screen, navigator, circuit, parent) {
CircuitContext(parent).also { it.circuit = circuit }
}
CompositionLocalProvider(LocalCircuitContext provides context) {
CircuitContent(
screen = screen,
modifier = modifier,
navigator = navigator,
circuit = circuit,
unavailableContent = unavailableContent,
context = context,
key = key,
presenterEnabled = presenterEnabled,
)
}
}

@Composable
private fun CircuitContent(
screen: Screen,
modifier: Modifier,
navigator: Navigator,
circuit: Circuit,
unavailableContent: (@Composable (screen: Screen, modifier: Modifier) -> Unit),
context: CircuitContext,
key: Any? = screen,
presenterEnabled: Boolean,
) {
val eventListener = rememberEventListener(screen, context, factory = circuit.eventListenerFactory)
DisposableEffect(eventListener, screen, context) { onDispose { eventListener.dispose() } }
val eventListener =
rememberEventListener(screen, context, factory = circuit.eventListenerFactory)
DisposableEffect(eventListener, screen, context) { onDispose { eventListener.dispose() } }

val presenter = rememberPresenter(screen, navigator, context, eventListener, circuit::presenter)
val presenter = rememberPresenter(screen, navigator, context, eventListener, circuit::presenter)

val ui = rememberUi(screen, context, eventListener, circuit::ui)
val ui = rememberUi(screen, context, eventListener, circuit::ui)

if (ui != null && presenter != null) {
CircuitContent(screen, modifier, presenter, presenterEnabled, ui, eventListener, key)
} else {
eventListener.onUnavailableContent(screen, presenter, ui, context)
unavailableContent(screen, modifier)
if (ui != null && presenter != null) {
CircuitContent(screen, presenter, isPaused, ui, modifier, eventListener, key)
} else {
eventListener.onUnavailableContent(screen, presenter, ui, context)
unavailableContent(screen, modifier)
}
}
}

@Composable
public fun <UiState : CircuitUiState> CircuitContent(
screen: Screen,
modifier: Modifier,
presenter: Presenter<UiState>,
ui: Ui<UiState>,
modifier: Modifier = Modifier,
eventListener: EventListener = EventListener.NONE,
key: Any? = screen,
) {
CircuitContent(
screen = screen,
modifier = modifier,
presenter = presenter,
presenterEnabled = true,
ui = ui,
eventListener = eventListener,
key = key,
)
CircuitContent(screen, presenter, isPaused = false, ui, modifier, eventListener, key)
}

@Composable
internal fun <UiState : CircuitUiState> CircuitContent(
screen: Screen,
modifier: Modifier,
presenter: Presenter<UiState>,
presenterEnabled: Boolean,
isPaused: Boolean,
ui: Ui<UiState>,
modifier: Modifier = Modifier,
eventListener: EventListener = EventListener.NONE,
key: Any? = screen,
) {
Expand All @@ -197,37 +160,25 @@ internal fun <UiState : CircuitUiState> CircuitContent(
onDispose { eventListener.onDisposePresent() }
}

var lastState by remember { mutableStateOf<UiState?>(null) }
val state =
if (presenterEnabled) {
presenter.present()
} else {
lastState
}
val pauseablePresenter = remember(presenter) { presenter.toPauseablePresenter(isPaused) }

SideEffect { pauseablePresenter.isPaused = isPaused }

SideEffect { lastState = state }
val state = pauseablePresenter.present()

// TODO not sure why stateFlow + LaunchedEffect + distinctUntilChanged doesn't work here
SideEffect {
if (state != null) {
eventListener.onState(state)
}
}
SideEffect { eventListener.onState(state) }
DisposableEffect(screen) {
eventListener.onStartContent()
onDispose { eventListener.onDisposeContent() }
}
if (state != null) {
Box {
ui.Content(state, modifier)
Box {
ui.Content(state, modifier)

if (!presenterEnabled) {
// Just for debugging. Easier to see if presenters are enabled or not
Spacer(Modifier.matchParentSize().background(Color.Magenta.copy(alpha = 0.25f)))
}
if (pauseablePresenter.isPaused) {
// Just for debugging. Easier to see if presenters are enabled or not
Spacer(Modifier.matchParentSize().background(Color.Magenta.copy(alpha = 0.25f)))
}
} else {
// TODO: What to do?
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ public fun <R : Record> NavigableCircuitContent(
) {
provider.content(
record,
// presenterEnabled. We only enable the presenter for the top record
backStack.topRecord == record,
// isPaused. We pause the presenter if it is not the top record
backStack.topRecord != record,
)
}
}
Expand Down Expand Up @@ -187,15 +187,14 @@ private fun <R : Record> BackStack<R>.buildCircuitContentProviders(
RecordContentProvider(
record = record,
content =
movableContentOf { record, presenterEnabled ->
movableContentOf { record, isPaused ->
CircuitContent(
screen = record.screen,
modifier = Modifier,
navigator = lastNavigator,
circuit = lastCircuit,
unavailableContent = lastUnavailableRoute,
key = record.key,
presenterEnabled = presenterEnabled,
isPaused = isPaused,
)
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
package com.slack.circuit.runtime.presenter

import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.CircuitUiState
import com.slack.circuit.runtime.Navigator
Expand Down Expand Up @@ -176,3 +181,33 @@ public inline fun <UiState : CircuitUiState> presenterOf(
}
}
}

public abstract class PauseablePresenter<UiState : CircuitUiState>(isPaused: Boolean = false) :
Presenter<UiState> {

public var isPaused: Boolean by mutableStateOf(isPaused)

@Composable
override fun present(): UiState {
var lastState by remember { mutableStateOf<UiState?>(null) }

return when {
// If we're paused, return the last state if we have one. If we don't we'll
// just have to call the presenter regardless
isPaused -> lastState ?: _present()
else -> _present()
}.also { SideEffect { lastState = it } }
}

@Composable protected abstract fun _present(): UiState
}

public fun <UiState : CircuitUiState> Presenter<UiState>.toPauseablePresenter(
isPaused: Boolean = false
): PauseablePresenter<UiState> {
if (this is PauseablePresenter<UiState>) return this
// Else we wrap the presenter
return object : PauseablePresenter<UiState>(isPaused) {
@Composable override fun _present(): UiState = this@toPauseablePresenter.present()
}
}

0 comments on commit 419bee7

Please sign in to comment.