How to integrate AlertDialog with Navigation component in Jetpack Compose? - android

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.

Related

ModalBottomSheet is showing above BottomBar in Jetpack Compose

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()
}
}

Jetpack Compose Navigation dialog configuration

I have a NavHost that looks something like this:
NavHost(
navController = navController,
startDestination = Screen.MAIN.route,
modifier = modifier
) {
dialog(
Screen.LOGIN.route,
dialogProperties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnBackPress = false,
dismissOnClickOutside = false
)
) { LoginScreen() }
composable(Screen.MAIN.route) { MainScreen() }
}
For composable screen I can configure my MainActivity's windowSoftInputMode to adjustResize as well as other parameters like:
WindowCompat.setDecorFitsSystemWindows(window, false)
But I can't seem to find a way to configure the way my dialog is displayed. It seems to use adjustPan functionality, as I can see, that when keyboard appears it pushes status bar up. How can I configure this dialog, or how can I at least change windowSoftInputMode of mentioned dialog?
EDIT
I tried accessing window from context and explicitly setting required parameters, but it doesn't seem to have any effect
val context = LocalContext.current
SideEffect {
val window = context.findWindow()!!
WindowCompat.setDecorFitsSystemWindows(window, false)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
}

Compose navigation lose state after pop screen (initial composition)

I am using compose navigation with single activity and no fragments.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MobileComposePlaygroundTheme {
Surface(color = MaterialTheme.colors.background) {
val navController = rememberNavController()
NavHost(navController, startDestination = "main") {
composable("main") { MainScreen(navController) }
composable("helloScreen") { HelloScreen() }
}
}
}
}
}
}
#Composable
private fun MainScreen(navController: NavHostController) {
val count = remember {
Log.d("TAG", "inner remember, that is, initialized")
mutableStateOf(0)
}
LaunchedEffect("fixedKey") {
Log.d("TAG", "inner LaunchedEffect, that is, initialized")
}
Column {
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
count.value++
Log.d("TAG", "count: ${count.value}")
},
modifier = Modifier.padding(8.dp)
) {
Text(text = "Increase Count ${count.value}")
}
Button(
onClick = { navController.navigate("helloScreen") },
modifier = Modifier.padding(8.dp)
) {
Text(text = "Go To HelloScreen")
}
}
}
#Composable
fun HelloScreen() {
Log.d("TAG", "HelloScreen")
Text("Hello Screen")
}
MainScreen -> HelloScreen -> back button -> MainScreen
After pop HelloScreen by back button, MainScreen restart composition from scratch. That is, not recomposition but initial composition. So remember and LaunchedEffect is recalculated.
I got rememberSaveable for maintaining states on this popping upper screen case. However how can I prevent re-execute LaunchedEffect? In addition, docs saying rememberSavable makes value to survive on configuration change but this is not the exact case.
I expected that LowerScreen is just hidden when UpperScreen is pushed, and LowerScreen reveal again when UpperScreen is popped, like old Android's onPause(), onResume(), etc.
In Compose, is this not recommended?
ps.
Lifecycle of Composable is not tied with ViewModel but with Activity
It needs more care about initialization of ViewModel
Why Compose team design like this?
Can you recommend good architecture sample code?

Clean TextField when BottomSheetScaffold collapse on Jetpack Compose

I'm having a little trouble adding a form inside a Bottom sheet because every time I open the bottomSheet, the previous values continue there. I'm trying to make something like this
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun BottomSheet() {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
Form {
// save foo somewhere
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
},
sheetPeekHeight = 0.dp
) {
Button(onClick = {
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.expand()
}
}) {
Text(text = "Expand")
}
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun Form(
onSaveFoo: (String) -> Unit
) {
var foo by remember { mutableStateOf("") }
Column {
Button(onClick = {
onSaveFoo(foo)
}) {
Text(text = "Save")
}
OutlinedTextField(value = foo, onValueChange = { foo = it })
}
}
There is a way to "clean" my form every time the bottom sheet collapses without manually setting all values to "" again?
Something like the BottomShettFragment. If I close and reopen the BottomSheetFragment, the previous values will not be there.
Firstly, they say that it is better to control your state outside of a composable function (in a viewmodel) and pass it as a parameter.
You may clear the textField value, when you decide to collapse your bottomSheet, for example in onSaveFoo function.
Add a MutableStateFlow to your viewmodel, subscribe to its updates via collectAsState extension in your composable. You can get a viewmodel by a composable function viewModel(ViewModelClass::class.java).
In onSaveFoo function update your state with new string or empty string if that's the behaviour you want to achieve. State updates should happen inside viewmodel. So create a method in your viewmodel to update your state and call it when you want to collapse your bottomsheet to clear the text contained in your state.
And another thing, remember saves the value across recompositions. The value will be lost only if your Composable is removed from the Composition. It will happen if you change the content of your bottomSheet.
Something like this:
sheetContent = {
if(bottomSheetScaffoldState.bottomSheetState.isExpanded){
Form {
// save foo somewhere
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
}else{
Spacer(modifier=Modifier.height(16.dp).background(Color.White)//or some other composable
}
},

How to implement BottomAppBar and BottomDrawer pattern using Android Jetpack Compose?

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))
}
}
}

Categories

Resources