-
-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
API overhaul because splitting the keys and the composables don't work. #64
base: main
Are you sure you want to change the base?
Conversation
8b55940
to
a3d85d7
Compare
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you're in a time crunch and I like the overall direction so approving. But this API feels very unidiomatic for Compose.
I really want to be able to write:
Backstack(listOfMyModels, key = { it.key }) { model ->
Content()
}
I think the only thing preventing you from making that API right now is that both the Backstack
function and the FrameController
class need to know of the frame type, and right now that would require passing the key function in twice. I think we could solve that by passing in a frame controller factory instead of the controller itself, and the factory would create a controller given the key function. Then Backstack
could instantiate the actual controller itself.
Anyway, that's quite a significant change, and I'm not 100% confident it is the right move either, so this is fine for now.
model: M, | ||
key: K, | ||
crossinline content: @Composable (M) -> Unit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need to take both the model and a function that takes the model? If you just take the function, the caller can just capture the model, right?
} | ||
} | ||
|
||
inline fun <reified M: Any> List<M>.toBackstackModel( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: either call this mapToBackstackModels or at least pluralize?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Counter nit: List is a Backstack model.
* with [Composable] code that can display them, suitable for use | ||
* with [Backstack]. | ||
*/ | ||
interface BackstackFrame<out K : Any> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I think you might notice better recomposition performance if you mark this interface as @Stable
. But also very possibly not, since it's going to always be in a List which is not stable. Would be something to measure/test.
|
||
/** | ||
* Models a frame in a Backstack, with a unique [key] to identify it, | ||
* and a [Content] function to display it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: it would be helpful to explain to maybe what the key is used for so that people would have some idea of what sort of thing to pass in.
|
||
/** | ||
* A frame controlled by a [FrameController], to be shown by [Backstack]. | ||
*/ | ||
@Immutable | ||
data class BackstackFrame<out T : Any>( | ||
val key: T, | ||
data class FrameAndModifier<out K : Any>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Modified(Backstack)Frame
?
val popping = toKey in fromKeys | ||
val fromFrame = fromFrames.last() | ||
val toFrame = toFrames.last() | ||
val popping = fromFrames.firstOrNull { it.key == toFrame.key } != null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
val popping = fromFrames.firstOrNull { it.key == toFrame.key } != null | |
val popping = fromFrames.any { it.key == toFrame.key } |
val progress = Animatable(0f) | ||
|
||
val fromVisibility = derivedStateOf { 1f - progress.value } | ||
val toVisibility = progress.asState() | ||
|
||
// Wrap modifier functions in each their own recompose scope so that if they read the visibility | ||
// (or any other state) directly, the modified node will actually be updated. | ||
@SuppressLint("UnnecessaryComposedModifier") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So glad I put that comment in – I hope i wrote a test too 😅
* | ||
* The [backstack] must follow some rules: | ||
* The [frames] list must follow some rules: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Frames list" is a lot less meaningful than "backstack", since the frame list semantically is a backstack, so I would prefer keeping backstack here.
* The [frames] list must follow some rules: | |
* The [backstack][frames] must follow some rules: |
* - Must always contain at least one item. | ||
* - Items in the stack must implement `equals` and not change over the lifetime of the screen. | ||
* If an item changes, it will be considered a new screen and any state held by the screen will | ||
* - Keys must implement `equals` and cannot change over the lifetime of the screen. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* - Keys must implement `equals` and cannot change over the lifetime of the screen. | |
* - [Keys][BackstackFrame.key] must implement `equals` and cannot change over the lifetime of the screen. |
following some talks & chats, I've landed on this PR. @rjrjr can you drop an update, just so I understand what's your current thinking on all this? I'm looking for a "pure Compose backstack container" solution (because of square/workflow-kotlin#750, square/workflow-kotlin#669) |
Wow, we left you dangling. Very sorry. I want to go back to that PR and clean up the API in the way that @zach-klippenstein suggested, but haven't managed to make time for it yet. What I should really do is ask him to pair with me. But the PR worked well as is, if you'd want to just fork. :/ |
All of our tests and demos were built using
String
as the key, withcontent
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:
And change the signature of the
Backstack()
function:Note that the param type,
K
, is still the type of the key, not the type of a particular flavor ofBackstackFrame
.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:
After:
Note that there are two flavors of
toBackstackModel
. The second one supports models with more interesting keys.Fixes #63