Jetpack Compose TopAppBar with dynamic actions

Viewed 1877
@Composable
fun TopAppBar(
    title: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    navigationIcon: @Composable (() -> Unit)? = null,
    actions: @Composable RowScope.() -> Unit = {},
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    elevation: Dp = AppBarDefaults.TopAppBarElevation
)

actions: @Composable RowScope.() -> Unit = {}

Usage Scenario: Using Compose Navigation to switch to different "screens", so the TopAppBar actions will be changed accordingly. Eg. Share buttons for content screen, Filter button for listing screen

Tried passing as a state to the TopAppBar's actions parameter, but having trouble to save the lambda block for the remember function.

val (actions, setActions) = rememberSaveable { mutableStateOf( appBarActions ) }

Want to change the app bar actions content dynamically. Any way to do it?

2 Answers

This the approach I used but I'm pretty new on compose, so I cannot be sure it is the correct approach.

Let's assume I have 2 screens: ScreenA and ScreenB They are handled by MainActivity screen. This is our MainActivity:

@ExperimentalComposeUiApi
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3Api::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CoolDrinksTheme {
                val navController = rememberNavController()

                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    var appBarState by remember {
                        mutableStateOf(AppBarState())
                    }

                    Scaffold(
                        topBar = {
                            SmallTopAppBar(
                                title = {
                                    Text(text = appBarState.title)
                                },
                                actions = {
                                    appBarState.actions?.invoke(this)
                                }
                            )
                        }
                    ) { values ->
                        NavHost(
                            navController = navController,
                            startDestination = "screen_a",
                            modifier = Modifier.padding(
                                values
                            )
                        ) {
                            composable("screen_a") {
                                ScreenA(
                                    onComposing = {
                                        appBarState = it
                                    },
                                    navController = navController
                                )
                            }
                            composable("screen_b") {
                                ScreenB(
                                    onComposing = {
                                        appBarState = it
                                    },
                                    navController = navController
                                )
                            }
                        }
                    }
                }
            }
        }
    }
}

As you can see I'm using a mutable state of a class which represents the state of our MainActivity (where the TopAppBar is declared and composed), in this example there is the title and the actions of our TopAppBar.

This mutable state is set with a callback function called inside the composition of each screen.

Here you can see the ScreenA

@Composable
fun ScreenA(
    onComposing: (AppBarState) -> Unit,
    navController: NavController
) {

    LaunchedEffect(key1 = true) {
        onComposing(
            AppBarState(
                title = "My Screen A",
                actions = {
                    IconButton(onClick = { }) {
                        Icon(
                            imageVector = Icons.Default.Favorite,
                            contentDescription = null
                        )
                    }
                    IconButton(onClick = { }) {
                        Icon(
                            imageVector = Icons.Default.Filter,
                            contentDescription = null
                        )
                    }
                }
            )
        )
    }


    Column(
        modifier = Modifier
            .fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Screen A"
        )
        Button(onClick = {
            navController.navigate("screen_b")
        }) {
            Text(text = "Navigate to Screen B")
        }
    }
}

And the ScreenB

@Composable
fun ScreenB(
    onComposing: (AppBarState) -> Unit,
    navController: NavController
) {

    LaunchedEffect(key1 = true) {
        onComposing(
            AppBarState(
                title = "My Screen B",
                actions = {
                    IconButton(onClick = { }) {
                        Icon(
                            imageVector = Icons.Default.Home,
                            contentDescription = null
                        )
                    }
                    IconButton(onClick = { }) {
                        Icon(
                            imageVector = Icons.Default.Delete,
                            contentDescription = null
                        )
                    }
                }
            )
        )
    }


    Column(
        modifier = Modifier
            .fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Screen B"
        )
        Button(onClick = {
            navController.popBackStack()
        }) {
            Text(text = "Navigate back to Screen A")
        }
    }
}

And finally this is the data class of our state:

data class AppBarState(
    val title: String = "",
    val actions: (@Composable RowScope.() -> Unit)? = null
)

In this way you have a dynamic appbar declared in the main activity but each screen is responsable to handle the content of the appbar.

First you need to add navigation dependency on you jetpack compose projects.

You can read the doc from this https://developer.android.com/jetpack/compose/navigation

def nav_version = "2.4.1"
implementation "androidx.navigation:navigation-compose:$nav_version"

Then define your screen in sealed class:

sealed class Screen(var icon: ImageVector, var route: String) {
    object ContentScreen: Screen(Icons.Default.Home, "home")
    object ListingScreen: Screen(Icons.Default.List, "list")
}

and this is the navigation function look like

@Composable
fun Navigation(paddingValues: PaddingValues, navController: NavHostController) {
    NavHost(navController, startDestination = Screen.ContentScreen.route, modifier = Modifier.padding(paddingValues)) {
        composable(Screen.ContentScreen.route) {
            //your screen content
        }
        composable(Screen.ListingScreen.route) {
            //your listing screen here
        }
    }
}

Finally in your mainactivity class

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TestAppTheme {
                val navController = rememberNavController()
                val navBackStackEntry by navController.currentBackStackEntryAsState()
                val currentRoute = navBackStackEntry?.destination?.route
                Scaffold(
                    topBar = {
                        TopAppBar(title = { Text(text = "main screen") }, actions = {
                            if (currentRoute == Screen.ContentScreen.route) {
                                //your share button action here
                            } else if (currentRoute == Screen.ListingScreen.route) {
                                //your filter button here
                            } else {
                                //other action
                            }
                        })
                    }
                ) {
                    Navigation(paddingValues = it, navController = navController)
                }
            }
        }
    }

I'm so sorry if the explanation to sort, because the limitation of my English

Related