Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jetpack Compose TopAppBar with dynamic actions

@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?

like image 360
JapCon Avatar asked Mar 05 '26 09:03

JapCon


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.

like image 117
TheFedex87 Avatar answered Mar 06 '26 22:03

TheFedex87


If your navigation is using compose-navigation, you can try this way.

  1. Define compose Components
import androidx.compose.foundation.layout.RowScope
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.compose.LocalOwnersProvider

@Composable
fun ProvideAppBarAction(actions: @Composable RowScope.() -> Unit) {
    if (LocalViewModelStoreOwner.current == null || LocalViewModelStoreOwner.current !is NavBackStackEntry)
        return
    val actionViewModel = viewModel(initializer = { TopAppBarViewModel() })
    SideEffect {
        actionViewModel.actionState = actions
    }
}

@Composable
fun ProvideAppBarTitle(title: @Composable () -> Unit) {
    if (LocalViewModelStoreOwner.current == null || LocalViewModelStoreOwner.current !is NavBackStackEntry)
        return
    val actionViewModel = viewModel(initializer = { TopAppBarViewModel() })
    SideEffect {
        actionViewModel.titleState = title
    }
}

@Composable
fun RowScope.AppBarAction(navBackStackEntry: NavBackStackEntry?) {
    val stateHolder = rememberSaveableStateHolder()
    navBackStackEntry?.LocalOwnersProvider(stateHolder) {
        val actionViewModel = viewModel(initializer = { TopAppBarViewModel() })
        actionViewModel.actionState?.let { it() }
    }
}


@Composable
fun AppBarTitle(navBackStackEntry: NavBackStackEntry?) {
    val stateHolder = rememberSaveableStateHolder()
    navBackStackEntry?.LocalOwnersProvider(stateHolder) {
        val actionViewModel = viewModel(initializer = { TopAppBarViewModel() })
        actionViewModel.titleState?.let { it() }
    }
}


private class TopAppBarViewModel : ViewModel() {

    var titleState by mutableStateOf(null as (@Composable () -> Unit)?, referentialEqualityPolicy())
    var actionState by mutableStateOf(null as (@Composable RowScope.() -> Unit)?, referentialEqualityPolicy())

}
  1. consume the composable components from appbar using viewModel
@Composable
fun MyTopAppBar(navController:NavController) {
    val currentContentBackStackEntry by produceState(
        initialValue = null as NavBackStackEntry?,
        producer = {
            navController.currentBackStackEntryFlow
                .filterNot { it.destination is FloatingWindow }
                .collect{ value = it }
        }
    )
    TopAppBar(
        navigationIcon = {
            val backPressDispatcher = LocalOnBackPressedDispatcherOwner.current
            IconButton(
                onClick = { backPressDispatcher?.onBackPressedDispatcher?.onBackPressed() },
                content = {
                    Icon(
                        imageVector = Icons.Default.ArrowBackIos,
                        contentDescription = "arrowBackIos"
                    )
                }
            )
        },
        title = {
            AppBarTitle(currentContentBackStackEntry)
        },
        actions = {
            AppBarAction(currentContentBackStackEntry)
        }
    )
}
  1. use the provider function inside the composable destination
fun NavGraphBuilder.buildGraph() {
    composable(route = "start1") {
        ProvideAppBarTitle { Text("1") }
        ProvideAppBarAction {
            Button(onClick = { /*TODO*/ }) {
                Text(text = "action1")
            }
            Button(onClick = { /*TODO*/ }) {
                Text(text = "action2")
            }
            Button(onClick = { /*TODO*/ }) {
                Text(text = "action3")
            }
        }
    }
    composable(route = "start2") {
        ProvideAppBarTitle{ Text("2") }
        ProvideAppBarAction{
            Button(onClick = { /*TODO*/ }) {
                Text(text = "action1")
            }
            Button(onClick = { /*TODO*/ }) {
                Text(text = "action2")
            }
            Button(onClick = { /*TODO*/ }) {
                Text(text = "action3")
            }
        }
    }
}
  • Detail explanation
  • Composable lamba is stored by the viewModel that is controlled by NavBackStackEntry
  • If we know the NavController, we can easily access the NavBackStackEntry
  • Actions and title can have their own state restored thx to NavBackStackEntry, Actions and Title composable are restored at the time the composesable destination restored.

side notes: You can wrap the AppBarTitle and AppBarAction with animation

@Composable
fun MyTopAppBar(navController:NavController) {
    val currentContentBackStackEntry by produceState(
        initialValue = null as NavBackStackEntry?,
        producer = {
            navController.currentBackStackEntryFlow
                .filterNot { it.destination is FloatingWindow }
                .collect{ value = it }
        }
    )
    TopAppBar(
        navigationIcon = {
            val backPressDispatcher = LocalOnBackPressedDispatcherOwner.current
            IconButton(
                onClick = { backPressDispatcher?.onBackPressedDispatcher?.onBackPressed() },
                content = {
                    Icon(
                        imageVector = Icons.Default.ArrowBackIos,
                        contentDescription = "arrowBackIos"
                    )
                }
            )
        },
        title = {
            Crossfade(targetState = currentContentBackStackEntry, label = "AppBarTitle") {
                if (it != null) {
                    AppBarTitle(it)
                }
            }
        },
        actions = {
            Crossfade(targetState = currentContentBackStackEntry, label = "AppBarActions") {
                if (it != null) {
                    Row{
                        AppBarAction(currentContentBackStackEntry)
                    }
                }
            }
        }
    )
}
like image 32
Watermelon Avatar answered Mar 06 '26 22:03

Watermelon



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!