I'm making an app where I want to wrap some destin...
# compose-destinations
s
I'm making an app where I want to wrap some destinations in a common Scaffold, so only the content inside of the scaffold would change/animate when navigating. Placing a separate scaffold on each screen would animate those scaffolds as well. If this scaffold were common for the whole app, I'd do this by just wrapping the root
DestinationsNavHost
inside of that shared composable like the sample app does here: https://github.com/raamcosta/compose-destinations/blob/main/sample/src/main/java/com/ramcosta/destinations/sample/SampleApp.kt#L30
Copy code
CommonScaffold {
    DestinationsNavHost(...)
}
However, my scaffold is only shared for a handful of screens, not every screen in the app. Is there any support for this type of behaviour? An idea I had was to do something like this (half pseudo-code to get the idea across)
Copy code
RootComposable {
    if (onRouteWithCommonScaffold) {
        CommonScaffold {
            DestinationsNavHost(...)
        }
    } else {
        DestinationsNavHost(...)
    }
}
Is this the right way to implement this? Is there any harm in conditionally defining the
DestinationsNavHost
in two separate places like this, or can the library handle that?
i
Here's the last thread on this use case and why shared elements is exactly what you should be using for this going forward: https://kotlinlang.slack.com/archives/C06CS4UCQ10/p1719588491485179?thread_ts=1719554859.324029&cid=C06CS4UCQ10
s
I assumed this was a slightly different case because this OP wants to have a global scaffold, hide it for a deeper child, and then show it again for an even deeper child. In my use-case I want to just have this shared element for one (nested) level of the graph (or an entire nested navgraph if you will) instead of wrapping it around my entire root graph. Do the same things still apply in that case?
s
Shared elements let you just write each screen exactly as each screen wants to be rendered, and then if you happen to share any elements between any destination you can use shared elements for that without altering each screen itself, besides just adding the right ID at the right components. So yes, it fits your use case too, just like it does many others.
s
Interesting, will have a look at those then. I assumed my use case might have been slightly simpler because of the nesting but I guess not. Thanks, will look into shared elements!
How do we inject the
AnimatedContentScope
to our screens in this case? The basic usage docs from Android suggest
this@AnimatedContent
(which could be a dependency passed to the NavHost), but the navigation docs specifically use
this@composable
for this. Do we have to go with the second case and manually define all our routes using
composable {}
, or can we make the library inject this scope for us as well?
s
The AnimatedContent scope is given to you by the navigation library inside the composable lambda. That is the one you want to use for this.
s
Alright cheers.
s
It comes here https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigati[…]ain/java/androidx/navigation/compose/NavGraphBuilder.kt;l=208 And if you really want to pass those to child composables without having to explicitly do it in the args, there is an idea of passing those down through a CompositionLocal as is done in the jetsnack sample here with
Copy code
val LocalNavAnimatedVisibilityScope = compositionLocalOf<AnimatedVisibilityScope?> { null }
val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null }
s
I see. I'm totally fine doing it myself, it's not like I absolutely have to avoid doing so. Was just wondering if the lib could provide it, as I saw in the release notes on GitHub that there is a pretty clean solution for the SharedTransitionScope using the dependencies container. Afaik that approach is not very useful if you have to pass in the dependencies manually anyways in the
composable {}
call so I can't use it. ^^ Thanks for the info, will try to piece something together. Considering this is a fairly common use case now I think it might not hurt to add it to the docs under that
Common use cases
section, or just having a docs page that talks about how to integrate shared element transitions with the lib
@Stylianos Gakis I've spent some time working on this. One last question remaining: • If I understand correctly, I should create a new Scaffold on every screen, and use the sharedElement modifier to indicate which parts should carry over between screens • The element I want to keep across pages is my topbar • If I add the modifier to my topbar, my entire screen flickers and the topbar jumps up and down a bit before settling in (can send screen recording of this if you'd like). The thread linked above suggests this might be because there is an entire new scaffold on every screen. • If I instead add the modifier to my scaffold the flickering and jumping are solved, but the transition animation between screens is gone (presumably because the modifier looks at the entire scaffold, which includes the content inside of it, so it "correctly" doesn't animate it) How do I share my scaffold's topbar without screen flickering or other glitchy things, but keep the animation between screens?
s
Idk if it being a scaffold complicates this in a particular way. You could try just having a
Copy code
Column {
  MyTopAppBar()
  ScreenContent(Modifier.weight(1f, true))
}
Instead to make this simple for yourself. And inside that custom MyTopAppBar use the same ID for the shared element. And make sure your screens call that same composable. Yes it would be interesting to see your screen recording. The shared elements apis are still not stable, so using the latest alpha is your best bet with them, but or course that sometimes may come with bugs. I can't tell from the info I got so far if what you're experiencing is a bug or a misconfiguration
s
Using a column seems to have the same glitchy effect as the scaffold. Screen recordings below. Will send code snippet as well in case of misconfiguration. First one is having the modifier on the entire scaffold (looks good but no transition), second one has the modifier on the topbar only (flickering and the topbar jumps up and down)
Code for glitchy result.kt
s
Right, looks something like the window insets on the top app bar are removed while in the middle of the transition. If you just applied WindowInsets(0.dp) to your top app bar does the glitch still happen?
s
Yes it does
I'll change the background colour to something else so it's easier to see what's happening maybe
👍 1
Not very useful I think but this is passing
colors =
to the topbar
s
And if you pass
windowInsets = WindowInsets(0.dp),
the same exact thing happens? Also how is the text defined? Also, try without using the material3 TopAppBar, just do a
Box() { Text() }
instead to see if that also behaves weirdly. I still feel like the text is jumping up exactly the height of the status bar so insets are to blame here, but I am not 100% sure of how m3 topappbar internally lays out stuff
s
Text is just `Text()`with a background colour modifier for the sake of the video. Will try the box now
👍 1
When I pass windowinsets 0 the same thing happens but it's just a bit higher up (behind status bars), still jumping
s
After you try just a box, it'd be good to also note which navigation and animation dependency versions you're using. And if this behavior changes between what you're using and latest alpha, if you're not already on latest alpha
s
Box seems to remain in place (after some fiddling with windowinsets), flickering happens across the entire screen now though (including the box) instead of just the content. I'm on destinations 2.1.0 beta 9 and navigation 2.8 beta 05 (latest), will look up what the latest alpha is rn and try that
s
Can I see what you mean now with the box? Can you just send the entire code instead then? The fact that the box changes things is good, probably closer to the solution now. It may be that you're for example not using fillMaxSize() on the destination root composable or something completely unrelated.
s
Sure give me a sec
Am I correct that latest alpha is 08? That was released a few months ago, latest beta is 05 from last week. Should I stick with my beta version instead?
👍 1
(sending code in a min)
👀 1
s
Looks like there's no shared element animation here anymore 👀 And I don't see any vertical jumping anymore here
s
Yea the jumping is fixed with the box like I said (text is up there but maybe a bit hard to read because it's black). It's almost what I'm looking for except for the flickering that still happens, which now includes the box/topbar as well so it's more jarring.
animation.kt
I was making a minimal reproducible example to send you and I noticed that the flickering went away 🙄 it seems to have been caused by my transition animation. I wanted to have one screen slide out to the left and the next screen come in from the right (and vise-versa when navigating back) as you could kinda see in my glithcy recordings from earlier. Any idea where I went wrong? Note that this didn't happen before the shared element transition so it must be related in some way.
s
You mean the fact that the screen was sliding away towards the same direction as the other one was coming in instead of going away on the other direction? Or the text going up and down a bit? I don't see any other "glitch" otherwise here, I may be missing what you mean here.
s
The entire screen flashing white. That's the only issue left because the topbar jumping up and down was solved by the box. If I remove my animation then it goes away, but the entire point of using a shared element transition was to use an animation (otherwise I could've just used a new scaffold on every screen). Before the shared element transition it did not flash white.
s
Oh right, that's not a glitch or anything. That's just what your background looks like behind the NavHost. Since you got 2 screens you're animating between, and you are fading each one in and out, there will no matter what be some time where you can see behind both of them. It's typical to do something like
Copy code
Surface(color = YourTheme.colorScheme.background) {
  NavHost(...)
}
So that if this happens you still see your color scheme's background behind everything. And each screen you got should also use that same background color so that there's no distinction between the two
s
Ooooo okay that makes sense
Thanks for all the help, looks good now. I'll try to puzzle my own topbar together with that box :p And as I said perhaps this info wouldn't be bad to add to the docs, I can imagine I'm not the only person that'll come in here asking for this exact thing ^^
s
Glad to have helped! Yeah you are definitely not the first one. The only reason I even thought to suggest it to you is because I've dealt with this too in the past and I was just as puzzled as you were here. I do think there is room for something like this in the docs, but I can't say I've looked through everything, perhaps it is somewhere in there 😄 And if not, it may be hard to see where such a suggestion would fit. For the Box thing, since Box works while TopAppBar does not, I still believe it's a problem around insets. But yeah do play around with it and if you do figure out what it was and how to solve it please do share it here too if you remember. I will need to do this soon too when we migrate to these versions 😄
s
Where such a suggestion would fit
There's a Common Use cases section where I feel like it would fit. Either scaffolds/topbars specifically or just integration with shared element transitions. I'd argue it is a common use case if the question is so common 😄
s
Oh that's a fitting place then. I would file an issue here https://issuetracker.google.com/issues/new?component=409828&amp;template=1093757 and explain what you'd like to see in the docs
s
Oh I was referring to compose destinations docs instead of the android docs, my bad
s
Ah I see. I feel like the "don't forget to add a background behind the NavHost" idea is something that applies to the nav component in general, not just to destinations hence I suggested that. But yeah perhaps if Rafael reads this thread he can have a better idea of if he'd like something like this to be in the compose-destinations docs too.
s
Not just that, rather the integration with shared element transitions in general. There's a short mention of it in the release notes of one of the releases on GitHub but that's about it. Perhaps it's out of scope but it seems common enough to consider
s
Yes, since shared elements are still in experimental status afaik, perhaps more docs will come later. Right now there is this https://developer.android.com/develop/ui/compose/animation/shared-elements#understand-scopes and there are snippets in there that mention how one could use NavHost's provided AnimatedContentScope too for example in the place where they say: "// This could also be your top-level NavHost as this provides an AnimatedContentScope". And they mention how NavHost plays into this 2-3 times in that document. There are also some samples here https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigati[…]edElementSample&amp;ss=androidx%2Fplatform%2Fframeworks%2Fsupport if you just seach for "SharedElementSample" in cs.android.com. I often find nice things this way if I do not find things in the docs directly 😄 But more examples in the docs themselves would be welcome indeed.
s
Is there even a way to use that animated content scope now? In the android docs they use
this@composable
but that returns an animated visibility scope when using Destinations
Or does it not make a difference?
s
(here perhaps a good parenthesis to say that I don't use compose-destinations myself, so I don't know some details about it 😄) https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]ontentScope&amp;sq=&amp;ss=androidx%2Fplatform%2Fframeworks%2Fsupport
sealed interface AnimatedContentScope : AnimatedVisibilityScope
contentscope seems to be a visibilityscope itself. I am not sure why it is exposed this way in compose-destinations and if there's any particular reason about it to be honest 🤷
s
Ugh I'm not done yet. The box + surface worked fine when this was the start route in my app (first route that shows when launching the app), but the text inside of the box disappears when it isn't the start route (e.g. i navigate to it from my Home screen or something). I've disabled my animation in the video cause it's not relevant to the issue. Box is coloured red to show that the box does stay in place, just the contents of it don't. And yes I've tried adding a sharedElement modifier to the text as well but that didn't make any difference
I feel like this is a lot more frustrating than it has to be 😅 surely a sticky piece of text can't be this hard. The only thing I changed was changing the start route to another one.
s
Yeah so if the content actually changes, you want to apply a separate transform there too. You can for example make the box the shared element, since that is the same pixel for pixel, but the text should have a separate ID and probably use sharedBounds instead, as described here https://developer.android.com/develop/ui/compose/animation/shared-elements#shared-bounds
In a shared element transition, you may have many different things animating between state A and B. And each one can have its own animation. The example right below what I linked shows how in the cupcake case, the text and the image animate separately for example.
s
It still happens when adding sharedBounds to the text, and it also happens when the text does not change between screens (e.g. static hardcoded string). It also doesn't happen when
screen1
is the start route of the graph, only when it isn't
s
It being on the start destination does not feel like it could be the main reason behind it. Sounds quite improbable at least. Probably something else being wrong.
s
Probably, but that is the only thing I'm changing. I'll try to make another MRE
👍 1
Done. This is the entire app now, issue still occurs. When HomeScreen is the start the text disappears (you can see the text is hardcoded as well, so it never changes). When I move the
start=true
from HomeScreen to Screen1 the issue is solved. Adding a sharedBounds() to the text changes nothing. (also for some reason Slack keeps changing the language of my text snippet to C++ even though I've set it to Kotlin so excuse the highlighting being broken)
s
So doing
Copy code
Text(
 text = "HomeScreen is the start",
 modifier = Modifier.sharedBounds(sharedTransitionScope.rememberSharedContentState(key = "text"))
)
on your wrapper text changes nothing you say right? I can't think of something else for this either. There may be a bug too tbf in the current animation version you may be using atm. Also yeah I am out of my depth regarding what
start = true
might be doing. I still find it hard to think that it's the culprit, but as I said I've never used this so I'd rather not make any more assumptions here.
s
start = true
just changes the value in RootGraph.startRoute, which is passed to the navhost. I also don't think it's the culprit, but rather that there might be a bug when an item has to get created on app launch Vs at a later stage or something.
👍 1
SharedBounds indeed changes nothing. Should I make a bug report to the android navigation tracker?
👍 1
s
If you would do that, you'd probably have to make a repro with all latest dependencies, and no compose-destinations at all so that it does not draw the attention away from the real issue. But yeah an issue with a repro project would be amazing if it can be reproduced consistently.
s
Yep that's what I was gonna do, will make a repo for it today then
🌟 1
Done, thanks for the help anyways! Hopefully someone can help me pinpoint the issue.