How to handle Single NavHost in Single Activity approach in Multi Module structure in Android Jetpack compose.
Did not see any example with below scenario to handle single NavHost in Single Activity.
Navigation should start from splash screen.
Custom Splash Screen -> Sign in Screen -> Home Screen with BottomBar
NavHost(navController, startDestination = "Splash")
{
composable(route = "Splash") {
Splash()
}
composable(route = "Sign In") {
SignIn()
}
composable(route = "Home") {
Home()
}
}
BottomBar has 3 options to navigate between each bottom bar items.
Where to add BottomBar in above scenario?
Updated as per comments
Scaffold(
bottomBar = { BottomBar(navController = navController) }) {
// sub graph for Home?
}
Related
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
I am using navigation-compose along with bottom bar in jetpack compose. I want to show different bottom bar for different type of user. For that, I need to set conditional startDestination in NavHost. How do i do that?
I've tried below but it change startDestination but does not reflect in UI.
val user by remember { mutableStateOf(viewModel.user) }.collectAsState()
var startDestination = Screens.UserType1.route
LaunchedEffect(user) {
startDestination = if (loggedInUser?.userType == UserType.Seller) Screens.SellerHome.route else Screens.BuyerHome.route
}
While below code throws java.util.NoSuchElementException: List contains no element matching the predicate.
when (user?.userType) {
UserType.Seller -> {
startDestination = Screens.SellerHome.route
}
UserType.Buyer -> {
startDestination = Screens.BuyerHome.route
}
else -> {}
}
My NavHost
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
...
}
Change this
var startDestination = Screens.UserType1.route
To this
var startDestination by remember { mutableStateOf(Screens.UserType1.route) }
I am using Jetpack Compose and the Android navigation component. When I am on a screen with an AlertDialog, I am unable to navigate back. I guess it's due to the AlertDialog catching the back button event. However I don't know how to connect the AlertDialog to the navigation component? Is there any official way or best practice to do this? Here's my code:
// sample with a screen and a "navigate to dialog" button.
// once the button is pressed, an AlertDialog is shown.
// Using the back button while the AlertDialog is open has no effect ):
#Composable
fun MyNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
NavHost(
navController = navController,
startDestination = "start_route",
modifier = modifier
) {
composable("start_route") {
Text("start screen")
}
// this is my screen with the dialog
dialog("dialog_route") {
AlertDialog(
onDismissRequest = { /*TODO*/ }, // guess I need to connect this to the navigation component? bug how?
title = {
Text(text = "Dialog title")
},
text = {
Text(text = "I am a dialog")
},
buttons = {}
)
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyJetpackComposeTheme {
val navController = rememberNavController()
Scaffold() { innerPadding ->
Column {
Button(onClick = { navController.navigate("dialog_route") }) {
Text("navigate to dialog")
}
MyNavHost(navController, modifier = Modifier.padding(innerPadding))
}
}
}
}
}
}
As per the dialog documentation:
This is suitable only when this dialog represents a separate screen in your app that needs its own lifecycle and saved state, independent of any other destination in your navigation graph. For use cases such as AlertDialog, you should use those APIs directly in the composable destination that wants to show that dialog.
So you shouldn't be using a dialog destination at all: a dialog destination is specifically and only for providing the content lambda of a regular Dialog. What your code is actually doing is creating an empty Dialog (i.e., you don't emit any composables in the lambda you pass to dialog, then creating another AlertDialog stacked on top of that empty dialog. That's not what you want to be doing.
Instead, you should be following the AlertDialog docs and directly creating the AlertDialog where you want it to be used, setting your own boolean for when it should be shown/hidden.
I have BottomBar added in ComposeActivity. BottomBar is responsible for showing 3 composable. I don't want to add ModalBottomSheet to Activity instead I have added ModalBottomSheet inside 3rd Screen Composable. When ModalBottomSheet is invoked it is displayed above the BottomBar. How ModalBottomSheet can be displayed when Call to Action is done from 3rd screen Composable and it should be above BottomBar.
Any help will be appreciated.
It should display over BottomBar and on Bottom.
Just Position your ModalBottomSheetLayout on top of the Scaffold component.
Example:
AppTheme() {
ModalBottomSheetLayout(){ //<-- Here
Scaffold(){
//<-- Not Here or below
NavigationGraph(){
}
}
}
}
and call bottomSheet from NavHost.
Also you can hide Bottombar with action from screen but its long way;
*it may vary depending on your component child structure
Screen:
#Composable
fun 3rdScreen(onClickForHideBottomBar:() -> Unit){
Button(onClick = { onClickForHideBottomBar()}) {
Text(text = "Hide BottomBar")
}
}
NavGraph:
#Composable
fun NavigationGraph(onClickForHideBottomBar:() -> Unit,){
NavHost(){
composable(){
3rdScreen(onClickForHideBottomBar ={onClickForHideBottomBar()})
}
}
}
Scaffold:
var bottomBarVisibility by remember { mutableStateOf(false)}
Scaffold(
bottomBar = {BottomNavigationView(bottomBarVisibility=bottomBarVisibility)}){
NavigationGraph(onClickForHideBottomBar = bottomBarVisibility = !bottomBarVisibility){}
}
BottomNavigationView:
#Composable
fun BottomNavigationView(bottomBarVisibility: Boolean){
AnimatedVisibility(visibleState =MutableTransitionState(bottomBarVisibility)){
BottomNavigation()
}
}
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