What is the best way to implement bottom sheet for multiple screens in jetpack compose? Do we have to define Bottom sheet layout in each screen? Then what to do if we wanted our bottom sheet to overlap on bottom nav bar?
You can create a custom layout like
MyAppCustomLayout(
showBottomBar: Boolean = false,
state: ModalBottomSheetState = ModalBottomSheetState(initialValue =
ModalBottomSheetValue.Hidden),
sheetContent: #Composable () -> Unit = {},
content: #Composable () -> Unit)
{
ModalBottomSheetLayout(
sheetState = state,
sheetContent = { sheetContent() })
{
Scaffold(
bottomBar = if(showBottomBar)
{{
YourBottomNavigationView()
}}
else {{}})
{ content() }
}
}
And use it anywhere in your app like below.
val state = rememberModalBottomSheetState()
MyAppCustomLayout(
state = state,
sheetcontent = {
Column {
Text("Some bottomSheet content")
}
})
{
Column {
Text("Some content")
}
}
If you are using Jetpack Navigation Compose in your project, you might consider using Jetpack Navigation Compose Material to implement it.
For more detail, refer to the samples
Related
I have a requirment in my jetpack compose app where i need to display modal bottom sheet and persistent bottom sheet and bottom navigation
How can i use all these three in a single screen
I have tried this layout based on this reference
I have below nested layout structure
ModalBottomSheetLayout
|- BottomSheetScaffold
|- Scaffold
|- BottomNavigation
when i use this structue i am facing issue with closing and opening of the modal bottom sheet and persistent bottom sheet
How can i achive this layout with all three ?
CODE
ModalBottomSheetLayout(
sheetContent = {
// MODAL SHEET CONTENT
},
sheetState = modalBottomSheetState,
) {
BottomSheetScaffold(
sheetContent = {
// BOTTOM SHEET CONTENT
},
scaffoldState = bottomSheetScaffoldState,
sheetPeekHeight = 0.dp,
) {
Scaffold(
scaffoldState = scaffoldWithDrawerState,
drawerContent = {
// DRAWER CONTENT
},
bottomBar = {
BottomBar(navController = navController)
},
) {
// NAV HOST FOR THE BOTTOM NAVIGATION SCREENS
}
}
}
Before Jetpack Compose I was using Navigation Component in the projects in View system world.
Apps had only one activity - toolbar, bottom bar and drawer were added only to this activity once.
Apps could have many screens (fragments) and only top destination fragments were displaying bottom bar and allowed drawer, for other fragments it was hidden.
All of that was handled with Navigation Component like this from the activity:
fun initNavigation() {
val topLevelDestinationFragments = setOf(R.id.homeFragment, R.id.photosFragment)
appBarConfiguration = AppBarConfiguration(
topLevelDestinationFragments,
binding.drawerLayout
)
setupActionBarWithNavController(navController, appBarConfiguration)
binding.drawerNavigationView.setupWithNavController(navController)
binding.bottomNavigationView.setupWithNavController(navController)
navController.addOnDestinationChangedListener { _, destination, _ ->
// don't change bars if a dialog fragment
if (destination is FloatingWindow) {
return#addOnDestinationChangedListener
}
// Google solution to hide navigation bars
// https://developer.android.com/guide/navigation/navigation-ui#listen_for_navigation_events
val allowBottomAndDrawerNavigation = destination.id in topLevelDestinationFragments
binding.bottomNavigationView.isVisible = allowBottomAndDrawerNavigation
binding.drawerLayout.setDrawerLockMode(
if (allowBottomAndDrawerNavigation) {
DrawerLayout.LOCK_MODE_UNLOCKED
} else {
DrawerLayout.LOCK_MODE_LOCKED_CLOSED
}
)
binding.toolbar.isVisible = // if we need to hide toolbar for specific fragments
}
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
Quite easy, no need to add separately toolbar for each fragment and so on (so it was added only to the layout of the activity, it wasn't added to each layout of each fragment)
Navigation Component automatically handles back and menu (for drawer) buttons on the toolbar, automatically switches between them because it knows top destinations
Is there something similar for Compose?
Because I checked official Google sample "JetNews" from CodeLabs git https://github.com/googlecodelabs/android-compose-codelabs/tree/end/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui
They use compose navigation there but they separately added Scaffold for each compose screen.
For example "Home" compose screen has it own Scaffold
Scaffold(
scaffoldState = scaffoldState,
topBar = {
val title = stringResource(id = R.string.app_name)
InsetAwareTopAppBar(
title = { Text(text = title) },
navigationIcon = {
IconButton(onClick = { coroutineScope.launch { openDrawer() } }) {
Icon(
painter = painterResource(R.drawable.ic_jetnews_logo),
contentDescription = stringResource(R.string.cd_open_navigation_drawer)
)
}
}
)
}
)
And "Article" compose screen has it own Scaffold
Scaffold(
topBar = {
InsetAwareTopAppBar(
title = {
Text(
text = "Published in: ${post.publication?.name}",
style = MaterialTheme.typography.subtitle2,
color = LocalContentColor.current
)
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
// Step 4: Content descriptions
contentDescription = stringResource(
R.string.cd_navigate_up
)
)
}
}
)
}
)
So basically here we duplicate the code and define manually different logic for navigationIcon (icon and action) of toolbar
Does it mean that if we have some details screens with back arrow button and without bottom bar then we define a separate Scaffold and can't just use one Scaffold for all compose screens of the app?
Or can we implement the same logic as well for all compose screens as we did with Navigation Component in View system? Also setting top destinations, hiding bottom bar and locking drawer for non top destinations.
I guess the correct way is to set top level Scaffold (where we define theme and navigation) with AppDrawer and BottomBar and add some logic if it should be added to a screen by checking the current navigation route:
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
e.g.:
drawerContent = if (isTopLevelDestination) {
{
AppDrawer(
...
)
}
} else {
null
},
bottomBar = {
if (isTopLevelDestination) {
AppBottomBar(
...
)
}
}
But with TopAppBar it's not that easy. We can't really add it to top level Scaffold in case when toolbar can have actions for specific screens
That's why each screen additionally defines its Scaffold with TopAppBar configured as needed.
Only if an app is very simple and toolbar doesn't have any actions for all screens of the app, just hardcoded titles for all screens, then we can define TopAppBar in top level Scaffold but for more complicated apps each screen should define its own toolbar
I'm building Android app with Jetpack Compose. Got stuck while trying to implement BottomAppBar with BottomDrawer pattern.
Bottom navigation drawers are modal drawers that are anchored to the bottom of the screen instead of the left or right edge. They are only used with bottom app bars. These drawers open upon tapping the navigation menu icon in the bottom app bar.
Description on material.io, and direct link to video.
I've tried using Scaffold, but it only supports side drawer. BottomDrawer appended to Scaffold content is displayed in content area and BottomDrawer doesn't cover BottomAppBar when open. Moving BottomDrawer after Scaffold function doesn't help either: BottomAppBar is covered by some invisible block and prevents clicking buttons.
I've also tried using BottomSheetScaffold, but it doesn't have BottomAppBar slot.
If Scaffold doesn't support this pattern, what would be correct way to implement it? Is it possible to extend Scaffold component? I fear that incorrect implementation from scratch might create issues later, when I'll try to implement navigation and snackbar.
I think the latest version of scaffold does have a bottom app bar parameter
They (Google Devs) invite you in the Jetpack Compose Layouts pathway to try adding other Material Design Components such as BottomNavigation or BottomDrawer to their respective Scaffold slots, and yet do not give you the solution.
BottomAppBar does have its own slot in Scaffold (i.e. bottomBar), but BottomDrawer does not - and seems to be designed exclusively for use with the BottomAppBar explicitly (see API documentation for the BottomDrawer).
At this point in the Jetpack Compose pathway, we've covered state hoisting, slots, modifiers, and have been thoroughly explained that from time to time we'll have to play around to see how best to stack and organize Composables - that they almost always have a naturally expressable way in which they work best that is practically intended.
Let me get us set up so that we are on the same page:
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LayoutsCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
LayoutsCodelab()
}
}
}
}
}
That's the main activity calling our primary/core Composable. This is just like in the codelab with the exception of the #ExperimentalMaterialApi annotation.
Next is our primary/core Composable:
#ExperimentalMaterialApi
#Composable
fun LayoutsCodelab() {
val ( gesturesEnabled, toggleGesturesEnabled ) = remember { mutableStateOf( true ) }
val scope = rememberCoroutineScope()
val drawerState = rememberBottomDrawerState( BottomDrawerValue.Closed )
// BottomDrawer has to be the true core of our layout
BottomDrawer(
gesturesEnabled = gesturesEnabled,
drawerState = drawerState,
drawerContent = {
Button(
modifier = Modifier.align( Alignment.CenterHorizontally ).padding( top = 16.dp ),
onClick = { scope.launch { drawerState.close() } },
content = { Text( "Close Drawer" ) }
)
LazyColumn {
items( 25 ) {
ListItem(
text = { Text( "Item $it" ) },
icon = {
Icon(
Icons.Default.Favorite,
contentDescription = "Localized description"
)
}
)
}
}
},
// The API describes this member as "the content of the
// rest of the UI"
content = {
// So let's place the Scaffold here
Scaffold(
topBar = {
AppBarContent()
},
//drawerContent = { BottomBar() } // <-- Will implement a side drawer
bottomBar = {
BottomBarContent(
coroutineScope = scope,
drawerState = drawerState
)
},
) {
innerPadding ->
BodyContent( Modifier.padding( innerPadding ).fillMaxHeight() )
}
}
)
}
Here, we've leveraged the Scaffold exactly as the codelab in the compose pathway suggests we should. Notice my comment that drawerContent is an auto-implementation of the side-drawer. It's a rather nifty way to bypass directly using the [respective] Composable(s) (material design's modal drawer/sheet)! However, it won't work for our BottomDrawer. I think the API is experimental for BottomDrawer, because they'll be making changes to add support for it to Composables like Scaffold in the future.
I base that on how difficult it is to use the BottomDrawer, designed for use solely with BottomAppBar, with the Scaffold - which explicitly contains a slot for BottomAppBar.
To support BottomDrawer, we have to understand that it is an underlying layout controller that wraps the entire app's UI, preventing interaction with anything but its drawerContent when the drawer is open. This requires that it encompasses Scaffold, and that requires that we delegate necessary state control - to the BottomBarContent composable which wraps our BottomAppBar implementation:
#ExperimentalMaterialApi
#Composable
fun BottomBarContent( modifier: Modifier = Modifier, coroutineScope: CoroutineScope, drawerState: BottomDrawerState ) {
BottomAppBar{
// Leading icons should typically have a high content alpha
CompositionLocalProvider( LocalContentAlpha provides ContentAlpha.high ) {
IconButton(
onClick = {
coroutineScope.launch { drawerState.open() }
}
) {
Icon( Icons.Filled.Menu, contentDescription = "Localized description" )
}
}
// The actions should be at the end of the BottomAppBar. They use the default medium
// content alpha provided by BottomAppBar
Spacer( Modifier.weight( 1f, true ) )
IconButton( onClick = { /* doSomething() */ } ) {
Icon( Icons.Filled.Favorite, contentDescription = "Localized description" )
}
IconButton( onClick = { /* doSomething() */ } ) {
Icon( Icons.Filled.Favorite, contentDescription = "Localized description" )
}
}
}
The result shows us:
The TopAppBar at top,
The BottomAppBar at bottom,
Clicking the menu icon in the BottomAppBar opens our BottomDrawer, covering the BottomAppBar and entire content space appropriately while open.
The BottomDrawer is properly hidden, until either the above referenced button click - or gesture - is utilized to open the bottom drawer.
The menu icon in the BottomAppBar opens the drawer partway.
Gesture opens the bottom drawer partway with a quick short swipe, but as far as you guide it to otherwise.
You may have to do something as shown below..
Notice how the Scaffold is called inside the BottomDrawer().
It's confusing though how the documentation says "They (BottomDrawer) are only used with bottom app bars". It made me think I have to look for a BottomDrawer() slot inside Scaffold or that I have to call BottomDrawer() inside BottomAppBar(). In both cases, I experienced weird behaviours. This is how I worked around the issue. I hope it helps someone especially if you are attempting the code lab exercise in Module 5 of Layouts in Jetpack Compose from the Jetpack Compose course.
#ExperimentalMaterialApi
#Composable
fun MyApp() {
var selectedItem by rememberSaveable { mutableStateOf(1)}
BottomDrawer(
modifier = Modifier.background(MaterialTheme.colors.onPrimary),
drawerShape = Shapes.medium,
drawerContent = {
Column(Modifier.fillMaxWidth()) {
for(i in 1..6) {
when (i) {
1 -> Row(modifier = Modifier.clickable { }.padding(16.dp)){
Icon(imageVector = Icons.Rounded.Inbox, contentDescription = null)
Text(text = "Inbox")
}
2 -> Row(modifier = Modifier.clickable { }.padding(16.dp)){
Icon(imageVector = Icons.Rounded.Outbox, contentDescription = null)
Text(text = "Outbox")
}
3 -> Row(modifier = Modifier.clickable { }.padding(16.dp)){
Icon(imageVector = Icons.Rounded.Archive, contentDescription = null)
Text(text = "Archive")
}
}
}
}
},
gesturesEnabled = true
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "Learning Compose Layouts" )
},
actions = {
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
}
)
},
bottomBar = { BottomAppBar(cutoutShape = CircleShape, contentPadding = PaddingValues(0.dp)) {
for (item in 1..4) {
BottomNavigationItem(
modifier = Modifier.clipToBounds(),
selected = selectedItem == item ,
onClick = { selectedItem = item },
icon = {
when (item) {
1 -> { Icon(Icons.Rounded.MusicNote, contentDescription = null) }
2 -> { Icon(Icons.Rounded.BookmarkAdd, contentDescription = null) }
3 -> { Icon(Icons.Rounded.SportsBasketball, contentDescription = null) }
4 -> { Icon(Icons.Rounded.ShoppingCart, contentDescription = null) }
}
}
)
}
}
}
) { innerPadding -> BodyContent(
Modifier
.padding(innerPadding)
.padding(8.dp))
}
}
}
I have a BottomNavBar which is called inside the bottomBar of a Scaffold.
Every screen contains a LazyColumn which displays a bunch of images.
For some reason when I finish scrolling, the BottomNavBar overlaps the lower part of the items list.
How can I fix this?
Here the set content of MainActivity
SetContent{
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "tartufozon") },
actions = {
IconButton(onClick = { Timber.d("Mail clicked") }) {
Icon(Icons.Default.Email, contentDescription = null)
}
}
)
},
bottomBar = {
BottomNavBar(navController = navController)
}
) {
BottomNavScreensController(navController = navController)
}
}
As per the API definition for Scaffold, your inner content (the trailing lambda you have your BottomNavScreensController in), is given a PaddingValues object that gives you the right amount of padding for your top app bar, bottom bar, etc.
Right now, you're not referencing that padding at all and hence, your content is not padded in. This is what is causing your overlap.
You can add a Box around your BottomNavScreensController to apply the padding, or pass the padding directly into your BottomNavScreensController so that each individual screen can correctly apply the padding; either solution works.
Scaffold(
topBar = {
//
},
bottomBar = {
//
}
) { innerPadding ->
// Apply the padding globally to the whole BottomNavScreensController
Box(modifier = Modifier.padding(innerPadding)) {
BottomNavScreensController(navController = navController)
}
}
}
Following ianhanniballake's answer and its comments and just to save you few minutes. The code would be something like:
Scaffold(
topBar = {
//
},
bottomBar = {
//
}
) { innerPadding ->
Box(modifier = Modifier.padding(
PaddingValues(0.dp, 0.dp, 0.dp, innerPadding.calculateBottomPadding()) {
BottomNavScreensController(navController = navController)
}
}
}
You no longer need to do any calculations. In the scaffold content, do:
content = { padding ->
Column(
modifier = Modifier.padding(padding)
) {...
Scaffold(
bottomBar = {
BottomNavigationBar(navController = navController)
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
DestinationsNavHost(
navGraph = NavGraphs.root,
navController = navController,
engine = navHostEngine
)
}
}
Add your layouts and views in content with its padding like in the example:
Scaffold(
content = {
Box(modifier = Modifier.padding(it)) {
//add your layout here
}
},
topBar = {
//your top bar
},
floatingActionButton = {
//your floating action bar
},
bottomBar = {
//your bottom navigation
}
)
Following ianhanniballake answer, if you have a list in your main screen and want to show/hide the bottom bar. It will perform unexpected behavior when you back to list screen (the scroll state is in very bottom) and show the navigation bar.The list item will scroll up a bit which cause the list doesn't fully scrolled to the bottom.
This is because of when you back to the list screen and want to show the bottom bar, the list already there. Then the bottom bar will show and list will not calculate the innerPadding for more.
To handle this, just pass the innerPadding into the list screen like this:
Scaffold(
bottomBar = {
BottomNavBar(navController)
}) { innerPadding ->
NavHost(
navController = navController,
startDestination = MovieListDirections.destination
) {
MovieListDirections.screenWithPaddingBottomBar(
this,
innerPadding.calculateBottomPadding() // pass here
)
}
Then in the list screen
#Composable
fun MovieListMainView(viewModel: MovieListViewModel = hiltViewModel(),
bottomBarHeight: Float) {
val movieList by viewModel.movieList.collectAsState()
var navBarHeight by rememberSaveable { mutableStateOf(0f) }
if (bottomBarHeight != 0f) {
navBarHeight = bottomBarHeight
}
MovieListLayout().MovieGridLayout(
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, navBarHeight.dp),
movies = movieList.movieList.movieItems,
onItemClick = {
viewModel.onMovieClicked(it)
})
}
I need to add a Toolbar in my Android application with a List like below. I am using Jetpack Compose to create the UI. Below is the composable function i am using for drawing the main view.
#Composable
fun HomeScreenApp() {
showPetsList(dogs = dogData)
}
You can use the TopAppBar.
The best way is to use the Scaffold. Something like:
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "TopAppBar")
},
navigationIcon = {
IconButton(onClick = { }) {
Icon(Icons.Filled.Menu,"")
}
},
backgroundColor = ....,
contentColor = ....
)
}, content = {
})
In Jetpack compose Toolbar can be easily implemented by using a Composable function called TopAppBar. You need to place TopAppBar along with your main composable function inside a column.
#Composable
fun HomeScreenApp() {
Column() {
TopAppBar(title = { Text(text = "Adopt Me") }, backgroundColor = Color.Red)
showPetsList(dogs = dogData)
}
}
The above function calls the TopAppBar inside a column followed by your main content view. The TopAppBar function takes in a Text object(Not string) as title. This can also be any Composable function. You can also specify other params like backgroundColor, navigationIcon, contentColor etc. Remember that TopAppBar is just a Composable provided by Jetpack team. It can be your custom function also just in case you need more customization.
Output