Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions backstack/api/android/backstack.api
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
public abstract interface class com/slack/circuit/backstack/BackStack : java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker {
public abstract fun containsRecord (Lcom/slack/circuit/backstack/BackStack$Record;Z)Z
public abstract fun getRootRecord ()Lcom/slack/circuit/backstack/BackStack$Record;
public abstract fun getSize ()I
public abstract fun getTopRecord ()Lcom/slack/circuit/backstack/BackStack$Record;
public abstract fun isRecordReachable (Ljava/lang/String;IZ)Z
public abstract fun pop (Lcom/slack/circuit/runtime/screen/PopResult;)Lcom/slack/circuit/backstack/BackStack$Record;
public static synthetic fun pop$default (Lcom/slack/circuit/backstack/BackStack;Lcom/slack/circuit/runtime/screen/PopResult;ILjava/lang/Object;)Lcom/slack/circuit/backstack/BackStack$Record;
public fun popUntil (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableList;
Expand Down Expand Up @@ -50,9 +52,12 @@ public final class com/slack/circuit/backstack/SaveableBackStack : com/slack/cir
public fun <init> (Lcom/slack/circuit/runtime/screen/Screen;)V
public synthetic fun containsRecord (Lcom/slack/circuit/backstack/BackStack$Record;Z)Z
public fun containsRecord (Lcom/slack/circuit/backstack/SaveableBackStack$Record;Z)Z
public synthetic fun getRootRecord ()Lcom/slack/circuit/backstack/BackStack$Record;
public fun getRootRecord ()Lcom/slack/circuit/backstack/SaveableBackStack$Record;
public fun getSize ()I
public synthetic fun getTopRecord ()Lcom/slack/circuit/backstack/BackStack$Record;
public fun getTopRecord ()Lcom/slack/circuit/backstack/SaveableBackStack$Record;
public fun isRecordReachable (Ljava/lang/String;IZ)Z
public fun iterator ()Ljava/util/Iterator;
public synthetic fun pop (Lcom/slack/circuit/runtime/screen/PopResult;)Lcom/slack/circuit/backstack/BackStack$Record;
public fun pop (Lcom/slack/circuit/runtime/screen/PopResult;)Lcom/slack/circuit/backstack/SaveableBackStack$Record;
Expand Down
6 changes: 6 additions & 0 deletions backstack/api/backstack.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ abstract fun interface com.slack.circuit.backstack/ProvidedValues { // com.slack
}

abstract interface <#A: com.slack.circuit.backstack/BackStack.Record> com.slack.circuit.backstack/BackStack : kotlin.collections/Iterable<#A> { // com.slack.circuit.backstack/BackStack|null[0]
abstract val rootRecord // com.slack.circuit.backstack/BackStack.rootRecord|{}rootRecord[0]
abstract fun <get-rootRecord>(): #A? // com.slack.circuit.backstack/BackStack.rootRecord.<get-rootRecord>|<get-rootRecord>(){}[0]
abstract val size // com.slack.circuit.backstack/BackStack.size|{}size[0]
abstract fun <get-size>(): kotlin/Int // com.slack.circuit.backstack/BackStack.size.<get-size>|<get-size>(){}[0]
abstract val topRecord // com.slack.circuit.backstack/BackStack.topRecord|{}topRecord[0]
abstract fun <get-topRecord>(): #A? // com.slack.circuit.backstack/BackStack.topRecord.<get-topRecord>|<get-topRecord>(){}[0]

abstract fun containsRecord(#A, kotlin/Boolean): kotlin/Boolean // com.slack.circuit.backstack/BackStack.containsRecord|containsRecord(1:0;kotlin.Boolean){}[0]
abstract fun isRecordReachable(kotlin/String, kotlin/Int, kotlin/Boolean): kotlin/Boolean // com.slack.circuit.backstack/BackStack.isRecordReachable|isRecordReachable(kotlin.String;kotlin.Int;kotlin.Boolean){}[0]
abstract fun pop(com.slack.circuit.runtime.screen/PopResult? = ...): #A? // com.slack.circuit.backstack/BackStack.pop|pop(com.slack.circuit.runtime.screen.PopResult?){}[0]
abstract fun push(#A, kotlin/String? = ...): kotlin/Boolean // com.slack.circuit.backstack/BackStack.push|push(1:0;kotlin.String?){}[0]
abstract fun push(com.slack.circuit.runtime.screen/Screen, kotlin/String? = ...): kotlin/Boolean // com.slack.circuit.backstack/BackStack.push|push(com.slack.circuit.runtime.screen.Screen;kotlin.String?){}[0]
Expand Down Expand Up @@ -51,12 +54,15 @@ final class com.slack.circuit.backstack/SaveableBackStack : com.slack.circuit.ba
constructor <init>(com.slack.circuit.backstack/SaveableBackStack.Record) // com.slack.circuit.backstack/SaveableBackStack.<init>|<init>(com.slack.circuit.backstack.SaveableBackStack.Record){}[0]
constructor <init>(com.slack.circuit.runtime.screen/Screen) // com.slack.circuit.backstack/SaveableBackStack.<init>|<init>(com.slack.circuit.runtime.screen.Screen){}[0]

final val rootRecord // com.slack.circuit.backstack/SaveableBackStack.rootRecord|{}rootRecord[0]
final fun <get-rootRecord>(): com.slack.circuit.backstack/SaveableBackStack.Record? // com.slack.circuit.backstack/SaveableBackStack.rootRecord.<get-rootRecord>|<get-rootRecord>(){}[0]
final val size // com.slack.circuit.backstack/SaveableBackStack.size|{}size[0]
final fun <get-size>(): kotlin/Int // com.slack.circuit.backstack/SaveableBackStack.size.<get-size>|<get-size>(){}[0]
final val topRecord // com.slack.circuit.backstack/SaveableBackStack.topRecord|{}topRecord[0]
final fun <get-topRecord>(): com.slack.circuit.backstack/SaveableBackStack.Record? // com.slack.circuit.backstack/SaveableBackStack.topRecord.<get-topRecord>|<get-topRecord>(){}[0]

final fun containsRecord(com.slack.circuit.backstack/SaveableBackStack.Record, kotlin/Boolean): kotlin/Boolean // com.slack.circuit.backstack/SaveableBackStack.containsRecord|containsRecord(com.slack.circuit.backstack.SaveableBackStack.Record;kotlin.Boolean){}[0]
final fun isRecordReachable(kotlin/String, kotlin/Int, kotlin/Boolean): kotlin/Boolean // com.slack.circuit.backstack/SaveableBackStack.isRecordReachable|isRecordReachable(kotlin.String;kotlin.Int;kotlin.Boolean){}[0]
final fun iterator(): kotlin.collections/Iterator<com.slack.circuit.backstack/SaveableBackStack.Record> // com.slack.circuit.backstack/SaveableBackStack.iterator|iterator(){}[0]
final fun pop(com.slack.circuit.runtime.screen/PopResult?): com.slack.circuit.backstack/SaveableBackStack.Record? // com.slack.circuit.backstack/SaveableBackStack.pop|pop(com.slack.circuit.runtime.screen.PopResult?){}[0]
final fun push(com.slack.circuit.backstack/SaveableBackStack.Record, kotlin/String?): kotlin/Boolean // com.slack.circuit.backstack/SaveableBackStack.push|push(com.slack.circuit.backstack.SaveableBackStack.Record;kotlin.String?){}[0]
Expand Down
5 changes: 5 additions & 0 deletions backstack/api/jvm/backstack.api
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
public abstract interface class com/slack/circuit/backstack/BackStack : java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker {
public abstract fun containsRecord (Lcom/slack/circuit/backstack/BackStack$Record;Z)Z
public abstract fun getRootRecord ()Lcom/slack/circuit/backstack/BackStack$Record;
public abstract fun getSize ()I
public abstract fun getTopRecord ()Lcom/slack/circuit/backstack/BackStack$Record;
public abstract fun isRecordReachable (Ljava/lang/String;IZ)Z
public abstract fun pop (Lcom/slack/circuit/runtime/screen/PopResult;)Lcom/slack/circuit/backstack/BackStack$Record;
public static synthetic fun pop$default (Lcom/slack/circuit/backstack/BackStack;Lcom/slack/circuit/runtime/screen/PopResult;ILjava/lang/Object;)Lcom/slack/circuit/backstack/BackStack$Record;
public fun popUntil (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableList;
Expand Down Expand Up @@ -50,9 +52,12 @@ public final class com/slack/circuit/backstack/SaveableBackStack : com/slack/cir
public fun <init> (Lcom/slack/circuit/runtime/screen/Screen;)V
public synthetic fun containsRecord (Lcom/slack/circuit/backstack/BackStack$Record;Z)Z
public fun containsRecord (Lcom/slack/circuit/backstack/SaveableBackStack$Record;Z)Z
public synthetic fun getRootRecord ()Lcom/slack/circuit/backstack/BackStack$Record;
public fun getRootRecord ()Lcom/slack/circuit/backstack/SaveableBackStack$Record;
public fun getSize ()I
public synthetic fun getTopRecord ()Lcom/slack/circuit/backstack/BackStack$Record;
public fun getTopRecord ()Lcom/slack/circuit/backstack/SaveableBackStack$Record;
public fun isRecordReachable (Ljava/lang/String;IZ)Z
public fun iterator ()Ljava/util/Iterator;
public synthetic fun pop (Lcom/slack/circuit/runtime/screen/PopResult;)Lcom/slack/circuit/backstack/BackStack$Record;
public fun pop (Lcom/slack/circuit/runtime/screen/PopResult;)Lcom/slack/circuit/backstack/SaveableBackStack$Record;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public interface BackStack<R : Record> : Iterable<R> {
/** The top-most record in the [BackStack], or `null` if the [BackStack] is empty. */
public val topRecord: R?

/** The bottom-most record in the [BackStack], or `null` if the [BackStack] is empty. */
public val rootRecord: R?
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added this to help check the current root screen without having to iterate the whole back stack.

tabs.forEach { tab ->
val selected = tab == backStack.rootRecord?.screen
Text(


/**
* Push a new [Record] onto the back stack. The new record will become the top of the stack.
*
Expand Down Expand Up @@ -103,6 +106,20 @@ public interface BackStack<R : Record> : Iterable<R> {
*/
public fun containsRecord(record: R, includeSaved: Boolean): Boolean

/**
* Whether a record with the given [key] is reachable within the back stack or saved state.
* Reachable means that it is either currently in the visible back stack or if we popped `depth`
* times, it would be found.
*
* @param key The record's key to look for.
* @param depth How many records to consider popping from the top of the stack before considering
* the key unreachable. A depth of zero means only check the current visible stack. A depth of 1
* means check the current visible stack plus one record popped off the top, and so on.
* @param includeSaved Whether to also check if the record is contained by any saved back stack
* state. See [saveState].
*/
public fun isRecordReachable(key: String, depth: Int, includeSaved: Boolean): Boolean

@Stable
public interface Record {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.Snapshot
import com.slack.circuit.runtime.screen.PopResult
import com.slack.circuit.runtime.screen.Screen
import kotlin.math.min
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
import kotlinx.coroutines.CompletableDeferred
Expand Down Expand Up @@ -92,6 +93,9 @@ internal constructor(
public override val topRecord: Record?
get() = entryList.firstOrNull()

override val rootRecord: Record?
get() = entryList.lastOrNull()

public override fun push(screen: Screen, resultKey: String?): Boolean {
return push(screen, emptyMap(), resultKey)
}
Expand Down Expand Up @@ -154,6 +158,24 @@ internal constructor(
return false
}

override fun isRecordReachable(key: String, depth: Int, includeSaved: Boolean): Boolean {
if (depth < 0) return false
// Check in the current entry list
for (i in 0 until min(depth, entryList.size)) {
if (entryList[i].key == key) return true
}
// If includeSaved, check saved backstack states too
if (includeSaved && stateStore.isNotEmpty()) {
val storedValues = stateStore.values
for ((i, stored) in storedValues.withIndex()) {
if (i >= depth) break
// stored can mutate, so safely get the record.
if (stored.getOrNull(i)?.key == key) return true
}
}
return false
}

public data class Record(
override val screen: Screen,
val args: Map<String, Any?> = emptyMap(),
Expand Down
6 changes: 0 additions & 6 deletions circuit-foundation/api/android/circuit-foundation.api
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,6 @@ public final class com/slack/circuit/foundation/ComposableSingletons$CircuitKt {
public final fun getLambda-1$circuit_foundation_release ()Lkotlin/jvm/functions/Function4;
}

public final class com/slack/circuit/foundation/ComposableSingletons$NavigableCircuitContentKt {
public static final field INSTANCE Lcom/slack/circuit/foundation/ComposableSingletons$NavigableCircuitContentKt;
public fun <init> ()V
public final fun getLambda-1$circuit_foundation_release ()Lkotlin/jvm/functions/Function4;
}

public final class com/slack/circuit/foundation/ContentProviderState {
public static final field $stable I
public fun <init> (Landroidx/compose/runtime/saveable/SaveableStateHolder;Lcom/slack/circuit/retained/RetainedStateHolder;Lcom/slack/circuit/backstack/BackStack;Lcom/slack/circuit/runtime/Navigator;Lcom/slack/circuit/foundation/Circuit;Lkotlin/jvm/functions/Function4;)V
Expand Down
6 changes: 0 additions & 6 deletions circuit-foundation/api/jvm/circuit-foundation.api
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,6 @@ public final class com/slack/circuit/foundation/ComposableSingletons$CircuitKt {
public final fun getLambda-1$circuit_foundation ()Lkotlin/jvm/functions/Function4;
}

public final class com/slack/circuit/foundation/ComposableSingletons$NavigableCircuitContentKt {
public static final field INSTANCE Lcom/slack/circuit/foundation/ComposableSingletons$NavigableCircuitContentKt;
public fun <init> ()V
public final fun getLambda-1$circuit_foundation ()Lkotlin/jvm/functions/Function4;
}

public final class com/slack/circuit/foundation/ContentProviderState {
public static final field $stable I
public fun <init> (Landroidx/compose/runtime/saveable/SaveableStateHolder;Lcom/slack/circuit/retained/RetainedStateHolder;Lcom/slack/circuit/backstack/BackStack;Lcom/slack/circuit/runtime/Navigator;Lcom/slack/circuit/foundation/Circuit;Lkotlin/jvm/functions/Function4;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,18 @@ class SaveableRestRootTest {
mainClock.autoAdvance = false
navigator.resetRoot(newRoot = TestScreen.ScreenA, saveState = true, restoreState = true)
mainClock.advanceTimeByFrame()
navigator.resetRoot(newRoot = TestScreen.ScreenB, saveState = true, restoreState = true)
mainClock.advanceTimeByFrame()
repeat(10) {
navigator.resetRoot(newRoot = TestScreen.ScreenB, saveState = true, restoreState = true)
mainClock.advanceTimeByFrame()
navigator.resetRoot(newRoot = TestScreen.ScreenA, saveState = true, restoreState = true)
mainClock.advanceTimeByFrame()
navigator.resetRoot(newRoot = TestScreen.ScreenC, saveState = true, restoreState = true)
mainClock.advanceTimeByFrame()
navigator.resetRoot(newRoot = TestScreen.ScreenA, saveState = true, restoreState = true)
mainClock.advanceTimeByFrame()
navigator.resetRoot(newRoot = TestScreen.ScreenB, saveState = true, restoreState = true)
mainClock.advanceTimeByFrame()
}
navigator.resetRoot(newRoot = TestScreen.ScreenA, saveState = true, restoreState = true)
mainClock.autoAdvance = true
onNodeWithTag(TAG_LABEL).assertTextEquals(TestScreen.ScreenA.label)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.SaveableStateHolder
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.runtime.setValue
Expand Down Expand Up @@ -59,7 +60,9 @@ import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.screen.Screen
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentSet

@OptIn(ExperimentalCircuitApi::class)
@Composable
Expand Down Expand Up @@ -142,7 +145,6 @@ public fun <R : Record> NavigableCircuitContent(
lastUnavailableRoute = unavailableRoute
}
val activeContentProviders = buildCircuitContentProviders(backStack = backStack)

navDecoration.DecoratedContent(activeContentProviders, backStack.size, modifier) { provider ->
val record = provider.record

Expand Down Expand Up @@ -188,22 +190,48 @@ private fun <R : Record> buildCircuitContentProviders(
backStack: BackStack<R>
): ImmutableList<RecordContentProvider<R>> {
val previousContentProviders = remember { mutableMapOf<String, RecordContentProvider<R>>() }
val activeRecordKeys = remember { mutableSetOf<String>() }
val recordKeys by
remember { mutableStateOf(persistentSetOf<String>()) }
.apply { value = backStack.map { it.key }.toPersistentSet() }
val latestBackStack by rememberUpdatedState(backStack)
DisposableEffect(recordKeys) {
// Delay cleanup until the next backstack change.
// - Any record in composition is considered active
// - Any record in the backstack can be shown by a decorator
// - Any reachable record can be shown on a root reset
val contentNotInBackStack =
previousContentProviders.keys.filterNot {
it in activeRecordKeys ||
it in recordKeys ||
latestBackStack.isRecordReachable(key = it, depth = 1, includeSaved = true)
}
onDispose {
// Only remove the keys that are no longer in the backstack or composition.
contentNotInBackStack
.filterNot {
latestBackStack.isRecordReachable(key = it, depth = 1, includeSaved = true) ||
it in activeRecordKeys
}
.forEach { previousContentProviders.remove(it) }
}
}
return backStack
.map { record ->
// Query the previous content providers map, so that we use the same
// RecordContentProvider instances across calls.
previousContentProviders.getOrPut(record.key) {
RecordContentProvider(record = record, content = createRecordContent())
RecordContentProvider(
record = record,
content =
createRecordContent(
onActive = { activeRecordKeys.add(record.key) },
onDispose = { activeRecordKeys.remove(record.key) },
),
)
}
}
.toImmutableList()
.also { list ->
// Update the previousContentProviders map so we can reference it on the next call
previousContentProviders.clear()
for (provider in list) {
previousContentProviders[provider.record.key] = provider
}
}
}

@Stable
Expand Down Expand Up @@ -238,12 +266,11 @@ public class ContentProviderState<R : Record>(
}
}

private fun <R : Record> createRecordContent() =
private fun <R : Record> createRecordContent(onActive: () -> Unit, onDispose: () -> Unit) =
movableContentOf<R, ContentProviderState<R>> { record, contentProviderState ->
with(contentProviderState) {
val lifecycle =
remember { MutableRecordLifecycle() }.apply { isActive = lastBackStack.topRecord == record }

saveableStateHolder.SaveableStateProvider(record.registryKey) {
// Provides a RetainedStateRegistry that is maintained independently for each record while
// the record exists in the back stack.
Expand All @@ -269,6 +296,11 @@ private fun <R : Record> createRecordContent() =
}
}
}
// Track if the movableContent is still active to correctly reuse it and not create a new one.
DisposableEffect(Unit) {
onActive()
onDispose { onDispose() }
}
}

/** The maximum radix available for conversion to and from strings. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuitx.gesturenavigation

import androidx.compose.animation.core.Transition
import androidx.compose.runtime.Immutable
import com.slack.circuit.backstack.NavArgument
import com.slack.circuit.foundation.animation.AnimatedNavState
import com.slack.circuit.runtime.screen.Screen

internal fun <T> Transition<T>.isStateBeingAnimated(equals: (T) -> Boolean): Boolean {
return isRunning && (equals(currentState) || equals(targetState))
}

internal val Transition<*>.isPending: Boolean
get() = this.currentState != this.targetState

/**
* A holder class used by the `AnimatedContent` composables. This enables us to pass through all of
* the necessary information as an argument, which is optimal for `AnimatedContent`.
Expand Down
5 changes: 5 additions & 0 deletions samples/star/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,16 @@ kotlin {
val androidInstrumentedTest by getting {
// Annoyingly cannot depend on commonJvmTest
dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.ui.testing.junit)
implementation(libs.androidx.compose.ui.testing.manifest)
implementation(libs.compose.ui.testing.junit)
implementation(libs.coroutines.android)
implementation(libs.coroutines.test)
implementation(libs.junit)
implementation(libs.leakcanary.android.instrumentation)
implementation(projects.circuitTest)
implementation(projects.internalTestUtils)
implementation(projects.samples.star.coilRule)
}
}
Expand Down
Loading