4

Imagine the usual behavior of a toolbar in Android.

You define a Toolbar widget in the Activity, and can access it using onCreateOptionsMenu and onOptionsItemSelected inside your fragments.

However, something like this is not possible with normal Jetpack Compose, as there is no way of accessing the Toolbar that is defined in the Activity's Scaffold.

So think of this scenario. You have an Activity, with the Scaffold defined in it, and a NavHost inside that Scaffold. The NavHost contains all the sub-pages of your application (Other Composables). The title can be handled view the Navigation Destination Listener, what remains is the Actions of the Toolbar.

How would you change the toolbar actions depending on the current page/composables you're in? And handle clicks on these actions?

P.S : Using a Toolbar in each of the pages is not a solution, as it makes for a bad user experience when switching between animated pages, where the toolbar will disppear and reappear on each page.

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
Ahmad Sattout
  • 2,248
  • 1
  • 19
  • 42
  • actually you have to use different toolbars because if you are going to add animations between screen transactions (`AnimatedNavHost`) then during animation you may have the same title for two screens – user924 May 14 '23 at 19:22

2 Answers2

5

I used an interface I named ToolbarController which contained callback methods that could set the value for the variable(s) used in the the call to scaffold's TopAppBar:

@Composable  
fun MyApp(){  
  
 var toolbarTitle by remember{ mutableStateOf("") }  
  
 // ToolbarController would be some interface you have defined  
 val toolbarController = object: ToolbarController {  
        override fun setTitle(title: String){  
            toolbarTitle = title  
        }  
    }  
  
 Scaffold(  
    topBar = { 
       TopAppBar( title = { Text(text = toolbarTitle) }  )  
    }  
 ){  
    SomeScreen(toolbarController = toolbarController)  
 }  
}  
  
@Composable  
fun SomeScreen(  
    toolbarController: ToolbarController  
) {  
    //I'm not 100% sure I need to use an effect here, but I think so...
    //And I'm not sure this is the right one. It is not a coroutine I call,
    //but it of course works with normal calls. Also SideEffect runs on every
    //recompose according to the docs, and that's not what we want.
    //https://developer.android.com/jetpack/compose/side-effects
    LaunchedEffect(true){
       toolbarController.setTitle("Some screen title")  
    }
}

Edit: And it is easy to use it for any of the toolbar properties, you could create the interface like this:

interface ToolbarController{
    fun configToolbar(
        title: String = "",
        navigationIcon: IconButton? = null,
        actions: List<IconButton> = listOf()
    )
}

The point is that you just make callback functions and run them in LaunchedEffect. That is one way to set toolbar properties from within a composable in the scaffold. The interface stuff is just a way to group these callbacks so it don't get too messy.

  • The title is the easy part, I'm more wondering about the toolbar actions – Ahmad Sattout Jul 23 '21 at 05:47
  • @AhmadSattout You can use the exact same approach for the actions. I actually made a `ScaffoldController` where I keep the callbacks for all the toolbar stuff, FAB and snackbar. –  Jul 23 '21 at 08:41
  • you could use `rememberUpdatedState` so that the `LaunchedEffect` won't be called every time in recomposition – dumbfingers Nov 09 '21 at 12:03
0

You can use the TopAppBar at a Scaffold level and use your current destination to customize the topBar.

Something like:

topBar = {

    // Use your logic here
    val currentDestination = navBackStackEntry?.destination
    if (currentDestination == ....) {
      CustomAppBar()
    } else {
       TopAppBar(
          title = { /*...*/ },
          actions = {
                if (currentDestination == ....) {
                    IconButton(onClick = { /* doSomething() */ }) {
                        Icon(Icons.Filled.Favorite, contentDescription = "")
                    }
                }
                IconButton(onClick = { /* doSomething() */ }) {
                    Icon(Icons.Filled.Add, contentDescription = "")
                }

            }){ //... }
    }
}

Otherwise just use separate TopAppBar in each screen.

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
  • Yes but imagine having 10 pages, each with their own logic. 1- You will have to make 10 branches for a `when` 2- Each page should handle it's actions using it's own state. For example, imagine a List page, where there is Sorting, Filtering and Searching. These states cannot be access from the main composable – Ahmad Sattout Jun 10 '21 at 13:04
  • @AhmadSattout in this case use separate `TopAppBar` in each screen. – Gabriele Mariotti Jun 10 '21 at 13:05
  • 2
    That would look terrible in a User's experience, the Toolbar will disappear and reappear in a transition or animation between the pages – Ahmad Sattout Jun 10 '21 at 13:25
  • 1
    @AhmadSattout the correct way is to set top level Scaffold with drawer and bottom bar but for each screen, yes, you define separate Scaffold with its own toolbar, it's in official samples https://github.com/googlecodelabs/android-compose-codelabs/blob/end/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/JetnewsApp.kt#L54 – user924 Mar 23 '22 at 15:20