Instantly share code, notes, and snippets.
Last active
August 20, 2024 10:55
-
Star
(4)
4
You must be signed in to star a gist -
Fork
(2)
2
You must be signed in to fork a gist
-
Save fvilarino/664b8c2ece4d16cb234935642fa12315 to your computer and use it in GitHub Desktop.
Shared App Bar - Final
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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