I want to know what is Scaffold in jetpack compose with a BottomAppBar example can anyone help me
Scaffold
Scaffold allows you to implement a UI with the basic Material Design layout structure. Scaffold provides slots for the most common top-level Material components, such as TopAppBar, BottomAppBar, FloatingActionButton, and Drawer.
Something like:
val scaffoldState = rememberScaffoldState()
// Create a coroutineScope for the animation
val coroutineScope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
drawerContent = { Text("Drawer content") },
bottomBar = {
BottomAppBar(cutoutShape = CircleShape) {
IconButton(
onClick = {
coroutineScope.launch { scaffoldState.drawerState.open() }
}
) {
Icon(Icons.Filled.Menu, contentDescription = "....")
}
}
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = { Text("Action") },
onClick = { /* .... */ }
)
},
floatingActionButtonPosition = FabPosition.Center,
isFloatingActionButtonDocked = true,
content = { innerPadding ->
//....
}
)
Related
Im trying to properly apply a bottom window inset on the BottomNavigation composable. I have an edge to edge scaffold it looks like that:
When using:
modifier.navigationBarsPadding()
On the bottom navigation im getting the following:
I'm trying to achieve the following:
Scaffold(
modifier = Modifier
.background(brush = NanitColors.blueGradient),
topBar = {
topBar()
},
bottomBar = {
if (shouldShowBottomBar) {
BottomNavigationBar(navController)
}
},
scaffoldState = scaffoldState,
drawerContent = {
Drawer { route ->
scope.launch {
scaffoldState.drawerState.close()
}
navController.navigate(route.route) {
popUpTo(navController.graph.startDestinationId)
launchSingleTop = true
}
currentDestination = route
}
},
drawerGesturesEnabled = scaffoldState.drawerState.isOpen,
) { innerPadding ->
NavigationHost(navController = navController, modifier = Modifier.padding(innerPadding))
}
val topBar: #Composable () -> Unit = {
MainToolbar(
modifier = modifier.statusBarsPadding(),
title = title ?: "",
onMenuClicked = {
scope.launch {
scaffoldState.drawerState.open()
}
}
)
}
BottomNavigation(
modifier = modifier.navigationBarsPadding()
) {
items.forEach { item ->
BottomNavigationBarItem(
item = item,
isSelected = selectedItem?.route == item.route,
onSelectedChange = { onSelectedItemChange(item) }
)
}
}
You can use accompanist systemuicontroller to set color to navigation bar, use default color for other screens and add color just for the screen you need:
#Composable
fun AppTheme(
systemBarColor: Color = MaterialTheme.colors.background,
content: #Composable () -> Unit,
) {
...
SideEffect {
systemUiController.setNavigationBarColor(
color = systemBarColor,
darkIcons = useDarkIcons
)
}
}
I am currently using a navigation drawer in jetpack compose, but I have encountered a problem. When I added the navigation drawer to the mainDrawerScreen, it was displaying for every screen I added, in my case it said "Main Menu" for every screen composable when navigated to. This is not ideal.
I decided to remove TopAppBar from mainDrawerScreen and create a seperat scaffold with the TopAppBar for each composable screen. However, now the navigation does not work. I tried to create a trailing lambda for navigation but might have done it the wrong way. Anyone got a suggestion to why it is not opening the navDrawer ? Appreciate the feedback!
MainDrawerSCreen:
#SuppressLint("UnusedMaterialScaffoldPaddingParameter")
#Composable
fun MainDrawerScreen() {
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
val scope = rememberCoroutineScope()
val navController = rememberNavController()
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {
DrawerHeader()
DrawerLayout(scope = scope, scaffoldState = scaffoldState , navController = navController)
}
) {
Navigation(navController = navController) // TODO This controls the navigation between different screens
}
}
My seperate created TopAppBar:
#Composable
fun TopAppBar1(
scope: CoroutineScope,
scaffoldState: ScaffoldState,
text: String,
onIconClick: () -> Unit
) {
TopAppBar(
title = { Text(text = text, fontSize = 18.sp) },
navigationIcon = {
IconButton(onClick = {
scope.launch {
withContext(Dispatchers.IO) { // should I keep this coroutine on background thread ?
scaffoldState.drawerState.open()
}
}
}) {
Icon(
Icons.Filled.Menu, "Menu",
Modifier.clickable { onIconClick.invoke()})
}
},
backgroundColor = LightBlue,
contentColor = MaterialTheme.colors.onPrimary,
)
}
ProfileScreen:
#SuppressLint("UnusedMaterialScaffoldPaddingParameter")
#Composable
fun ProfileScreen1() {
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
val scope = rememberCoroutineScope()
Scaffold(
topBar = {
TopAppBar1(scope = scope, scaffoldState = scaffoldState, text = "Profile") {
scope.launch { scaffoldState.drawerState.currentValue } <----------- Trailing Lambda
}
},
content = {
ActualBackground() // Background
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) { // Content }
I show you what I did :
TopAppBar.kt :
#Composable
fun TopAppBarScreen(
title: String = "",
screen: String,
navController: NavController
) {
TopAppBar(
title = {
Text(
text = title,
style = Typography.h2
)
},
navigationIcon = {
IconButton(
onClick = {
navController.navigate(screen)
}
) {
Icon(Icons.Filled.ArrowBack, "backIcon")
}
},
backgroundColor = Color.Background,
contentColor = Color.BackgroundDarkGrey,
elevation = 0.dp,
)
}
Navigation.kt
#Composable
fun Navigation() {
val navController = rememberNavController()
val viewModel = hiltViewModel<CountrySelectorViewModel>()
val userViewModel = InputViewModel(LocalContext.current)
NavHost(
navController = navController,
startDestination = Screen.WELCOME_SCREEN
) {
composable(Screen.WELCOME_SCREEN) {
WelcomeScreen(navController = navController)
}
}
Screen :
object Screen {
const val WELCOME_SCREEN = "WelcomeScreen"
}
When I call my TopAppBar :
TopAppBarScreen(
screen = Screen.WELCOME_SCREEN,
navController = navController
)
Look at how the navigation works and it's the same if you are using a button to change screen.
I hope this can help you
I'm trying to implement ModalDrawer with Jetpack Compose. Now I'm using ModalDrawer composable function which requires two main parameters: DrawerContent and Content. This is what kind of result I get when I pass an empty content parameter and few elements inside a DrawerContent parameter. I get those two drawers, instead of one. It's really confusing.
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "Home") }
)
},
drawerContent = {
val drawerState = rememberDrawerState(DrawerValue.Open)
ModalDrawer(
drawerState = drawerState,
drawerContent = { MyDrawerContent() },
content = { }
)
},
drawerContentColor = MaterialTheme.colors.onSurface,
content = {}
)
Better to use or only ModalDrawer without scaffold or Scaffold with drawer content
because ModalDrawer already have drawerContent parameter so here is showing two drawers because of two drawer contents (nested).
Scaffold(
drawerContent = {
Text("Drawer title", modifier = Modifier.padding(16.dp))
Divider()
// Drawer items
}
) {
// Screen content
}
OR
ModalDrawer(
drawerState = drawerState,
drawerContent = {
Column {
Text("Text in Drawer")
Button(onClick = {
scope.launch {
drawerState.close()
}
}) {
Text("Close Drawer")
}
}
},
content = {
Column {
Text("Text in Bodycontext")
Button(onClick = {
scope.launch {
drawerState.open()
}
}) {
Text("Click to open")
}
}
}
)
I am writing an android application pure in compose and I am using scaffold in every screen to implement topBar, bottomBar, fab, etc.
My question is should I be using scaffold in every screen or just in MainActivity?
what are the best practices while using composables?
Can I use scaffold inside of scaffold ?
I have researched a lot but didn't find answer anywhere even jetpack compose sample apps do not provide anything about best practices to build an app in jetpack compose.
please can anyone help me.
My code looks like this
MainActivity
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PasswordManagerApp()
}
}
}
#Composable
fun PasswordManagerApp() {
val mainViewModel: MainViewModel = hiltViewModel()
val navController = rememberNavController()
val systemUiController = rememberSystemUiController()
val scaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()
Theme(
darkTheme = mainViewModel.storedAppTheme.value
) {
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = { scaffoldState.snackbarHostState }
) {
Box(modifier = Modifier) {
AppNavGraph(
mainViewModel = mainViewModel,
navController = navController,
scaffoldState = scaffoldState
)
DefaultSnackbar(
snackbarHostState = scaffoldState.snackbarHostState,
onDismiss = { scaffoldState.snackbarHostState.currentSnackbarData?.dismiss() },
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}
}
Screen 1:
#Composable
fun LoginsScreen(
...
) {
...
Scaffold(
topBar = {
HomeTopAppBar(
topAppBarTitle = LoginsScreen.AllLogins.label,
onMenuIconClick = {},
switchState = viewModel.switch.value,
onSwitchIconClick = { viewModel.setSwitch(it) },
onSettingsIconClick = {navigateToSettings()}
)
},
scaffoldState = scaffoldState,
snackbarHost = { scaffoldState.snackbarHostState },
floatingActionButton = {
MyFloatingBtn(
onClick = { navigateToNewItem() }
)
},
drawerContent = {
//MyDrawer()
},
bottomBar = {
MyBottomBar(
navController = navController,
currentRoute = currentRoute,
navigateToAllLogins = navigateToAllLogins,
navigateToAllCards = navigateToAllCards,
navigateToAllOthers = navigateToAllOthers,
)
},
floatingActionButtonPosition = FabPosition.End,
isFloatingActionButtonDocked = false,
) {
Box(modifier = Modifier.fillMaxSize()) {
Column(
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(bottom = 48.dp)
.verticalScroll(scrollState)
) {...}
}
}
Screen 2:
#Composable
fun CardsScreen(
...
) {
...
Scaffold(
topBar = {
HomeTopAppBar(
topAppBarTitle = CardsScreen.AllCards.label,
onMenuIconClick = {},
switchState = viewModel.switch.value,
onSwitchIconClick = { viewModel.setSwitch(it) },
onSettingsIconClick = {navigateToSettings()}
)
},
floatingActionButton = {
MyFloatingBtn(
onClick = { navigateToNewItem() })
},
drawerContent = {
//MyDrawer()
},
bottomBar = {
MyBottomBar(
navController = navController,
currentRoute = currentRoute,
navigateToAllLogins = navigateToAllLogins,
navigateToAllCards = navigateToAllCards,
navigateToAllOthers = navigateToAllOthers,
)
},
floatingActionButtonPosition = FabPosition.End,
isFloatingActionButtonDocked = false
) {
Column(
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(bottom = 48.dp)
.verticalScroll(scrollState)
) {...}
}
Like the others in comments, i didn't find any guidelines.
It looks like everything is possible :
Nested Scaffolds
One Scaffold per app
One Scaffold per page
At first sight, you should choose the solution that match your needs.
But...
Keep in mind that one Scaffold per app could be the "better" solution because :
It's the only way to keep Snackbars alive when navigating.
It's the easiest way to have advanced transition animation between screens. I mean controlling animation component by component the way you want during transition (top bar, bottom nav, floating action button, page content...). As well, default NavHost transition animations look better with this solution.
With that solution, you'll have to deal with having different top app bar, floating action button... per page. That's something you can solve by having a shared view state for these elements.
#Immutable
data class ScaffoldViewState(
#StringRes val topAppBarTitle: Int? = null,
#StringRes val fabText: Int? = null,
// TODO : ...etc (top app bar actions, nav icon...)
)
var scaffoldViewState by remember {
mutableStateOf(ScaffoldViewState())
}
Scaffold(
topBar = {
TopAppBar(
title = {
scaffoldViewState.topAppBarTitle?.let {
Text(text = stringResource(id = it))
}
}
)
},
floatingActionButton = {
// TODO : a bit like for topBar
}
) {
NavHost {
composable("a") {
scaffoldViewState = // TODO : choose the top app bar and fab appearance for this page
// TODO : your page here
}
composable("b") {
scaffoldViewState = // TODO : choose the top app bar and fab appearance for this page
// TODO : your page here
}
}
}
As it has been said in comments both approaches are valid. Sure, you can use Scaffold inside another Scaffold like in the sample Jetnews app by Google
If you want "nice" screen transitions while only content changes, then define the component in top level Scaffold (e.g. the Drawer menu is usually shared in the top level Scaffold). Toolbars / AppBars are easier to implement per screen (inner Scaffold), not only in Jetpack Compose, because usually they have different titles, action buttons, etc.
In a simple app without dedicated Toolbars only one Scaffold could be used.
I'm trying to design a layout using Compose that consists of:
TopAppBar
Body (content)
BottomAppBar
A Bottom Sheet that represents a menu when clicked (Modal Bottom Sheet)
-------TopAppBar-------
------MainContent------
------BottomAppBar-----
----ModalBottomSheet---
Compose offers 3 components:
Scaffold
BottomSheetScaffold
ModalBottomSheetLayout
Scaffold has no bottom sheet property
BottomSheetScaffold has no BottomAppBar property
ModalBottomSheetLayout has only content and sheetContent
Which of these components should I combine and in what **structure** to achieve what I want?
Scaffold(
topBar = { TopBar() },
content = { innerPadding -> Body(innerPadding) },
bottomAppbar = { BottomAppBar() }
)
ModalBottomSheetLayout(
sheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden
),
sheetContent = { SheetContent() },
)
BottomSheetScaffold(
scaffoldState = ...,
sheetContent = { SheetContent() },
content = { ScreenContent() },
)
You can use something like:
val bottomState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
ModalBottomSheetLayout(
sheetState = bottomState,
sheetContent = {
//. sheetContent
}
) {
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = {
Text(text = "TopAppBar")
}
)
},
bottomBar = {
BottomAppBar(modifier = Modifier) {
IconButton(
onClick = {
coroutineScope.launch { bottomState.show() }
}
) {
Icon(Icons.Filled.Menu, contentDescription = "Localized description")
}
}
},
content = { innerPadding ->
//...main content
}
)
}