Skip to content

Commit 8b55940

Browse files
committed
API overhaul because splitting the keys and the composables don't work.
All of our tests and demos were built using `String` as the key, with `content` that does nothing but render the key. This approach doesn't reflect reality very well, and masked #63, where keys for more interesting objects can get out of sync with the `content` lambda that can render them. When popping, you would wind up crashing when the up to date lambda is unable to interpret the key for the screen that is being animated away. The fix is to change the API from something that takes a list of keys and a function that can render them, to a list of model objects that themselves are able to provide `@Composable Content()`. IMHO the updated API actually feels pretty good, more like the conventional hoisted-state `@Composable Foo(model: FooModel)` idiom. (Of course I've been working on this all day, so I'm biased.) We provide a new interface: ```kotlin interface BackstackFrame<out K : Any> { val key: K @composable fun Content() } ``` And change the signature of the `Backstack()` function: ```kotlin fun <K : Any> Backstack( frames: List<BackstackFrame<K>>, modifier: Modifier = Modifier, frameController: FrameController<K> ) ``` Note that the param type, `K`, is still the type of the key, not the type of a particular flavor of `BackstackFrame`. This makes it easy for us to provide convenience functions to map lists of arbitrary model objects to `BackstackFrame` instances, so it's not much more verbose than it used to be to make it go. Before: ```kotlin Backstack(backstack) { screen -> when(screen) { Screen.ContactList -> ShowContactList(navigator) is Screen.ContactDetails -> ShowContact(screen.id, navigator) is Screen.EditContact -> ShowEditContact(screen.id, navigator) } } ``` After: ```kotlin Backstack( backstack.toBackstackModel { screen -> when(screen) { Screen.ContactList -> ShowContactList(navigator) is Screen.ContactDetails -> ShowContact(screen.id, navigator) is Screen.EditContact -> ShowEditContact(screen.id, navigator) } ) ``` Note that there are two flavors of `toBackstackModel`. The second one supports models with more interesting keys. ```kotlin data class Portrait( val id: Int, val url: String ) Backstack( backstack.toBackstackModel( getKey = { it.id } ) { PrettyPicture(it.url) } ) ``` Fixes #63
1 parent 82429b4 commit 8b55940

File tree

11 files changed

+205
-165
lines changed

11 files changed

+205
-165
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# -SNAPSHOT will automatically be appended. Pass -PisRelease=true to gradlew to release (this will
22
# also append the current compose version number after a +).
3-
releaseVersion=0.10.0
3+
releaseVersion=0.11.0

compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/BackstackViewerApp.kt

+10-9
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import com.zachklipp.compose.backstack.BackstackTransition.Crossfade
4141
import com.zachklipp.compose.backstack.BackstackTransition.Slide
4242
import com.zachklipp.compose.backstack.defaultBackstackAnimation
4343
import com.zachklipp.compose.backstack.rememberTransitionController
44+
import com.zachklipp.compose.backstack.toBackstackModel
4445
import com.zachklipp.compose.backstack.xray.xrayed
4546

4647
private val DEFAULT_BACKSTACKS = listOf(
@@ -147,7 +148,14 @@ private fun AppScreens(model: AppModel) {
147148

148149
MaterialTheme(colors = lightColors()) {
149150
Backstack(
150-
backstack = model.currentBackstack,
151+
frames = model.currentBackstack.toBackstackModel { screen ->
152+
AppScreen(
153+
name = screen,
154+
showBack = screen != model.bottomScreen,
155+
onAdd = { model.pushScreen("$screen+") },
156+
onBack = model::popScreen
157+
)
158+
},
151159
frameController = rememberTransitionController<String>(
152160
transition = model.selectedTransition.second,
153161
animationSpec = animation ?: defaultBackstackAnimation(),
@@ -165,14 +173,7 @@ private fun AppScreens(model: AppModel) {
165173
modifier = Modifier
166174
.fillMaxSize()
167175
.border(width = 3.dp, color = Color.Red),
168-
) { screen ->
169-
AppScreen(
170-
name = screen,
171-
showBack = screen != model.bottomScreen,
172-
onAdd = { model.pushScreen("$screen+") },
173-
onBack = model::popScreen
174-
)
175-
}
176+
)
176177
}
177178
}
178179

compose-backstack-xray/src/main/java/com/zachklipp/compose/backstack/xray/XrayController.kt

+11-10
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import androidx.compose.ui.graphics.DefaultCameraDistance
1212
import androidx.compose.ui.graphics.graphicsLayer
1313
import androidx.compose.ui.input.pointer.pointerInput
1414
import androidx.compose.ui.unit.dp
15+
import com.zachklipp.compose.backstack.BackstackFrame
1516
import com.zachklipp.compose.backstack.FrameController
16-
import com.zachklipp.compose.backstack.FrameController.BackstackFrame
17+
import com.zachklipp.compose.backstack.FrameController.FrameAndModifier
1718
import com.zachklipp.compose.backstack.NoopFrameController
1819
import kotlin.math.sin
1920

@@ -22,16 +23,16 @@ import kotlin.math.sin
2223
* the screens in the backstack in pseudo-3D space. The 3D stack can be navigated via touch
2324
* gestures.
2425
*/
25-
@Composable fun <T : Any> FrameController<T>.xrayed(enabled: Boolean): FrameController<T> =
26-
remember { XrayController<T>() }.also {
26+
@Composable fun <K : Any> FrameController<K>.xrayed(enabled: Boolean): FrameController<K> =
27+
remember { XrayController<K>() }.also {
2728
it.enabled = enabled
2829
it.wrappedController = this
2930
}
3031

31-
private class XrayController<T : Any> : FrameController<T> {
32+
private class XrayController<K : Any> : FrameController<K> {
3233

3334
var enabled: Boolean by mutableStateOf(false)
34-
var wrappedController: FrameController<T> by mutableStateOf(NoopFrameController())
35+
var wrappedController: FrameController<K> by mutableStateOf(NoopFrameController())
3536

3637
private var offsetDpX by mutableStateOf(500.dp)
3738
private var offsetDpY by mutableStateOf(10.dp)
@@ -41,7 +42,7 @@ private class XrayController<T : Any> : FrameController<T> {
4142
private var alpha by mutableStateOf(.4f)
4243
private var overlayAlpha by mutableStateOf(.2f)
4344

44-
private var activeKeys by mutableStateOf(emptyList<T>())
45+
private var activeKeys by mutableStateOf(emptyList<BackstackFrame<K>>())
4546

4647
private val controlModifier = Modifier.pointerInput(Unit) {
4748
detectTransformGestures { _, pan, zoom, _ ->
@@ -56,14 +57,14 @@ private class XrayController<T : Any> : FrameController<T> {
5657
if (!enabled) wrappedController.activeFrames else {
5758
activeKeys.mapIndexed { index, key ->
5859
val modifier = Modifier.modifierForFrame(index, activeKeys.size, 1f)
59-
return@mapIndexed BackstackFrame(key, modifier)
60+
return@mapIndexed FrameAndModifier(key, modifier)
6061
}
6162
}
6263
}
6364

64-
override fun updateBackstack(keys: List<T>) {
65-
activeKeys = keys
66-
wrappedController.updateBackstack(keys)
65+
override fun updateBackstack(frames: List<BackstackFrame<K>>) {
66+
activeKeys = frames
67+
wrappedController.updateBackstack(frames)
6768
}
6869

6970
/**

compose-backstack/api/compose-backstack.api

+14-11
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
public abstract interface class com/zachklipp/compose/backstack/BackstackFrame {
2+
public abstract fun Content (Landroidx/compose/runtime/Composer;I)V
3+
public abstract fun getKey ()Ljava/lang/Object;
4+
}
5+
16
public final class com/zachklipp/compose/backstack/BackstackKt {
2-
public static final fun Backstack (Ljava/util/List;Landroidx/compose/ui/Modifier;Lcom/zachklipp/compose/backstack/BackstackTransition;Landroidx/compose/animation/core/AnimationSpec;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)V
3-
public static final fun Backstack (Ljava/util/List;Landroidx/compose/ui/Modifier;Lcom/zachklipp/compose/backstack/BackstackTransition;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
4-
public static final fun Backstack (Ljava/util/List;Landroidx/compose/ui/Modifier;Lcom/zachklipp/compose/backstack/FrameController;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
5-
public static synthetic fun Backstack$default (Ljava/util/List;Landroidx/compose/ui/Modifier;Lcom/zachklipp/compose/backstack/BackstackTransition;Landroidx/compose/animation/core/AnimationSpec;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V
7+
public static final fun Backstack (Ljava/util/List;Landroidx/compose/ui/Modifier;Lcom/zachklipp/compose/backstack/BackstackTransition;Landroidx/compose/runtime/Composer;II)V
8+
public static final fun Backstack (Ljava/util/List;Landroidx/compose/ui/Modifier;Lcom/zachklipp/compose/backstack/FrameController;Landroidx/compose/runtime/Composer;II)V
69
}
710

811
public abstract interface class com/zachklipp/compose/backstack/BackstackTransition {
@@ -30,15 +33,15 @@ public abstract interface class com/zachklipp/compose/backstack/FrameController
3033
public abstract fun updateBackstack (Ljava/util/List;)V
3134
}
3235

33-
public final class com/zachklipp/compose/backstack/FrameController$BackstackFrame {
34-
public fun <init> (Ljava/lang/Object;Landroidx/compose/ui/Modifier;)V
35-
public synthetic fun <init> (Ljava/lang/Object;Landroidx/compose/ui/Modifier;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
36-
public final fun component1 ()Ljava/lang/Object;
36+
public final class com/zachklipp/compose/backstack/FrameController$FrameAndModifier {
37+
public fun <init> (Lcom/zachklipp/compose/backstack/BackstackFrame;Landroidx/compose/ui/Modifier;)V
38+
public synthetic fun <init> (Lcom/zachklipp/compose/backstack/BackstackFrame;Landroidx/compose/ui/Modifier;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
39+
public final fun component1 ()Lcom/zachklipp/compose/backstack/BackstackFrame;
3740
public final fun component2 ()Landroidx/compose/ui/Modifier;
38-
public final fun copy (Ljava/lang/Object;Landroidx/compose/ui/Modifier;)Lcom/zachklipp/compose/backstack/FrameController$BackstackFrame;
39-
public static synthetic fun copy$default (Lcom/zachklipp/compose/backstack/FrameController$BackstackFrame;Ljava/lang/Object;Landroidx/compose/ui/Modifier;ILjava/lang/Object;)Lcom/zachklipp/compose/backstack/FrameController$BackstackFrame;
41+
public final fun copy (Lcom/zachklipp/compose/backstack/BackstackFrame;Landroidx/compose/ui/Modifier;)Lcom/zachklipp/compose/backstack/FrameController$FrameAndModifier;
42+
public static synthetic fun copy$default (Lcom/zachklipp/compose/backstack/FrameController$FrameAndModifier;Lcom/zachklipp/compose/backstack/BackstackFrame;Landroidx/compose/ui/Modifier;ILjava/lang/Object;)Lcom/zachklipp/compose/backstack/FrameController$FrameAndModifier;
4043
public fun equals (Ljava/lang/Object;)Z
41-
public final fun getKey ()Ljava/lang/Object;
44+
public final fun getFrame ()Lcom/zachklipp/compose/backstack/BackstackFrame;
4245
public final fun getModifier ()Landroidx/compose/ui/Modifier;
4346
public fun hashCode ()I
4447
public fun toString ()Ljava/lang/String;

compose-backstack/src/androidTest/java/com/zachklipp/compose/backstack/BackstackStateTest.kt

+25-25
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@ import androidx.compose.runtime.saveable.rememberSaveable
1010
import androidx.compose.runtime.setValue
1111
import androidx.compose.ui.Modifier
1212
import androidx.compose.ui.test.assertIsDisplayed
13-
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
1413
import androidx.compose.ui.test.junit4.createComposeRule
1514
import androidx.compose.ui.test.onNodeWithText
1615
import androidx.compose.ui.test.performClick
17-
import androidx.test.ext.junit.rules.ActivityScenarioRule
1816
import com.google.common.truth.Truth.assertThat
1917
import org.junit.Rule
2018
import org.junit.Test
@@ -24,13 +22,15 @@ class BackstackStateTest {
2422
@get:Rule
2523
val compose = createComposeRule()
2624

25+
private fun List<String>.toCounters() = toBackstackModel {
26+
var counter by rememberSaveable { mutableStateOf(0) }
27+
BasicText("$it: $counter", Modifier.clickable { counter++ })
28+
}
29+
2730
@Test fun screen_state_is_restored_on_pop() {
2831
val backstack = mutableStateListOf("one")
2932
compose.setContent {
30-
Backstack(backstack, frameController = NoopFrameController()) {
31-
var counter by rememberSaveable { mutableStateOf(0) }
32-
BasicText("$it: $counter", Modifier.clickable { counter++ })
33-
}
33+
Backstack(backstack.toCounters(), frameController = NoopFrameController())
3434
}
3535

3636
// Update some state on the first screen.
@@ -55,10 +55,7 @@ class BackstackStateTest {
5555
@Test fun screen_state_is_discarded_after_pop() {
5656
val backstack = mutableStateListOf("one", "two")
5757
compose.setContent {
58-
Backstack(backstack, frameController = NoopFrameController()) {
59-
var counter by rememberSaveable { mutableStateOf(0) }
60-
BasicText("$it: $counter", Modifier.clickable { counter++ })
61-
}
58+
Backstack(backstack.toCounters(), frameController = NoopFrameController())
6259
}
6360

6461
// Update some state on the second screen.
@@ -78,10 +75,7 @@ class BackstackStateTest {
7875
@Test fun screen_state_is_discarded_when_removed_from_backstack_while_hidden() {
7976
var backstack by mutableStateOf(listOf("one"))
8077
compose.setContent {
81-
Backstack(backstack, frameController = NoopFrameController()) {
82-
var counter by rememberSaveable { mutableStateOf(0) }
83-
BasicText("$it: $counter", Modifier.clickable { counter++ })
84-
}
78+
Backstack(backstack.toCounters(), frameController = NoopFrameController())
8579
}
8680

8781
// Update some state on the first screen.
@@ -112,13 +106,16 @@ class BackstackStateTest {
112106
val backstack = mutableStateListOf("one")
113107
val transcript = mutableListOf<String>()
114108
compose.setContent {
115-
Backstack(backstack, frameController = NoopFrameController()) {
116-
BasicText(it)
117-
DisposableEffect(Unit) {
118-
transcript += "+$it"
119-
onDispose { transcript += "-$it" }
120-
}
121-
}
109+
Backstack(
110+
backstack.toBackstackModel {
111+
BasicText(it)
112+
DisposableEffect(Unit) {
113+
transcript += "+$it"
114+
onDispose { transcript += "-$it" }
115+
}
116+
},
117+
frameController = NoopFrameController()
118+
)
122119
}
123120

124121
assertThat(transcript).containsExactly("+one")
@@ -143,10 +140,13 @@ class BackstackStateTest {
143140

144141
val backstack = mutableStateListOf(Screen("one"))
145142
compose.setContent {
146-
Backstack(backstack, frameController = NoopFrameController()) {
147-
var counter by rememberSaveable { mutableStateOf(0) }
148-
BasicText("${it.name}: $counter", Modifier.clickable { counter++ })
149-
}
143+
Backstack(
144+
backstack.toBackstackModel {
145+
var counter by rememberSaveable { mutableStateOf(0) }
146+
BasicText("${it.name}: $counter", Modifier.clickable { counter++ })
147+
},
148+
frameController = NoopFrameController()
149+
)
150150
}
151151

152152
// Update some state on the first screen.

compose-backstack/src/androidTest/java/com/zachklipp/compose/backstack/BackstackTransitionsTest.kt

+8-4
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,12 @@ class BackstackTransitionsTest {
6363
assertTransition(Crossfade, forward = false)
6464
}
6565

66+
private fun List<String>.toBackstack() = toBackstackModel { BasicText(it) }
67+
6668
private fun assertInitialStateWithSingleScreen(transition: BackstackTransition) {
6769
val originalBackstack = listOf("one")
6870
compose.setContent {
69-
Backstack(originalBackstack, transition = transition) { BasicText(it) }
71+
Backstack(originalBackstack.toBackstack(), transition = transition)
7072
}
7173

7274
compose.onNodeWithText("one").assertIsDisplayed()
@@ -75,7 +77,7 @@ class BackstackTransitionsTest {
7577
private fun assertInitialStateWithMultipleScreens(transition: BackstackTransition) {
7678
val originalBackstack = listOf("one", "two")
7779
compose.setContent {
78-
Backstack(originalBackstack, transition = transition) { BasicText(it) }
80+
Backstack(originalBackstack.toBackstack(), transition = transition)
7981
}
8082

8183
compose.onNodeWithText("two").assertIsDisplayed()
@@ -87,15 +89,17 @@ class BackstackTransitionsTest {
8789
val secondBackstack = listOf("one", "two")
8890
var backstack by mutableStateOf(if (forward) firstBackstack else secondBackstack)
8991
compose.mainClock.autoAdvance = false
92+
9093
compose.setContent {
9194
Backstack(
92-
backstack,
95+
backstack.toBackstack(),
9396
frameController = rememberTransitionController(
9497
animationSpec = animation,
9598
transition = transition
9699
)
97-
) { BasicText(it) }
100+
)
98101
}
102+
99103
val initialText = if (forward) "one" else "two"
100104
val newText = if (forward) "two" else "one"
101105

0 commit comments

Comments
 (0)