Skip to content
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

Map-based backstack crashes when popping. #63

Open
rjrjr opened this issue Aug 26, 2022 · 1 comment · May be fixed by #64
Open

Map-based backstack crashes when popping. #63

rjrjr opened this issue Aug 26, 2022 · 1 comment · May be fixed by #64
Assignees

Comments

@rjrjr
Copy link
Owner

rjrjr commented Aug 26, 2022

If you modify BackstackTransitionsTest.assertTransition to build its backstacks out of Map<Int, String>, like so, you crash with NoSuchElementException when popping. We're popping to a list that no longer includes the information to paint the outgoing screen, which is a pretty realistic situation.

    val firstBackstack = mapOf(1 to "one")
    val secondBackstack = mapOf(1 to "one", 2 to "two")
    var backstack by mutableStateOf(if (forward) firstBackstack else secondBackstack)
    compose.mainClock.autoAdvance = false
    compose.setContent {
      Backstack(
        backstack.keys.toList(),
        frameController = rememberTransitionController(
          animationSpec = animation,
          transition = transition
        )
      ) { BasicText(backstack.getValue(it)) }
    }
@rjrjr rjrjr self-assigned this Aug 26, 2022
rjrjr added a commit that referenced this issue Aug 26, 2022
`BackstackTransitionsTest` now uses a map, to expose problems hidden when key and content are the same thing. As a result we crash when popping.

Reproduces #63.
rjrjr added a commit that referenced this issue Aug 26, 2022
`BackstackTransitionsTest` now uses a map, to expose problems hidden when key and content are the same thing. As a result we crash when popping.

Reproduces #63.
rjrjr added a commit that referenced this issue Aug 26, 2022
`BackstackTransitionsTest` now uses a map, to expose problems hidden when key and content are the same thing. As a result we crash when popping.

Reproduces #63.
rjrjr added a commit that referenced this issue Aug 26, 2022
`BackstackFrame` now holds on to both a `key` and a `@Composable` derived from the `content` function it was originally seen with. Previously during a pop we would try to display the outgoing `key` by passing it to the `content(T)` function passed with the updated `backstack` list.

Fixes #63
@rjrjr
Copy link
Owner Author

rjrjr commented Aug 26, 2022

No matter how I try to put BackstackFrame in charge of both the key and the @Composable (T) -> Unit, I can't get away from this same crash, where the popped key is passed to the updated lambda, which is unable to interpret it.

Looking at the stack trace, no matter how I package things up, the composable lambda passed to the original Backstack() call is always invoked directly by the recomposer with the updated map from the later call. Kind of smells like the Backstack() function as implemented makes it impossible to properly hoist state? Something like that.

I think I can make it work by changing the API to encourage the back stack elements to be self sufficient -- make them implement an interface similar to Workflow's ComposableRendering. Note that the existing unit tests and even the BackstackViewerApp effectively make that assumption themselves. They're all built around List<String>, and the @Composable () -> Unit renders the String keys directly. They are never used to look something up in other state that can get out of sync.

I'm kind of sad about that because it'll make the API feel more OO and less Composesque, but I'm getting to the point where I don't believe it can be made to work any other way.

Basically:

fun interface WithContent {
  @Composable fun Content()
}

fun <T : WithContent> Backstack(
  backstack: List<T>,
  modifier: Modifier = Modifier,
  frameController: FrameController<T>
)

rjrjr added a commit that referenced this issue Aug 27, 2022
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)
  }
)
```

Fixes #63
rjrjr added a commit that referenced this issue Aug 27, 2022
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)
  }
)
```

Fixes #63
rjrjr added a commit that referenced this issue Aug 27, 2022
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)
  }
)
```

Fixes #63
rjrjr added a commit that referenced this issue Aug 27, 2022
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)
  }
)
```

Fixes #63
rjrjr added a commit that referenced this issue Aug 27, 2022
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
rjrjr added a commit that referenced this issue Aug 27, 2022
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
rjrjr added a commit that referenced this issue Aug 27, 2022
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant