I have nested column, when I click add button the goal is add another text field and when I click delete button (which still hidden because first index) the goal is remove its text field. It seems doesn't recompose but the list size is changed.
I have tried using LazyColumn and foreach inside leads to force close, still no luck.
Any help appreciated, thank you!
My current code :
#Composable
fun ProblemScreen() {
val list = remember {
mutableStateListOf<MutableList<String>>()
}
LaunchedEffect(key1 = Unit, block = {
repeat(3) {
val listDesc = mutableListOf<String>()
repeat(1) {
listDesc.add("")
}
list.add(listDesc)
}
})
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
) {
list.forEachIndexed { indexParent, parent ->
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "Parent ${indexParent + 1}", fontSize = 18.sp)
Spacer(modifier = Modifier.weight(1f))
Button(onClick = {
parent.add("")
println("PARENT SIZE : ${parent.size}")
}) {
Icon(imageVector = Icons.Rounded.Add, contentDescription = "Add")
}
}
parent.forEachIndexed { indexChild, child ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
TextField(
value = "",
onValueChange = {
},
colors = TextFieldDefaults.textFieldColors(),
maxLines = 1,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(16.dp))
Button(
onClick = {
parent.removeAt(indexChild)
},
modifier = Modifier.alpha(if (indexChild != 0) 1f else 0f)
) {
Icon(
imageVector = Icons.Rounded.Delete,
contentDescription = "Delete"
)
}
}
}
}
}
}
}
As said in docs, mutable objects that are not observable, such as mutableListOf(), are not observable by Compose and don't trigger a recomposition.
So instead of
val list = remember {
mutableStateListOf<MutableList<String>>()
}
Use:
val list = remember {
mutableStateListOf<List<String>>()
}
And when you need to update the List, create a new one:
//parent.add("")
list[indexParent] = parent + ""
I have a Column with some rows, and I want to align the last row at the botton, but this row is never located at the bottom of the screen, it stays right after the previous row:
Column {
// RED BOX
Row(
modifier = Modifier
.fillMaxWidth()
.height(130.dp)
.padding(vertical = 15.dp, horizontal = 30.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = stringResource(id = R.string.app_name),
style = TextStyle(fontSize = 40.sp),
color = Color.White
)
Text(
text = stringResource(id = R.string.app_description),
style = TextStyle(fontSize = 13.sp),
fontWeight = FontWeight.Bold,
color = Color.Black
)
}
}
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(15.dp)
)
// GREEN BOX
val currentRoute = currentRoute(navController)
items.forEach { item ->
DrawerItem(item = item, selected = currentRoute == item.route) {
navController.navigate(item.route) {
launchSingleTop = true
}
scope.launch {
scaffoldState.drawerState.close()
}
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 15.dp, horizontal = 30.dp),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.Center
) {
Text(
text = BuildConfig.VERSION_NAME,
style = TextStyle(fontSize = 11.sp),
color = Color.Black,
)
}
}
I want to get the same as I show in the picture. I want to have the first row (red), then the second row (green) and then a third row that fits at the bottom of the screen (blue)
You can do it in many ways.
You can use a Column with verticalArrangement = Arrangement.SpaceBetween assigning a weight(1f,false) to the last row:
Column(
Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween) {
//All elements
Column {
// RED BOX
//...
Spacer(
modifier = Modifier
.fillMaxWidth()
.background(Green)
.height(15.dp)
)
//... Green box
}
//LAST ROW
Row(
modifier = Modifier
.weight(1f, false)
) {
//...
}
}
You can use a Spacer(modifier.weight(1f)) between GreenBox and Blue Box to create space between them or you can create your custom column with Layout function and set y position of last Placeable as height of Composable - height of last Composble
Column(modifier = Modifier
.fillMaxHeight()
.background(Color.LightGray)) {
Text(
"First Text",
modifier = Modifier
.background(Color(0xffF44336)),
color = Color.White
)
Text(
"Second Text",
modifier = Modifier
.background(Color(0xff9C27B0)),
color = Color.White
)
Spacer(modifier = Modifier.weight(1f))
Text(
"Third Text",
modifier = Modifier
.background(Color(0xff2196F3)),
color = Color.White
)
}
Result:
Custom Layout
#Composable
private fun CustomColumn(
modifier: Modifier,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val looseConstraints = constraints.copy(
minWidth = 0,
maxWidth = constraints.maxWidth,
minHeight = 0,
maxHeight = constraints.maxHeight
)
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each child
measurable.measure(looseConstraints)
}
// Track the y co-ord we have placed children up to
var yPosition = 0
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Place children in the parent layout
placeables.forEachIndexed { index, placeable ->
println("Placeable width: ${placeable.width}, measuredWidth: ${placeable.measuredWidth}")
// Position item on the screen
if (index == placeables.size - 1) {
placeable.placeRelative(x = 0, y = constraints.maxHeight - placeable.height)
} else {
placeable.placeRelative(x = 0, y = yPosition)
}
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
Usage
CustomColumn(
modifier = Modifier
.fillMaxHeight()
.background(Color.LightGray)
) {
Text(
"First Text",
modifier = Modifier
.background(Color(0xffF44336)),
color = Color.White
)
Text(
"Second Text",
modifier = Modifier
.background(Color(0xff9C27B0)),
color = Color.White
)
Spacer(modifier = Modifier.weight(1f))
Text(
"Third Text",
modifier = Modifier
.background(Color(0xff2196F3)),
color = Color.White
)
}
Result:
In this example with Layout you should consider how you measure your measureables with Constraints and your total width and height. It requires a little bit practice but you get more unique designs and with less work(more optimised) composables than ready ones. Here i set layout as maxWidth so no matter which width you assign it takes whole width. It's for demonstration you can set max width or height in layout based on your needs.
I have simple LazyColumn where each row contains a BasicTextField (the rows contain label names which can be changed). The row's state is set to "editMode" when it is clicked. Only then I want the BasicTextField to be editable, which makes the edit icons visible. However, I have to click two times on the row, but I want it to set focus on the TextField when the row is clicked for the first time. Using the focusRequest just does not work (nothing happens). Where could the problem be? Here is the code:
val focusRequester = FocusRequester()
val inputService = LocalTextInputService.current
Row(
modifier = modifier
.fillMaxWidth()
.height(48.dp)
.clickable(enabled = true) {
onLabelClick() // here I set the editMode = true
inputService?.showSoftwareKeyboard()
focusRequester.requestFocus()
}
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Box(
modifier = Modifier.weight(1f)
) {
BasicTextField(
modifier = Modifier
.fillMaxSize()
.focusRequester(focusRequester)
.focusable(enabled = true),
value = tmpLabelName,
onValueChange = {
tmpLabelName = it
},
enabled = editMode,
singleLine = true,
textStyle = MaterialTheme.typography.body1,
decorationBox = { innerTextField ->
Box(
contentAlignment = Alignment.CenterStart
) {
if (tmpLabelName.isEmpty()) {
// show hint if text is empty
Text(
text = stringResource(id = R.string.enter_label_name)
)
} else {
innerTextField()
}
}
}
)
}
AnimatedVisibility(
visible = editMode,
enter = slideInHorizontally(
initialOffsetX = { fullWidth ->
fullWidth / 2
}
),
exit = fadeOut()
) {
Row {
IconButton(onClick = {
onLabelSaveClick(
label.name, tmpLabelName
)
}) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = stringResource(id = R.string.save_label_changes)
)
}
IconButton(onClick = {
onLabelDeleteClick(label.name)
}) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = stringResource(id = R.string.delete_label)
)
}
}
}
}
So I've been struggling with the following problem for a while. I achieved this (John's example):
But what I'm trying to do is to force the hour to always be shown directly after the text, and if the text is too long - overflow the text. So Jane Doe's example is perfect, same as Jack Doe's (but in Jack's case that's all a dummy text).
And I can't really figure out what I'm doing wrong.
That's the piece of code I wrote:
Row(modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
// there's another Row printing the name
}
Spacer(Modifier.height(5.dp))
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
Row(modifier = Modifier.wrapContentWidth(), horizontalArrangement = Arrangement.Start) {
Text(
text = item.message,
style = MaterialTheme.typography.subtitle1,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
Circle() // that's my function which just shows the circle
Text(
text = dateString,
style = MaterialTheme.typography.subtitle1,
maxLines = 1,
modifier = Modifier.padding(horizontal = 4.dp)
)
}
}
}
}
I'll be really grateful for any kind of help.
Something like this?
#Preview(showBackground = true)
#Composable
fun Test() {
Column {
Message(message = "short message")
Message(message = "short")
Message(message = "very long message")
}
}
#Composable
fun Message(message: String) {
Text("John Doe")
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Text(
message,
modifier = Modifier.weight(1F, fill = false),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
Text("8:35PM")
}
}
You can achieve this with a Layout and using IntrinsicWidth. You can build on this example as it only uses 2 Texts for the children, but this should get you on the right path.
#Composable
fun MyRow(
modifier: Modifier = Modifier,
content: #Composable () -> Unit,
) {
Layout(
content = content,
modifier = modifier,
) { measurables, constraints ->
check(measurables.size == 2) { "This composable requires 2 children" }
val first = measurables.first()
val second = measurables[1]
val looseConstraints = constraints.copy(
minWidth = 0,
minHeight = 0,
)
val secondMeasurable = second.measure(looseConstraints)
val maxHeight = secondMeasurable.height
val availableWidth = constraints.maxWidth - secondMeasurable.width
val maxWidth = first.maxIntrinsicWidth(maxHeight).coerceAtMost(availableWidth)
val firstMeasurable = first.measure(
Constraints(
minWidth = maxWidth,
maxWidth = maxWidth,
minHeight = 0,
maxHeight = maxHeight
)
)
layout(
constraints.maxWidth,
maxHeight,
) {
firstMeasurable.place(0, 0)
secondMeasurable.place(maxWidth, 0)
}
}
}
#Composable
#Preview
fun MyRowPreview() {
SampleTheme {
Surface(modifier = Modifier.width(320.dp)) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = "John Doe",
style = MaterialTheme.typography.h5,
)
MyRow(modifier = Modifier.fillMaxWidth()) {
Text(
text = "Short Label",
style = MaterialTheme.typography.body1,
modifier = Modifier.padding(end = 8.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
Text(
text = "8:35 PM",
style = MaterialTheme.typography.body1,
modifier = Modifier.padding(start = 8.dp),
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "John Doe",
style = MaterialTheme.typography.h5,
)
MyRow(modifier = Modifier.fillMaxWidth()) {
Text(
text = "A long label that will require truncation goes here",
style = MaterialTheme.typography.body1,
modifier = Modifier.padding(end = 8.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
Text(
text = "8:35 PM",
style = MaterialTheme.typography.body1,
modifier = Modifier.padding(start = 8.dp),
)
}
}
}
}
}
TopAppBar(
backgroundColor = Color.Transparent,
elevation = 0.dp,
modifier= Modifier.fillMaxWidth(),
navigationIcon = {
IconButton(
onClick = { TODO },
enabled = true,
) {
Icon(
painter = painterResource(id = R.drawable.icon_back_arrow),
contentDescription = "Back",
)
}
}
},
title = {
Text(
modifier = if (action == null) Modifier.fillMaxWidth() else Modifier,
textAlign = if (action == null) TextAlign.Center else TextAlign.Start,
maxLines = 1,
text = "Hello"
)
},
actions = {
action?.run {
Text(
modifier = Modifier
.padding(horizontal = 16.dp)
.clickable(onClick = TODO),
color = Color.Green,
text ="Cancel",
)
}
}
I'm new in jetpack and want to align title of TopAppBar at center if action is null. Title is not align center of layout. when there is no navigationIcon it work but adding navigationIcon it show slightly right. How can I do it to make title text at center of layout.
With Material2 you have to use the other constructor of TopAppBar that has no pre-defined slots for content, allowing you to customize the layout of content inside.
You can do something like:
val appBarHorizontalPadding = 4.dp
val titleIconModifier = Modifier.fillMaxHeight()
.width(72.dp - appBarHorizontalPadding)
TopAppBar(
backgroundColor = Color.Transparent,
elevation = 0.dp,
modifier= Modifier.fillMaxWidth()) {
//TopAppBar Content
Box(Modifier.height(32.dp)) {
//Navigation Icon
Row(titleIconModifier, verticalAlignment = Alignment.CenterVertically) {
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.high,
) {
IconButton(
onClick = { },
enabled = true,
) {
Icon(
painter = painterResource(id = R.drawable.ic_add_24px),
contentDescription = "Back",
)
}
}
}
//Title
Row(Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically) {
ProvideTextStyle(value = MaterialTheme.typography.h6) {
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.high,
){
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
maxLines = 1,
text = "Hello"
)
}
}
}
//actions
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Row(
Modifier.fillMaxHeight(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
content = actions
)
}
}
}
With Material3 you can simply use the CenterAlignedTopAppBar:
CenterAlignedTopAppBar(
title = { Text("Centered TopAppBar") },
navigationIcon = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = "Localized description"
)
}
}
)
If you're using Material3, you can use the CenterAlignedTopAppBar as well.
fun CenterAlignedTopAppBar(
title: #Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: #Composable () -> Unit = {},
actions: #Composable RowScope.() -> Unit = {},
colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(),
scrollBehavior: TopAppBarScrollBehavior? = null
) {
SingleRowTopAppBar(
modifier = modifier,
title = title,
titleTextStyle =
MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont),
centeredTitle = true,
navigationIcon = navigationIcon,
actions = actions,
colors = colors,
scrollBehavior = scrollBehavior
)
}
EDIT: the old answer is out of date, please use CenterAlignedTopAppBar
CenterAlignedTopAppBar(
title = { Text(text = stringResource(id = titleRes)) },
actions = {
IconButton(onClick = onActionClick) {
Icon(
imageVector = actionIcon,
contentDescription = actionIconContentDescription,
tint = MaterialTheme.colorScheme.onSurface
)
}
},
colors = colors,
modifier = modifier,
)
OLD ANSWER:
I redid the native implementation a bit.
Need to do just two things:
1.Add this file to your project. This is a slightly modified implementation of the TopAppBar class.
https://gist.github.com/evansgelist/aadcd633e9b160f9f634c16e99ffe163
2.Replace in your code TopAppBar to CenterTopAppBar. And it's all!
Scaffold(
topBar = {
CenterTopAppBar(
title = {
Text(
textAlign = TextAlign.Center,
text = text,
)
},
EDIT
extension's code
val Number.toPx
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this.toFloat(),
Resources.getSystem().displayMetrics
)
Result
The core of the title centering is that the space of the left and right occupying slots is the same. You only need to adjust the default size of the slots. We give the left and right slots the default space occupying slots, which can solve this problem well, and the code is simple.
#Composable
fun TopBar(title: Int, actions: #Composable (() -> Unit)? = null, popOnClick: () -> Unit) {
val modifier = Modifier.size(width = 70.dp, height = 50.dp).background(Color.Red)
TopAppBar(
title = {
Text(text = stringResource(id = title), fontSize = 16.sp,
textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()) },
navigationIcon = {
Box(modifier = modifier, contentAlignment = Alignment.Center) {
IconButton(onClick = { popOnClick() }) {
Icon(Icons.Filled.ArrowBack, contentDescription = "", tint = MaterialTheme.colors.primary)
}
}
},
actions = {
Box(modifier = modifier, contentAlignment = Alignment.Center) {
if (actions != null) {
actions()
}
}
},
backgroundColor = MaterialTheme.colors.surface,
elevation = 1.dp
)
}
It depends what's in the appbar.
If you only have the title, then you could do the following:
topBar = {
TopAppBar(content = {
Text(
modifier = Modifier.fillMaxWidth(),
text = "Title Text",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h6,
)
})
},
If you have an icon either side you should be able to do the same, may have to adjust things if have two icons one side and one the other, then may want to use add a same sized icon to even things out and put enabled to false to it's not clickable and color to transparent so it's not seen, otherwise you could try and figure out the size and add padding end to the text, it seems you also need to add 16.dp padding to the transparent icon, as the navigation icon is given extra padding before the title but not between title and actions
Here's what I did for the arrowIcon and title
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
navController.popBackStack()
}
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "back arrow icon"
)
}
},
title = {
Text(
modifier = Modifier
.fillMaxWidth(),
text = "Basic Navigation",
textAlign = TextAlign.Center,
)
},
actions = {
IconButton(
modifier = Modifier
.padding(start = 16.dp),
enabled = false,
onClick = {}
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "back arrow icon",
tint = Color.Transparent
)
}
}
)
}
I'm just a begginer with Jetpack Compose and before I searched for a solution of that problem I tried to figure my own, maybe it will be enough for someone. I needed centered title for TopAppBar with navigation icon on the left side only or with two icons on the left and right side, this solution was OK for me. Later I configurated that for including passed icon on the right side or no.
TopAppBar(
backgroundColor = Color.Green,
elevation = 5.dp,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// size of an icon and placeholder box on the right side
val iconSize = 32.dp
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Arrow Back",
modifier = Modifier
.clickable {
navController.popBackStack()
}
.size(iconSize)
)
Text(text = "Centered Title", fontSize = 32.sp)
// placeholder on the right side in order to have centered title - might be replaced with icon
Box(
modifier = Modifier
.size(iconSize)
) { }
}
I can't attach screenshot, preview is here: https://i.stack.imgur.com/UNQTF.png
I found a good workaround if you're using Material 2.
Create a Row, that contains:
Column with weight 1, containing an Icon
Column with weight 1, containing Header
Spacer with weight 1
The device's screen will automatically divide into 3 equal sections.
Here is a code sample:
Scaffold(
topBar = {
TopAppBar(
backgroundColor = Transparent,
elevation = 0.dp,
modifier = Modifier.fillMaxWidth()
) {
/** TopAppBar Content */
Row(
modifier = Modifier.height(51.dp).fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
/** Icon*/
Column(modifier = Modifier.weight(1f).padding(start = 9.dp)) {
IconButton(
onClick = { /*TODO*/ },
modifier = Modifier
.width(22.dp)
.height(22.dp),
enabled = true,
) {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_arrow_back),
contentDescription = "Back",
tint = ThemeBlue
)
}
}
/** Header */
Column(modifier = Modifier.weight(1f)) {
Text(
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.h1,
textAlign = TextAlign.Center,
maxLines = 1,
text = "Header",
)
}
/*TODO
* When the stable version of Material 3 comes out
* we should replace the whole TopAppBar with CenterAlignedTopAppBar.
* For now, this Spacer keeps the Header component in the center of the topBar
*/
Spacer(modifier = Modifier.weight(1f))
}
}
},
content = {
Divider(color = ThemeBlue, thickness = 0.5.dp)
}
)
Here is a screenshot of the result this code produces:
Use fillMaxWidth() to let the Text View occupy AppBar width
Use wrapContentWidth(align = Alignment.CenterHorizontally) to align the Text Title.
TopAppBar(
title = {
Text(
text = screenname,
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(align= Alignment.CenterHorizontally)
)
},
modifier = modifier
)
title = {
Row(
modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
Text(
stringResource(R.string.app_title).uppercase(),
style = MaterialTheme.typography.h1
)
Spacer(modifier.width(1.dp))
}
},
#Composable
fun TopAppBarCompose(){
TopAppBar(
title = {
Box(modifier = Modifier.fillMaxWidth()) {
Text(
text = "Hello",
fontSize = 30.sp,
modifier = Modifier.align(Alignment.Center)
)
}
},
)
}
The previous solutions are too complex. It is actually quite simple:
title = {
Text(
text = "title",
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}