https://kotlinlang.org logo
#ballast
Title
# ballast
u

ubuntudroid

11/02/2023, 12:33 PM
It would be great if
RouterContract.Events.BackstackChanged
would also pass the old backstack or inform about what exactly has changed. This would enable us to do things like
Copy code
saveableStateHolder.removeState(it.originalDestinationUrl)
Because right now it doesn’t look like the router manages saveable state for us, right? So I did something like this:
Copy code
routerState.renderCurrentDestination(
   route = { screen ->
      val saveableStateProviderKey = remember(key1 = screen) {
         routerState.currentDestinationOrNotFound!!.originalDestinationUrl // seems to be the only way to get hold of the original destination URL
      }
      saveableStateHolder.SaveableStateProvider(key = saveableStateProviderKey) {
         when(screen) {
            Screen.Pastes -> {
               ...
            }
            Screen.Login -> {
               ...
            }
         }
      }
   },
   notFound = {
      ...
   }
)
c

Casey Brooks

11/02/2023, 3:13 PM
Having a better way to determine what changed from each Navigation event is something that I have thought about, but haven’t really come to a good solution yet. The main problem is that I would either need to 1. perform a diff on the old and new backstacks to determine what changed (which would be difficult to get right) 2. ask the user to include the “type of change” with each navigation request like
router.trySend(GoToDestination("/newRoute", NavigationDirection.Forward))
(which is cumbersome and adds extra boilerplate) 3. just ask the user to figure this out for themselves. #3 is the option I’ve opted for thus far. And to that point, if you need to compare the previous vs current backstacks on each change, it’s not difficult for you to do that yourself. An earlier version actually did include both the previous and current backstacks in the
BackstackChanged
event, but ultimately decided that it wasn’t all that useful on its own and pulled it out.
Also, I’ve intentionally created Ballast Navigation to not depend on Compose directly, so it doesn’t handle anything with saveableState or anything like that, and honestly I’m not even sure what would be needed to support that. But assuming you need the previous backstack to clear it when a new
BackstackChanged
event comes in, you can simply store the backstack as a variable in the EventHandler class. Events are processed sequentially so you will never need to worry about race conditions or anything like that, though it is running asynchronous with respect to the UI. So you might need to handle that kind of “clearing” logic from somewhere within the Compose UI, rather than the EventHandler.
u

ubuntudroid

11/02/2023, 3:47 PM
I agree it isn’t an easy problem and going with 3 makes sense. Storing the backstack as a variable will likely work, but it means duplicating state which I personally would like to avoid. To support Compose SaveableState the code I’ve posted above would essentially be enough for starters. But I obviously need to clear the state once the according destination disappears from the backstack, therefore my question.
c

Casey Brooks

11/02/2023, 4:10 PM
Yeah, duplicating the state isn’t a great solution, but I think you could use Flow operators to help somewhat. For example, something like this might work:
Copy code
fun <T: Any> Flow<T>.zipWithNext(): Flow<Pair<T?, T>> = flow {
    var prev: T? = null
    collect { value ->
        emit(prev to value)
        prev = value
    }
}

val zippedFlow: Flow<Pair<Backstack<Screen>?, Backstack<Screen>>> = remember(router) {
    router.observeStates().zipWithNext()
}
val value: Pair<Backstack<Screen>?, Backstack<Screen>>? by zippedFlow.collectAsState(initial = null)

if(value != null) {
    val (oldBackstack, currentBackstack) = value!!
    currentBackstack.renderCurrentDestination(
        route = { screen ->
            LaunchedEffect(key1 = screen) {
                // clear the values from the old backstack
            }

            when(screen) {
                Screen.Pastes -> {
                    PastesScreen.Content(router)
                }
                Screen.Login -> {
                    LoginScreen.Content(router)
                }
            }
        },
        notFound = {
            // TODO add "not found" screen
        }
    )
}
u

ubuntudroid

11/02/2023, 4:40 PM
Yes, that looks like a reasonable approach. Thanks for the snippet! 🙏