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)
}
Related
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'm using the new Material 3 NavigationBar and NavigationBarItem components, I want the NavigationBar to be thinner, because the default one is too large. I want one similar to the one Gmail or Drive has (see picture at the end for the comparison). Making the icon smaller doesn't work, and neither changing all the available paddings (Icon, NavigationBar and NavigationBarItem).
This is the Composable code, if I change the NavigationBar heigh (using Modifier) then this happens:
I primarly want to remove the space between the label and the bottom, and the one between the top and the icon.
#Composable
fun MyAppBottomBar(navController: NavController, tabs: Array<MenuBottom>) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route ?: MenuBottom.INICIO.route
val rutas = remember { MenuBottom.values().map { it.route } }
if (currentRoute in rutas) {
NavigationBar(containerColor = elevation01) {
tabs.forEachIndexed { index, item ->
NavigationBarItem(
selected = currentRoute == item.route,
onClick = {
if (item.route != currentRoute) {
navController.navigate(item.route) {
popUpTo(navController.graph.startDestinationId) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
},
label = { Text(stringResource(id = item.title)) },
icon = {
if (item.route == currentRoute) {
Icon(item.selectedIcon, contentDescription = null, tint = Color.Black)
} else {
Icon(item.unselectedIcon, contentDescription = null)
}
},
colors = NavigationBarItemDefaults.colors(
selectedIconColor = Color.Black,
unselectedIconColor = Color.Black,
indicatorColor = Greenyellow,
selectedTextColor = Color.Black,
unselectedTextColor = Color.Black
)
)
}
}
}
}
NavigationBar does not use padding. It uses a fixed height. How do I know that? While working with MUI (which is a React implementation of Material design) I found that they use a fixed height, and margin: auto to center items vertically. Then I figured jetpack compose NavigationBar might do the same and the idea was right. So whats the solution?
NavigationBar(
modifier = Modifier.height(64.dp)
) {...}
Or change it to your taste. It will shrink and grow the space taken up.
This most likely has to do with the bottom navigation. The grey bar you see has its own padding by default.
Have a look at this documentation on how to remove those System intents.
Here's what you need to do in a nutshell:
implementation "com.google.accompanist:accompanist-insets:<version>"
Then in your MainActivity call WindowCompat.setDecorFitsSystemWindows(window, false) like this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {...}
}
Now your App should use the entire Screen and your NavigationBar should have the same height as the one from Gmail.
To apply single ones of them again e.g. StatusBar call a modifier:
Modifier.statusBarsPadding()
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.
Problem : TextField (inside lazy column) text goes below the keybaord
Explanation :
I have a LazyColumn that contains a list of items displaying text fields , In the manifest the activity has windowSoftInputMode="adjustResize" and I am also setting the flag WindowCompat.setDecorFitsSystemWindows(window,false) in the onCreate Method before setContent and I want to make text appear above the keyboard at all times for smoother editing experience !
Using Accompanist Library providing Window Insets to give padding to the Box like this
Box(modifier = Modifier.weight(1f).navigationBarsWithImePadding()){
LazyColumn() {
items(myItems) { item->
ItemComposable(item)
}
}
}
As you can see , there's navigationBarsWithImePadding on the box , but it does't work since the text goes below the keyboard , I tried setting the modifier on LazyColumn but then it provides padding with the LazyColumn relative to other items outside of the box !
so I tried contentPadding
LazyColumn(contentPadding=insets.ime.toPaddingValues(additionalBottom=insets.navigationBars.bottom.dp)) {
items(editor.blocks) { block ->
RenderBlock(block)
}
}
Again did't work , since the content padding is applied to the last item / or after it , The keyboard goes above the text
Replacing LazyColumn with a simple Column and using a verticalScroll modifier causes the same problem , Because the list can be long vertical scroll becomes a need
Finally I have a solution for it. Just follow these steps below:
Step 1: In your AndroidManifest.xml
<activity
...
android:windowSoftInputMode="adjustResize">
</activity>
Step 2: In your Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
AppTheme {
ProvideWindowInsets(
windowInsetsAnimationsEnabled = true,
consumeWindowInsets = false,
) {
// your content
}
}
}
Step 3:
LazyColumn(
modifier = Modifier
.navigationBarsWithImePadding()
.verticalScroll(state = rememberScrollState())
.height(LocalConfiguration.current.screenHeightDp.dp)
.fillMaxWidth(),
) {
// your TextField items
}
Step 4:
// init your CoroutineScope
val coroutineScope = rememberCoroutineScope()
// init your BringIntoViewRequester
val bringIntoViewRequester = BringIntoViewRequester()
// use them in your TextField modifier
modifier = Modifier
/* ... your other modifiers*/
.bringIntoViewRequester(bringIntoViewRequester)
.onFocusEvent {
if (it.isFocused || it.hasFocus) {
coroutineScope.launch {
delay(250)
bringIntoViewRequester.bringIntoView()
}
}
}
}
Hope it helps.
If you use Column, you can refer to the following code snippet:
Column(
modifier = Modifier
// other modifiers
.verticalScroll(scrollState, reverseScrolling = true)
.navigationBarsWithImePadding(),
verticalArrangement = Arrangement.Bottom,
)
In Jetpack compose 1.0.0-beta01, I am calling the BottomSheetScaffold like this:
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = { Text("") },
sheetShape = Shapes.large,
backgroundColor = AppTheme.colors.uiBackground,
modifier = modifier
) { (content) }
... and getting the following error:
java.lang.IllegalArgumentException: The initial value must have an associated anchor.
Any tips on fixing this?
Don't forget to add the following atribute:
sheetPeekHeight = 0.dp
So your code should be like this:
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = { Text("") },
sheetShape = Shapes.large,
sheetPeekHeight = 0.dp, // <--- new line
backgroundColor = AppTheme.colors.uiBackground,
modifier = modifier
) { (content) }
When bottomSheetState is expand, sheetContent must have real content to show.
You need check this.
I got the same issue when using ModalBottomSheetLayout, and my compose material version is 1.2.0-rc02
androidx.compose.material:material:1.2.0-rc02
I want to show the bottom modal when one item is selected, and if no item is selected, the modal should be empty.
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
EditProgressContent(book = book)
}
) { ... }
#Composable
fun EditProgressContent(book: Book?) {
if (book == null) return
Text(book.title)
}
When book is null, I got the same crash. There is no peekHeight parameter for ModalBottomSheetLayout, so I have to add a pixel when book is null
#Composable
fun EditProgressContent(book: Book?) {
if (book == null) {
Box(modifier = Modifier.size(1.dp)
return
}
Text(book.title)
}
The code looks silly, hope it can be fixed from Google.
In case you end up on this page because you use the BackdropScaffold, the suggested solution does the trick as well. Simply set the peekHeight.
For example like this:
BackdropScaffold(
appBar = {},
backLayerContent = {},
frontLayerContent = {},
peekHeight = 0.dp
)
Then Preview works like a charm again.
Fun fact though: Don't set it to 56.dp which is the default init value it normally should be initialised with (value of BackdropScaffoldDefaults.PeekHeight). 56.dp results in the anchor rendering problem in my current setup.
(Using compose version '1.1.1')
On new version, this error happens when the initial State is Expanded, where try to open the Modal before launched. Try launcher via ModalBottomSheetState from LaunchedEffect (maybe can need a delay)