A dialog can have a rather complex ui, acting more like a floating screen rather than a typical AlertDialog. Therefore it can be desired to let the dialog have its own ViewModel and being able to navigate to it. When using the jetpack compose navigation artifact the code indicates that only one composable is shown at any time inside the NavHost.
Is there a way to navigate to a dialog that is overlaid onto the current ui? This would be in line with how we can navigate to fragment dialogs. Thanks.
Aha. This is now a feature in compose navigation version 2.4.0-alpha04
From the release notes
The NavHost of the navigation-compose artifact now supports dialog
destinations in addition to composable destinations. These dialog
destinations will each be shown within a Composable Dialog, floating
above the current composable destination.
val navController = rememberNavController()
Scaffold { innerPadding ->
NavHost(navController, "home", Modifier.padding(innerPadding)) {
composable("home") {
// This content fills the area provided to the NavHost
HomeScreen()
}
dialog("detail_dialog") {
// This content will be automatically added to a Dialog() composable
// and appear above the HomeScreen or other composable destinations
DetailDialogContent()
}
}
}
Example:
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Menu.route
) {
// some other screens here: composable(...) { ... }
dialog(
route = "exit_dialog",
dialogProperties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true,
)
) {
Box(modifier = Modifier.width(280.dp)) {
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(10.dp))
.background(DialogBorder)
.padding(bottom = 3.dp)
.clip(RoundedCornerShape(10.dp))
.background(DialogBackground),
) {
Column {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.text_dialog_exit_title),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = Color.Black
)
Text(
text = stringResource(id = R.string.text_dialog_exit_description),
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center,
color = Color.Black
)
}
Divider(color = DialogBorder)
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
Box(
modifier = Modifier
.weight(1f)
.clickable {
// dismiss dialog
navController.popBackStack()
}
.padding(horizontal = 16.dp, vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(id = R.string.button_cancel),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.Black
)
}
Box(
modifier = Modifier
.fillMaxHeight()
.width(1.dp)
.background(DialogBorder),
)
Box(
modifier = Modifier
.weight(1f)
.clickable {
// go back to home other screen
navController.popBackStack(
route = "home_screen",
inclusive = false
)
}
.padding(horizontal = 16.dp, vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(id = R.string.button_ok),
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
color = Color.Black
)
}
}
}
}
}
}
}
It's a feature request: https://issuetracker.google.com/issues/179608120
You can star it so perhaps we'll increase it's priority
Related
I'm just learning how to use LayzyColumn. The LazyColumn does not show me the list of items. When I click the button, the items that are loaded in the list are not displayed.
Here my code:
#Composable
fun MainContent() {
val list = remember { mutableListOf<String>() }
LazyColumn(
contentPadding = PaddingValues(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
itemsIndexed(list) { index, item ->
Card(
backgroundColor = Color(0xFFF7C2D4),
elevation = 4.dp
) {
Row(
Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = item.takeLast(5), Modifier.weight(1F))
}
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
Alignment.BottomEnd
) {
FloatingActionButton(onClick = { list.add(UUID.randomUUID().toString()) }) {
Icon(imageVector = Icons.Default.Add, contentDescription = "")
}
}
}
Compose cannot track changes of plain kotlin types. You need to use Compose mutable states.
In this case mutableStateListOf should be used instead of mutableListOf.
I suggest you start with this youtube video which explains the basic principles of why do you need to use state in Compose. You can continue deepening your knowledge with state in Compose documentation.
I'm struggling with implementing the UI component.
I want to achive something like this:
A box with only bottom shadow.
For now I'm able to add elevation but it add's shadow in every direction.
This is my current code and it's preview:
#Composable
fun PushNotificationsDisabledInfo(onTap: () -> Unit) {
Surface(
elevation = dimensionResource(R.dimen.card_elevation_big),
shape = RoundedCornerShape(dimensionResource(R.dimen.corner_radius_large)),
modifier = Modifier
.background(
color = colorResource(
id = R.color.white
)
)
.padding(dimensionResource(R.dimen.grid_2_5x))
) {
Box(
Modifier
.clip(shape = RoundedCornerShape(dimensionResource(R.dimen.corner_radius_large)))
.background(
color = colorResource(R.color.white)
)
.clickable(
onClick = { onTap() },
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = true),
)
) {
Row(Modifier.padding(dimensionResource(R.dimen.grid_2x))) {
Image(
painter = painterResource(R.drawable.ic_error_big),
contentDescription = stringResource(R.string.empty_content_description),
modifier = Modifier.size(dimensionResource(R.dimen.grid_4x))
)
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.grid_2x)))
Column {
Text(
text = stringResource(R.string.notifications_settings_push_notifications_disabled_title),
style = SiemensTextStyle.caption1,
color = colorResource(R.color.red)
)
Text(
text = stringResource(R.string.notifications_settings_push_notifications_disabled_message),
style = SiemensTextStyle.caption2,
color = colorResource(R.color.black)
)
}
}
}
}
}
Any ideas how to implement only bottom shadow using Compose?
Copy-paste the following extension & use it in your Card's Modifier:
private fun Modifier.bottomElevation(): Modifier = this.then(Modifier.drawWithContent {
val paddingPx = 8.dp.toPx()
clipRect(
left = 0f,
top = 0f,
right = size.width,
bottom = size.height + paddingPx
) {
this#drawWithContent.drawContent()
}
})
I am trying to have a layout where the content should be scrollable. But it doesn't work. Am I doing anything wrong here?
#Composable
fun Screen() {
Box(
modifier = Modifier
.padding(16.dp)
.fillMaxSize()
) {
val offset = remember { mutableStateOf(0f) }
Column( // This should be scrollable
modifier = Modifier
.fillMaxSize()
.scrollable(
orientation = Orientation.Vertical,
state = rememberScrollableState { delta ->
offset.value = offset.value + delta
delta
}
)
) {
Text(...)
Text(...)
Spacer(modifier = Modifier.padding(8.dp))
LazyColumn(...)
Spacer(modifier = Modifier.padding(8.dp))
DropdwonMenu(...)
Text(...)
Spacer(modifier = Modifier.padding(8.dp))
Text(...)
Spacer(modifier = Modifier.padding(8.dp))
Column(...)
Spacer(modifier = Modifier.padding(8.dp))
Button(...)
}
Column(
Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
) {
Error(snackbarHostState)
}
}
LaunchedEffect(state.error) {
if (state.error.isNotEmpty()) {
snackbarHostState.showSnackbar(state.error)
}
}
Replace scrollable with .verticalScroll(rememberScrollState()). Your code is updating offset but this value is not actually being used anywhere to offset content. scrollable is useful if you need fine control over scrolling. But it appears in your case that you don't need it and verticalScroll will do the job.
I am developing a Question/Answer Quiz App in JetPack Compose. I have the Card as below
Card(modifier = Modifier
.wrapContentHeight(Alignment.CenterVertically)
.wrapContentWidth(Alignment.CenterHorizontally)
.padding(10.dp)
.width(300.dp)
.height(600.dp)
.clip(RoundedCornerShape(8.dp)),
elevation = 10.dp,
backgroundColor = Color.White
)
{
Column(
modifier = Modifier
.wrapContentHeight(Alignment.CenterVertically)
.wrapContentWidth(Alignment.CenterHorizontally)
.padding(8.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top
){
Text(
text = "Question : " + query.question_id,
style = Typography.h1
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = query.question,
style = Typography.subtitle1
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = "Options",
style = Typography.h1
)
Spacer(modifier = Modifier.height(2.dp))
OptionsDetailsList(lstOptions = lstOptions)
Spacer(modifier = Modifier.height(10.dp))
Button(onClick = {
}
) {
Text(text = "View Answer")
}
Spacer(modifier = Modifier.height(10.dp))
}
}
}
The Answer List is a LazyColumn as below
#Composable
fun OptionsDetailsList(lstOptions: List<CertAnswers>){
Log.d("ListOptions Count" , lstOptions.size.toString())
LazyColumn(){
item {
Spacer(modifier = Modifier.requiredHeight(1.dp))
}
items(lstOptions){
item ->
Text(text = item.answer,
modifier=Modifier.padding(3.dp),
style = Typography.subtitle1,
)
Spacer(modifier = Modifier.requiredHeight(1.dp))
}
}
}
My Answers DataModel is as Below
data class CertAnswers(
#PrimaryKey (autoGenerate = true)
val id : Int,
var question_id : Int,
val ans_title: String,
val answer : String,
var isSolution: Boolean
)
With isSolution I can get whether an option is right answer.
But I want to show only on the button click and update LazyColumn to show the relevant option in different color.
I tried to call the OptionsDetailsList method again on button click but its not working.
How can i update the LazyColumn on Button Click within CardView.
You need to store a state value in your CardView indicating wether the button was tapped. rememberSaveable will make sure it's saved during recompositions and scrolling. I pass query.question_id as a key: in case it'll change most probably the value should be reinitialized. Check out more about state in compose in documentation
var answerRevealed by rememberSaveable(query.question_id) { mutableStateOf(false) }
You can change the background of the lazy column elements depending on this state. p.s. I suggest you use Modifier as the last argument, so you don't need a comma at the end and you can add/remove/reorder modifiers easily:
#Composable
fun OptionsDetailsList(answerRevealed: Boolean, lstOptions: List<CertAnswers>) {
Log.d("ListOptions Count", lstOptions.size.toString())
LazyColumn() {
item {
Spacer(modifier = Modifier.requiredHeight(1.dp))
}
items(lstOptions) { item ->
Text(
text = item.answer,
style = Typography().subtitle1,
modifier = Modifier
.padding(3.dp)
.background(
if (answerRevealed) {
if (item.isSolution) {
Color.Green
} else {
Color.Red
}
} else {
Color.Transparent
}
)
)
Spacer(modifier = Modifier.requiredHeight(1.dp))
}
}
}
In your button just set this state to true. You can also hide the button once it's tapped, or change the text.
// hide after onClick
if (!answerRevealed) {
Button(onClick = {
answerRevealed = true
}
) {
Text(text = "View Answer")
}
}
// or change text
Button(onClick = {
answerRevealed = true
}
) {
Text(text = if (answerRevealed) "Hide Answer" else "View Answer")
}
My question is related to this one: How to achieve this layout in Jetpack Compose
I have this code:
#Composable
fun TestUi() {
Row {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(color = Color.Yellow)
.fillMaxHeight()
) {
CircularProgressIndicator()
}
Image(imageVector = vectorResource(id = R.drawable.ic_launcher_background))
}
}
I expected to get this:
But I got this instead:
How can I get the Box to fill all the available height without affecting the height of the Row?
I know I could use a ConstraintLayout to solve this, but it seems too much for such a simple use case.
This worked for me Modifier.height(IntrinsicSize.Min)
#Composable
fun content() {
return Row(
modifier = Modifier
.height(IntrinsicSize.Min)
) {
Box(
modifier = Modifier
.width(8.dp)
.fillMaxHeight()
.background(Color.Red)
)
Column {
Text("Hello")
Text("World")
}
}
}
source: https://www.rockandnull.com/jetpack-compose-fillmaxheight-fillmaxwidth/
Have a look at the Layout Composable or Modifier. You can measure the defining element first and then provide modified constraints to the dependent element. If you want to use this as a modifier you should add a size check for the list.
Layout(content = {
Box(modifier = Modifier
.size(width = 30.dp, height = 50.dp)
.background(Color.Green))
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(color = Color.Yellow)
.fillMaxHeight()
) {
CircularProgressIndicator()
}
}) { measurables: List<Measurable>, constraints: Constraints ->
val heightDef = measurables[0].measure(constraints)
val other = measurables[1].measure(
constraints.copy(
maxHeight = heightDef.height,
maxWidth = constraints.maxWidth - heightDef.width)
)
layout(
width = heightDef.width + other.width,
height = heightDef.height
) {
other.placeRelative(0,0)
heightDef.placeRelative(other.width,0)
}
}
For height, instead of fillMaxHeight, just put
.height(vectorResource(id = R.drawable.ic_launcher_background).defaultHeight)
Just for reference, here's the equivalent design implemented with ConstraintLayout:
#Composable
fun TestUi() {
ConstraintLayout {
val (box, image) = createRefs()
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(color = Color.Yellow)
.constrainAs(box) {
height = Dimension.fillToConstraints
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
}
) {
CircularProgressIndicator()
}
Image(
imageVector = vectorResource(id = R.drawable.ic_launcher_background),
modifier = Modifier.constrainAs(image) {
top.linkTo(parent.top)
start.linkTo(box.end)
}
)
}
}
you can use constraintlayout, the trick is in this part of the code height =Dimension.fillToConstraints
ConstraintLayout(){
val (boxRef, imgRef) = createRefs()
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(color = Color.Yellow)
.constrainAs(boxRef){
start.linkTo(parent.start)
end.linkTo(imgRef.start)
top.linkTo(parent.top)
height = Dimension.fillToConstraints
}
) {
CircularProgressIndicator()
}
Image(imageVector = vectorResource(id = R.drawable.ic_launcher_background),modifier=Modifier.constrainAs(imgRef){
start.linkTo(boxRef.end)
top.linkTo(parent.top)
end.linkTo(parent.end)
})
}