Skip to content

Instantly share code, notes, and snippets.

@fvilarino
Last active August 20, 2024 10:55
Show Gist options
  • Save fvilarino/664b8c2ece4d16cb234935642fa12315 to your computer and use it in GitHub Desktop.
Save fvilarino/664b8c2ece4d16cb234935642fa12315 to your computer and use it in GitHub Desktop.
Shared App Bar - Final
val HomeRoute = "home"
val SettingsRoute = "settings"
val ManyOptionsRoute = "manyOptions"
val NoAppBarRoute = "noAppBar"
sealed interface Screen {
val route: String
val isAppBarVisible: Boolean
val navigationIcon: ImageVector?
val navigationIconContentDescription: String?
val onNavigationIconClick: (() -> Unit)?
val title: String
val actions: List<ActionMenuItem>
class Home : Screen {
override val route: String = HomeRoute
override val isAppBarVisible: Boolean = true
override val navigationIcon: ImageVector? = null
override val onNavigationIconClick: (() -> Unit)? = null
override val navigationIconContentDescription: String? = null
override val title: String = "Home"
override val actions: List<ActionMenuItem> = listOf(
ActionMenuItem.IconMenuItem.AlwaysShown(
title = "Settings",
onClick = {
_buttons.tryEmit(AppBarIcons.Settings)
},
icon = Icons.Filled.Settings,
contentDescription = null,
)
)
enum class AppBarIcons {
Settings
}
private val _buttons = MutableSharedFlow<AppBarIcons>(extraBufferCapacity = 1)
val buttons: Flow<AppBarIcons> = _buttons.asSharedFlow()
}
class Settings : Screen {
override val route: String = SettingsRoute
override val isAppBarVisible: Boolean = true
override val navigationIcon: ImageVector = Icons.Default.ArrowBack
override val onNavigationIconClick: () -> Unit = {
_buttons.tryEmit(AppBarIcons.NavigationIcon)
}
override val navigationIconContentDescription: String? = null
override val title: String = "Settings"
override val actions: List<ActionMenuItem> = emptyList()
enum class AppBarIcons {
NavigationIcon
}
private val _buttons = MutableSharedFlow<AppBarIcons>(extraBufferCapacity = 1)
val buttons: Flow<AppBarIcons> = _buttons.asSharedFlow()
}
class ManyOptionsScreen : Screen {
override val route: String = ManyOptionsRoute
override val isAppBarVisible: Boolean = true
override val navigationIcon: ImageVector = Icons.Default.ArrowBack
override val onNavigationIconClick: () -> Unit = {
_buttons.tryEmit(AppBarIcons.NavigationIcon)
}
override val navigationIconContentDescription: String? = null
override val title: String = "Many Options"
private var _favoriteIcon by mutableStateOf<ImageVector>(Icons.Default.FavoriteBorder)
override val actions: List<ActionMenuItem> by derivedStateOf {
listOf(
ActionMenuItem.IconMenuItem.AlwaysShown(
title = "Search",
onClick = {
_buttons.tryEmit(AppBarIcons.Search)
},
icon = Icons.Filled.Search,
contentDescription = null,
),
ActionMenuItem.IconMenuItem.AlwaysShown(
title = "Favorite",
onClick = {
_buttons.tryEmit(AppBarIcons.Favorite)
},
icon = _favoriteIcon,
contentDescription = null,
),
ActionMenuItem.IconMenuItem.ShownIfRoom(
title = "Star",
onClick = {
_buttons.tryEmit(AppBarIcons.Star)
},
icon = Icons.Filled.Star,
contentDescription = null,
),
ActionMenuItem.IconMenuItem.ShownIfRoom(
title = "Refresh",
onClick = {
_buttons.tryEmit(AppBarIcons.Refresh)
},
icon = Icons.Filled.Refresh,
contentDescription = null,
),
ActionMenuItem.NeverShown(
title = "Settings",
onClick = {
_buttons.tryEmit(AppBarIcons.Settings)
},
),
ActionMenuItem.NeverShown(
title = "About",
onClick = {
_buttons.tryEmit(AppBarIcons.About)
},
),
)
}
fun setFavoriteIcon(icon: ImageVector) {
_favoriteIcon = icon
}
enum class AppBarIcons {
NavigationIcon,
Search,
Favorite,
Star,
Refresh,
Settings,
About,
}
private val _buttons = MutableSharedFlow<AppBarIcons>(extraBufferCapacity = 1)
val buttons: Flow<AppBarIcons> = _buttons.asSharedFlow()
}
class NoAppBar : Screen {
override val route: String = NoAppBarRoute
override val isAppBarVisible: Boolean = false
override val navigationIcon: ImageVector? = null
override val onNavigationIconClick: (() -> Unit)? = null
override val navigationIconContentDescription: String? = null
override val title: String = ""
override val actions: List<ActionMenuItem> = emptyList()
}
}
fun getScreen(route: String?): Screen? = when (route) {
HomeRoute -> Screen.Home()
SettingsRoute -> Screen.Settings()
ManyOptionsRoute -> Screen.ManyOptionsScreen()
NoAppBarRoute -> Screen.NoAppBar()
else -> null
}
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PlaygroundTheme {
val navController = rememberNavController()
val snackbarHostState = remember { SnackbarHostState() }
val appBarState = rememberAppBarState(
navController,
)
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
if (appBarState.isVisible) {
PlaygroundTopAppBar(
appBarState = appBarState,
modifier = Modifier.fillMaxWidth(),
)
}
}
) { paddingValues ->
NavHost(
navController = navController,
startDestination = HomeRoute,
modifier = Modifier.padding(paddingValues)
) {
composable(
route = HomeRoute,
) {
HomeScreen(
appBarState = appBarState,
onSettingsClick = { navController.navigate(SettingsRoute) },
toNoAppBarScreen = { navController.navigate(NoAppBarRoute) },
toManyOptionsScreen = { navController.navigate(ManyOptionsRoute) },
modifier = Modifier.fillMaxSize(),
)
}
composable(
route = SettingsRoute,
) {
SettingsScreen(
appBarState = appBarState,
onBackClick = { navController.popBackStack() },
modifier = Modifier.fillMaxSize(),
)
}
composable(
route = ManyOptionsRoute,
) {
ManyOptionsScreen(
appBarState = appBarState,
snackbarHostState = snackbarHostState,
onBackClick = { navController.popBackStack() },
modifier = Modifier.fillMaxSize(),
)
}
composable(
route = NoAppBarRoute,
) {
NoAppBarScreen(
modifier = Modifier.fillMaxSize(),
)
}
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PlaygroundTopAppBar(
appBarState: AppBarState,
modifier: Modifier = Modifier,
) {
var menuExpanded by remember {
mutableStateOf(false)
}
TopAppBar(
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
),
navigationIcon = {
val icon = appBarState.navigationIcon
val callback = appBarState.onNavigationIconClick
if (icon != null) {
IconButton(onClick = { callback?.invoke() }) {
Icon(
imageVector = icon,
contentDescription = appBarState.navigationIconContentDescription
)
}
}
},
title = {
val title = appBarState.title
if (title.isNotEmpty()) {
Text(
text = title
)
}
},
actions = {
val items = appBarState.actions
if (items.isNotEmpty()) {
ActionsMenu(
items = items,
isOpen = menuExpanded,
onToggleOverflow = { menuExpanded = !menuExpanded },
maxVisibleItems = 3,
)
}
},
modifier = modifier
)
}
@Stable
class AppBarState(
navController: NavController,
scope: CoroutineScope,
) {
init {
navController.currentBackStackEntryFlow
.distinctUntilChanged()
.onEach { backStackEntry ->
val route = backStackEntry.destination.route
currentScreen = getScreen(route)
}
.launchIn(scope)
}
var currentScreen by mutableStateOf<Screen?>(null)
private set
val isVisible: Boolean
get() = currentScreen?.isAppBarVisible == true
val navigationIcon: ImageVector?
get() = currentScreen?.navigationIcon
val navigationIconContentDescription: String?
get() = currentScreen?.navigationIconContentDescription
val onNavigationIconClick: (() -> Unit)?
get() = currentScreen?.onNavigationIconClick
val title: String
get() = currentScreen?.title.orEmpty()
val actions: List<ActionMenuItem>
get() = currentScreen?.actions.orEmpty()
}
@Composable
fun rememberAppBarState(
navController: NavController,
scope: CoroutineScope = rememberCoroutineScope(),
) = remember {
AppBarState(
navController,
scope,
)
}
@Composable
fun HomeScreen(
appBarState: AppBarState,
onSettingsClick: () -> Unit,
toNoAppBarScreen: () -> Unit,
toManyOptionsScreen: () -> Unit,
modifier: Modifier = Modifier,
) {
val screen = appBarState.currentScreen as? Screen.Home
LaunchedEffect(key1 = screen) {
screen?.buttons?.onEach { button ->
when (button) {
Screen.Home.AppBarIcons.Settings -> onSettingsClick()
}
}?.launchIn(this)
}
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(
onClick = toManyOptionsScreen
) {
Text(
text = "Many action bar items screen"
)
}
Button(
onClick = toNoAppBarScreen
) {
Text(
text = "No app bar screen"
)
}
}
}
}
@Composable
fun SettingsScreen(
appBarState: AppBarState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val screen = appBarState.currentScreen as? Screen.Settings
LaunchedEffect(key1 = screen) {
screen?.buttons
?.onEach { button ->
when (button) {
Screen.Settings.AppBarIcons.NavigationIcon -> onBackClick()
}
}
?.launchIn(this)
}
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Text(
text = "Settings content"
)
}
}
@Composable
fun ManyOptionsScreen(
appBarState: AppBarState,
snackbarHostState: SnackbarHostState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val scope = rememberCoroutineScope()
var favoritesEnabled by remember { mutableStateOf(false) }
fun showSnackbar(text: String) {
scope.launch {
snackbarHostState.showSnackbar(
message = text
)
}
}
val screen = appBarState.currentScreen as? Screen.ManyOptionsScreen
LaunchedEffect(key1 = screen) {
snapshotFlow { favoritesEnabled }
.onEach { enabled ->
screen?.setFavoriteIcon(
if (enabled) Icons.Default.Favorite else Icons.Default.FavoriteBorder
)
}
.launchIn(this)
screen?.buttons
?.onEach { button ->
when (button) {
Screen.ManyOptionsScreen.AppBarIcons.NavigationIcon -> onBackClick()
Screen.ManyOptionsScreen.AppBarIcons.Search,
Screen.ManyOptionsScreen.AppBarIcons.Star,
Screen.ManyOptionsScreen.AppBarIcons.Refresh,
Screen.ManyOptionsScreen.AppBarIcons.Settings,
Screen.ManyOptionsScreen.AppBarIcons.About -> showSnackbar(
"Clicked on ${button.name}"
)
Screen.ManyOptionsScreen.AppBarIcons.Favorite -> {
favoritesEnabled = !favoritesEnabled
screen.setFavoriteIcon(
if (favoritesEnabled) Icons.Default.Favorite else Icons.Default.FavoriteBorder
)
}
}
}
?.launchIn(this)
}
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Text(
text = "Many options content"
)
}
}
@Composable
fun NoAppBarScreen(
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Text(
text = "No App Bar content"
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment