Interesting approach to handling topbar content in...
# compose-destinations
d
Interesting approach to handling topbar content involving saving it's data in Destinations... I wonder if something like this could be done in compose-destinations? https://fvilarino.medium.com/shared-action-bar-in-jetpack-compose-6e02f8391c73
r
Don’t love certain details of the approach, but I can think of ways to help developers doing this kind of thing. So I don’t wanna say that it definitely won’t happen, but it’s not a priority right now
d
How do you do this currently?
r
Same way we would with normal jetpack compose navigation. Depends how we want to change the top bar. Personally I usually change these bars at the top level I setup the Scaffold in. I listen for current destination changes, and when it does I produce a different bar UI. I can even get the ViewModel using the current NavBackStackEntry (which will be the same instance as the screen has) and communicate with that VM as much as I need to.
Other approach is to make the top bar part of each screen. You can create some kind of screen template Composable which takes
topBar
,
mainContent
, etc.
Like:
Copy code
@Destination
@Composable
fun MyScreen() = ScreenTemplate(
    topBar = {
        MyScreenTopBar()
    },
    mainContent = {
        //...
    },
    // other areas potentially
)
ScreenTemplate can be sophisticated or it can be just a Column with a slot for top bar and one for main content.
d
That would mean recreating the scaffold on each screen and then where would we put the navhost?
r
You mean the second approach? No need to recreate the whole scaffold, you can leave the top bar empty where you define the Scaffold, and leave that area for each screen to handle.
it really depends... you can even make ScreenTemplate use Scaffold yes, I don't see any issue with that 🙂
d
Yeah, the second. I've read that some even use nested scaffolds... but I'd suppose transitions would load the whole scaffold and not just the middle area, which might not look as nice. And the first approach would need to manage screens in two places. In the xxxScreen composable and children AND in the scaffold level to listen to nav events.
r
any framework that helps users do this, will loose a bit of versatility. I like the freedom we have with Compose for these things. I remember specifically working with action bar in the XML days where you have very specific APIs and then struggling to do a simple thing. Back then I honestly thought, why not just give me a LinearLayout I can put my Views in 😄
With first approach, you can have two Composables in the MyScreen.kt file. One for top bar and one for main content. Then in the Scaffold, you just need a big when statement and call the corresponding TopBar. The only annoyance here is the big when statement which needs to be updated every time you delete or add a screen.. Currently with Compose Destinations, there is a sealed Destination which lets us do this in a safe way, using exhaustive when statement.
But I do understand if you have hundreds of screens, it will be very ugly
when
..
d
That's why I tried making a MutableState<MainScaffoldState> and passing it down to the screens. MainScaffoldState is a sealed interface with some common states. But I could totally imagine a library taking the most common uses in hand and just standardising this whole story. The unusual cases could always still be handled manually. It can even be an optional dependency. There's so many articles about this, and everybody does it differently, but in the end, it boils down to mostly the same structure. Yeah, and when the app gets bigger, then some kind of standardisation of the most common patterns could save TONS of boilerplate and group the logic in the screen instead of in multiple places.
r
If I was manually writing the code that Compose Destinations generates, I could easily add a
TopBar
Composable to the
DestinationSpec
interface.
And then by getting the Destination object that implements that interface, you can just call Composable TopBar in the Scaffold.
but with ksp plugin, it would need to be more generic somehow.. It's an interesting challenge to solve, maybe I will think about it a little 😛
d
Say:
Copy code
@Destination
@Composable
fun HomeScreen(
   topBar: @Composable () -> Unit,
...
could be used somehow... (
topBar
doesn't have to be hard-coded... one could specify any composable name, but maybe the allow composables could have an interface defined with some kind of annotation so that all the destinations can derive and override them.)? Or any other parameter passed like that, the challenge would be how to define a base interface of common composables that could be specified like that and then be generated to be called on the scaffold? But I guess with ksp this is probably not so realistic...
Another way (using something like a delegate class):
Copy code
@DestinationsBase
interface Screen {
  val topBar: @Composable () -> Unit = {
    // some default topBar
  }
}
This could maybe be done like the args delegate class to have some class to handle which screen to use... but this would lack the context in the actual Screen composable...
r
What about this:
Copy code
@DestinationExtension
interface TitleHolder {

    @get:Composable
    val title: String

}

@DestinationExtension
interface TopBarHolder {

    @Composable
    fun TopBar(navBackStackEntry: NavBackStackEntry)

}

object MyScreenExtensions : TitleHolder, TopBarHolder {
    
    override val title: String
        @Composable
        get() = TODO("Not yet implemented")

    @Composable
    override fun TopBar(navBackStackEntry: NavBackStackEntry) {
        
    }

}

@Composable
@Destination(
    extensions = [
        MyScreenExtensions::class
    ]
)
fun MyScreen() {}
which would make it so the generated
MyScreenDestination
would implement both
TopBarHolder
and
TitleHolder
(by delegating to MyScreenExtensions object in this case). Like:
Copy code
public object MyScreenDestination : DirectionDestinationSpec,
	TitleHolder by MyScreenExtensions,
	TopBarHolder by MyScreenExtensions
And then in the scaffold you could do:
Copy code
...
topBar = {
    (navBackStackEntry.destination() as? TopBarHolder)?.TopBar(navBackStackEntry)
}
...
where
navBackStackEntry
would be a state we can get like this:
navController.currentBackStackEntryFlow.asState(...)
d
Then
MyScreenExtensions
needs to be implemented for each screen?
r
Each screen that you want to make a TopBarHolder, yes
but that would always be the case, right? If you want to specify a different TopBar for each screen, each screen needs to specify it 😄
d
Say that the only thing that changes is the title, not the Topbar?
And really, that title might even depend on the screen content...
r
The cool part of this is that it's completely up to you
I gave two examples there, Tittle and TopBar (looking back, they probably don't make sense together ahah)
if you just want title, then just do TitleHolder, your screen extensions will only implement that
then in scaffold you can have a TopBar composable that happens to call the destination's title
And really, that title might even depend on the screen content...
Thats totally fine, you can make title a function (like I did with TopBar) that receives the NavBackStackEntry
from that NavBackStackEntry, you can get the ViewModel associated
and then collect state from it
it will be the same ViewModel instance the main screen Composable is communicating with
d
This sounds really not bad... and it might also solve the BottomBar problem in a similar fashion... The only ugly thing is the casting, and I'd maybe also add a
global
parameter in
@DestinationExtension
to specify if all the destinations should inherit from it (maybe excluding some, like dialogs, with a
NoDestinationExtension::class
placeholder in
extentions
...
Or even just an empty list in
extentions
or a
null
, that it shouldn't be included in
global
extensions
Would there be access to dependencies too?
r
This sounds really not bad... and it might also solve the BottomBar problem in a similar fashion...
Exactly that was my goal.. This is really just adding behaviour(i.e methods, fields, whatever) to your generated Destination. Any thing you want.
The only ugly thing is the casting, and I'd maybe also add a
global
parameter in
@DestinationExtension
to specify if all the destinations should inherit from it (maybe excluding some, like dialogs, with a
NoDestinationExtension::class
placeholder in
extentions
...
I was thinking something like this:
Copy code
annotation class DestinationExtension(
    val requireOn: KClass<out Annotation> = Nothing::class
)
and then in the title example:
Copy code
@DestinationExtension(
    requireOn = RootNavGraph::class
)
interface TitleHolder {

    @get:Composable
    val title: String

}
which means all destinations that belong directly or indirectly to RootNavGraph would be required to add an extension for TitleHolder or the build would fail at compile time.
This way you can add behaviour only to specific graphs or to every screen (if using the Root, then it's basically all screens).
this way, given there is safety, probably the cast is alright.. I guess I could also generate a function that does the cast for you so the ugly stays hidden 😄
in this case it could be:
Copy code
@Composable
fun NavBackStackEntry.title() = (this.destination() as TitleHolder).Title()
Don't love the name "extensions" (and "Holder" - although this one would be user's code, so whatever 😄)
but it needs to be something generic.. since it's not specific to TopBar, Title or anything really...
Would there be access to dependencies too?
The reason we need dependencies block for the main Composable part is that the library is calling your Composable for you. All these "extensions" are called by you, so any dependencies you need, you can just pass them down from the NavHost level until the Scaffold TopBar (for example).
👍 1
d
The thing that's missing in all this is that one would have to implement the interface for each screen (unless there's no dynamic content in it...), since each screen has a different view model and title in the TopBar or even options in the more menu of the topbar... I'm thinking that maybe you could also have a base class or interface for args of the screen composable that would be required in the extension and pass the interface to the extension definition and to the composable function in it.
That could allow to define a limited set of templates
r
I’m not sure I’m following.. can you illustrate with some pseudo code? 😅
d
Copy code
interface TitleArgHolder {
   val title: String
}

@DestinationExtension
interface TopBarHolder {

    @Composable
    fun TopBar(titleArgHolder: TitleArgHolder, navBackStackEntry: NavBackStackEntry)

}

@Destination(
    extensions = [
        MyScreenExtensions::class
    ]
)
@Composable
fun HomeScreen(
   // This goes into the args and overrides title in TitleArgHolder
   title: String,
   topBar: @Composable () -> Unit,
...
I didn't really think it out till the end, but this is the general idea... then MyScreenExtensions could be re-used for all screens that have
title
as an arg, w/o tons of boilerplate.
r
So title would be a navigation argument of HomeScreen and all other screens that use this extension, right? Just making sure I got it
d
yeah
It seems like there's already quite a bit of boilerplate, I'm really trying to reduce to the bear minimum... and encourage re-use.
The other point bothering me was that if you have to derive from the extension for a whole NavGraph, there are still some exceptions, like dialogs that should keep the last state of the scaffold...
The dialog is popping up over the previous screen, the topbar shouldn't change...
r
Hmm dialog exception is a good point. Still not sure about the nav argument common to multiple destinations thing.. I’ll explore both. Thanks for feedback 👍