Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
c453e48
Add a detail screen
stagg Aug 6, 2025
9dde251
Rework the content layout to be adaptive
stagg Aug 6, 2025
500a45d
Fix root reset
stagg Aug 6, 2025
30c6a24
Merge branch 'main' into j-sample-bottombar-adaptive
stagg Sep 8, 2025
303c8d8
Get it mostly working with `ListDetailPaneScaffold`
stagg Sep 9, 2025
18c5475
wip swipe to restore thing, trying a backstack record stash thing
stagg Sep 13, 2025
5693ede
WIP - Better approach by preventing the backstack pop
stagg Sep 16, 2025
df23f8c
Merge branch 'main' into j-sample-bottombar-adaptive
stagg Oct 17, 2025
bb4d5dc
Merge remote-tracking branch 'origin/main' into j-sample-bottombar-ad…
stagg Oct 17, 2025
5634d63
merge fixes
stagg Oct 17, 2025
e2f58bd
Add navigator as a param on the AnimatedNavDecorator.Factory create
stagg Oct 22, 2025
59b39fa
Make `AdaptiveNavDecoration` just be concerned with the ListDetail la…
stagg Oct 22, 2025
8df817e
gitignore
stagg Oct 22, 2025
e2fd5f0
retain pane position
stagg Oct 23, 2025
0406398
tidy up
stagg Oct 24, 2025
d7a7015
figure out some slide over stuff
stagg Oct 25, 2025
03855a7
compose-jb `1.10.0-alpha03`
stagg Oct 30, 2025
0936ef8
shared element changes
stagg Oct 30, 2025
4db0e3d
use the common `@Preview`
stagg Oct 30, 2025
37701b8
Add `navigationevent`
stagg Oct 30, 2025
48ff104
Make deprecated `BackHandler` work for now
stagg Oct 30, 2025
e9eb275
baseline
stagg Oct 30, 2025
198fd5c
lock files
stagg Oct 30, 2025
b1307cc
Merge branch 'main' into j-compose-110
stagg Oct 31, 2025
da56fd8
lifecycle-jb `2.10.0-alpha03`
stagg Oct 31, 2025
b5d0d6c
Use `withCompositionLocal`
stagg Oct 31, 2025
11828ef
Use `scheduleFrameEndCallback`
stagg Oct 31, 2025
bbeb297
Fix retained values getting dropped early
stagg Oct 31, 2025
ced266f
baseline
stagg Oct 31, 2025
fd4d32a
Update to compose multi-platform 1.10.0-beta01
stagg Nov 4, 2025
f921e81
Specify dependencies via version catalog
stagg Nov 4, 2025
578fa25
Merge branch 'main' into j-compose-110
stagg Nov 4, 2025
14a6e9c
Hot reload is part of the main plugin now
stagg Nov 5, 2025
5335413
Merge branch 'main' into j-compose-110
stagg Nov 6, 2025
181e23e
cleanup hot reload
stagg Nov 7, 2025
af603e6
Revert "Make deprecated `BackHandler` work for now"
stagg Nov 7, 2025
ea02fe9
Use navigation event
stagg Nov 7, 2025
b46d202
baseline
stagg Nov 7, 2025
1b53545
Merge branch 'main' into j-sample-bottombar-adaptive
stagg Nov 13, 2025
87b0ba4
navstack
stagg Nov 13, 2025
d3b9eeb
Merge branch 'main' into j-compose-110
stagg Nov 13, 2025
30202cc
roll these back to stable releases
stagg Nov 14, 2025
33e22ab
Actual SaveableNavStack implementation
stagg Nov 14, 2025
121dbd7
NavStack everywhere
stagg Nov 14, 2025
5994879
Merge branch 'j-compose-110' into j-sample-bottombar-adaptive
stagg Nov 14, 2025
48bd6f1
Start wiring into NavEventHandler
stagg Nov 14, 2025
27b2112
Fix some things, break some more
stagg Nov 17, 2025
434f8df
notes
stagg Nov 18, 2025
fe6a1db
Shared NavStackList
stagg Nov 18, 2025
d8e05e3
some nav fixes
stagg Nov 18, 2025
b799929
kdoc
stagg Nov 19, 2025
385f8b5
Keep last record in history on pop, update circuitx-navigation
stagg Nov 21, 2025
defe8a8
Customizable LocalRecordLifecycle
stagg Nov 22, 2025
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,7 @@ fastlane/report.xml
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm

# AI Stuff
/.claude/settings.local.json
/CLAUDE.local.md
2 changes: 1 addition & 1 deletion backstack/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ kotlin {
api(libs.compose.runtime)
api(libs.compose.ui)
api(libs.coroutines)
api(projects.circuitRuntimeScreen)
api(projects.circuitRuntime)
implementation(libs.compose.runtime.saveable)
}
}
Expand Down
14 changes: 14 additions & 0 deletions backstack/dependencies/androidReleaseRuntimeClasspath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ androidx.collection:collection
androidx.compose.runtime:runtime-android
androidx.compose.runtime:runtime-annotation-android
androidx.compose.runtime:runtime-annotation
androidx.compose.runtime:runtime-retain-android
androidx.compose.runtime:runtime-retain
androidx.compose.runtime:runtime-saveable-android
androidx.compose.runtime:runtime-saveable
androidx.compose.runtime:runtime
Expand All @@ -32,12 +34,17 @@ androidx.core:core-ktx
androidx.core:core-viewtree
androidx.core:core
androidx.customview:customview-poolingcontainer
androidx.documentfile:documentfile
androidx.dynamicanimation:dynamicanimation
androidx.emoji2:emoji2
androidx.graphics:graphics-path
androidx.interpolator:interpolator
androidx.legacy:legacy-support-core-utils
androidx.lifecycle:lifecycle-common-jvm
androidx.lifecycle:lifecycle-common
androidx.lifecycle:lifecycle-livedata-core-ktx
androidx.lifecycle:lifecycle-livedata-core
androidx.lifecycle:lifecycle-livedata
androidx.lifecycle:lifecycle-process
androidx.lifecycle:lifecycle-runtime-android
androidx.lifecycle:lifecycle-runtime-compose-android
Expand All @@ -50,6 +57,9 @@ androidx.lifecycle:lifecycle-viewmodel-ktx
androidx.lifecycle:lifecycle-viewmodel-savedstate-android
androidx.lifecycle:lifecycle-viewmodel-savedstate
androidx.lifecycle:lifecycle-viewmodel
androidx.loader:loader
androidx.localbroadcastmanager:localbroadcastmanager
androidx.print:print
androidx.profileinstaller:profileinstaller
androidx.savedstate:savedstate-android
androidx.savedstate:savedstate-compose-android
Expand All @@ -58,7 +68,11 @@ androidx.savedstate:savedstate-ktx
androidx.savedstate:savedstate
androidx.startup:startup-runtime
androidx.tracing:tracing
androidx.transition:transition
androidx.versionedparcelable:versionedparcelable
androidx.window:window-core-android
androidx.window:window-core
androidx.window:window
com.google.guava:listenablefuture
org.jetbrains.androidx.lifecycle:lifecycle-common
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose
Expand Down
4 changes: 4 additions & 0 deletions backstack/dependencies/jvmRuntimeClasspath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ androidx.collection:collection
androidx.compose.runtime:runtime-annotation-jvm
androidx.compose.runtime:runtime-annotation
androidx.compose.runtime:runtime-desktop
androidx.compose.runtime:runtime-retain-desktop
androidx.compose.runtime:runtime-retain
androidx.compose.runtime:runtime-saveable-desktop
androidx.compose.runtime:runtime-saveable
androidx.compose.runtime:runtime
Expand All @@ -19,6 +21,8 @@ androidx.lifecycle:lifecycle-viewmodel-desktop
androidx.lifecycle:lifecycle-viewmodel-savedstate-desktop
androidx.lifecycle:lifecycle-viewmodel-savedstate
androidx.lifecycle:lifecycle-viewmodel
androidx.navigationevent:navigationevent-desktop
androidx.navigationevent:navigationevent
androidx.savedstate:savedstate-compose-desktop
androidx.savedstate:savedstate-compose
androidx.savedstate:savedstate-desktop
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class RememberSaveableBackstackTest {
fun backStackStartsWithRootScreen() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
val backStack = rememberSaveableBackStack(TestScreen.ScreenA)
backStack.toList()
backStack.peekNavStack()
}
.test { assertEquals(awaitItem().first().screen, TestScreen.ScreenA) }
}
Expand Down Expand Up @@ -52,8 +52,8 @@ class RememberSaveableBackstackTest {
val secondStack = awaitItem()

assertNotSame(firstStack, secondStack)
assertEquals(firstStack.toList().first().screen, TestScreen.ScreenA)
assertEquals(secondStack.toList().first().screen, TestScreen.ScreenB)
assertEquals(firstStack.peekNavStack().first().screen, TestScreen.ScreenA)
assertEquals(secondStack.peekNavStack().first().screen, TestScreen.ScreenB)
}
}

Expand All @@ -67,8 +67,8 @@ class RememberSaveableBackstackTest {
val secondStack = awaitItem()

assertNotSame(firstStack, secondStack)
assertEquals(firstStack.toList().first().screen, TestScreen.ScreenA)
assertEquals(secondStack.toList().first().screen, TestScreen.ScreenB)
assertEquals(firstStack.peekNavStack().first().screen, TestScreen.ScreenA)
assertEquals(secondStack.peekNavStack().first().screen, TestScreen.ScreenB)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,144 +16,17 @@
package com.slack.circuit.backstack

import androidx.compose.runtime.Stable
import androidx.compose.runtime.snapshots.Snapshot
import com.slack.circuit.backstack.BackStack.Record
import com.slack.circuit.runtime.screen.Screen
import com.slack.circuit.backstack.NavStack.Record

/**
* A caller-supplied stack of [Record]s for presentation with a `Navigator`. Iteration order is
* top-first (first element is the top of the stack).
*
* BackStack extends NavStack but is intended for backward-only navigation patterns. Implementations
* may provide no-op implementations for forward navigation methods.
*/
@Stable
public interface BackStack<R : Record> : Iterable<R> {
/** The number of records contained in this [BackStack] that will be seen by an iterator. */
public val size: Int

/** 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?

/**
* Push a new [Record] onto the back stack. The new record will become the top of the stack.
*
* @param record The record to push onto the stack.
* @return If the [record] was successfully pushed onto the back stack
*/
public fun push(record: R): Boolean

/**
* Push a new [Screen] onto the back stack. This will be enveloped in a [Record] and the new
* record will become the top of the stack.
*
* @param screen The screen to push onto the stack.
* @return If the [screen] was successfully pushed onto the back stack
*/
public fun push(screen: Screen): Boolean

/**
* Attempt to pop the top item off of the back stack, returning the popped [Record] if popping was
* successful or `null` if no entry was popped.
*/
public fun pop(): R?

/**
* Pop records off the top of the backstack until one is found that matches the given predicate.
*/
public fun popUntil(predicate: (R) -> Boolean): List<R> {
return buildList {
while (topRecord?.let(predicate) == false) {
val popped = pop() ?: break
add(popped)
}
}
}

/**
* Saves the current back stack entry list in an internal state store. It can be later restored by
* the root screen to [restoreState].
*
* This call will overwrite any existing stored state with the same root screen.
*/
public fun saveState()

/**
* Restores the saved state with the given [screen], adding it on top of the existing entry list.
* If you wish to replace the current entry list, you should [pop] all of the existing entries off
* before calling this function.
*
* @param screen The root screen which was previously saved using [saveState].
* @return Returns true if there was any back stack state to restore.
*/
public fun restoreState(screen: Screen): Boolean

/**
* Peek at the [Screen] in the internal state store that have been saved using [saveState].
*
* @return The list of [Screen]s currently in the internal state store, will be empty if there is
* no saved state.
*/
public fun peekState(): List<Screen>

/**
* Removes the state associated with the given [screen] from the internal state store.
*
* @return true if the state was removed, false if no state was found for the given screen.
*/
public fun removeState(screen: Screen): Boolean

/**
* Whether the back stack contains the given [record].
*
* @param includeSaved Whether to also check if the record is contained by any saved back stack
* state. See [saveState].
*/
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 {
/**
* A value that identifies this record uniquely, even if it shares the same [screen] with
* another record. This key may be used by [BackStackRecordLocalProvider]s to associate
* presentation data with a record across composition recreation.
*
* [key] MUST NOT change for the life of the record.
*/
public val key: String

/** The [Screen] that should present this record. */
public val screen: Screen
}
}

/** `true` if the [BackStack] contains no records. [Iterable.firstOrNull] will return `null`. */
public val BackStack<out Record>.isEmpty: Boolean
get() = size == 0

/** `true` if the [BackStack] contains exactly one record. */
public val BackStack<out Record>.isAtRoot: Boolean
get() = size == 1
public interface BackStack<R : Record> : NavStack<R>, Iterable<R> {

/** Clear any saved state from the [BackStack]. */
public fun BackStack<out Record>.clearState() {
Snapshot.withMutableSnapshot {
for (screen in peekState()) {
removeState(screen)
}
}
@Stable public interface Record : NavStack.Record
}
Loading