jetpack compose removing elements from list of text fields - android

.
I want my code to remove elements from list of text fields properly.
Each element has an X button to remove it's text field.
If I start removing elements from the bottom it works but it doesn't work for removing random elements
I want to use forEachIndexed for displaing the list
Please help me with solving this problem. I've been trying to do this for some time but every trial is unsuccessful.
This is a piece of code that I've managed to write but removing elements doesn't work properly
val listOfWords = mutableStateListOf<String>()
#Composable
fun Main() {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Words",
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 4.dp),
style = MaterialTheme.typography.h6
)
listOfWords.forEachIndexed { index, word ->
Input(word, 30, "Word", 1,
{newWord ->
listOfWords[index] = newWord
Log.d("text ",word)
},
{
listOfWords.removeAt(index)
}
)
}
IconButton(
onClick = {
listOfWords.add("")
}
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Add"
)
}
}
}
#Composable
fun Input(
word: String,
maxChar: Int,
label: String,
maxLines: Int,
onEdit: (word: String) -> (Unit),
onRemove: () -> (Unit)
) {
var text by remember { mutableStateOf(word) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp, 0.dp, 8.dp, 0.dp)
) {
OutlinedTextField(
value = text,
onValueChange = {
if (it.length <= maxChar) text = it
onEdit(text)
},
modifier = Modifier.fillMaxWidth(),
label = { Text(label) },
leadingIcon = {
Icon(Icons.Default.Edit, null)
},
trailingIcon = {
IconButton(onClick = {
onRemove()
}) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = "Back"
)
}
},
maxLines = maxLines
)
Text(
text = "${text.length} / $maxChar",
textAlign = TextAlign.End,
style = MaterialTheme.typography.caption,
modifier = Modifier
.fillMaxWidth()
.padding(end = 16.dp)
)
}
}

The problem is here.
var text by remember { mutableStateOf(word) }
Without supplying a key to Input's remember, compose will not be able refresh/update your remaining Input's states during Main's re-composition every time an Input is removed.
You can use the word parameter as key for remember to re-calculate every composition pass (i.e when you add/remove or typed a value in the TextField), and your code should probably work as you expected.
var text by remember(word) { mutableStateOf(word) }

Have you tried doing the following instead?
listOfWords.forEachIndexed { index, word ->
... // rest of code
{
listOfWords.removeAt(index)
}

Related

Nested Column doesn't Recompose

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 + ""

Cannot calculate specific dimensions for composable TextField

I am trying to create multiple items to encapsulate the specific behavior of every component but I cannot specify the dimensions for every view.
I want a Textfield with an X icon on its right
setContent {
Surface(
modifier = Modifier
.fillMaxSize()
.background(color = white)
.padding(horizontal = 15.dp)
) {
Row(horizontalArrangement = Arrangement.spacedBy(15.dp)) {
Searcher(
modifier = Modifier.weight(1f),
onTextChanged = { },
onSearchAction = { }
)
Image(
painter = painterResource(id = R.drawable.ic_close),
contentDescription = null,
colorFilter = ColorFilter.tint(blue)
)
}
}
}
The component is the following
#Composable
fun Searcher(
modifier: Modifier = Modifier,
onTextChanged: (String) -> Unit,
onSearchAction: () -> Unit
) {
Row {
SearcherField(
onTextChanged = onTextChanged,
onSearchAction = onSearchAction,
modifier = Modifier.weight(1f)
)
CircularSearch(
modifier = Modifier
.padding(horizontal = 10.dp)
.align(CenterVertically)
)
}
}
and the SearcherField:
#Composable
fun SearcherField(
modifier: Modifier = Modifier,
onTextChanged: (String) -> Unit,
onSearchAction: () -> Unit
) {
var fieldText by remember { mutableStateOf(emptyText) }
TextField(
value = fieldText,
onValueChange = { value ->
fieldText = value
if (value.length > 2)
onTextChanged(value)
},
singleLine = true,
textStyle = Typography.h5.copy(color = White),
colors = TextFieldDefaults.textFieldColors(
cursorColor = White,
focusedIndicatorColor = Transparent,
unfocusedIndicatorColor = Transparent,
backgroundColor = Transparent
),
trailingIcon = {
if (fieldText.isNotEmpty()) {
IconButton(onClick = {
fieldText = emptyText
}) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = emptyText
)
}
}
},
placeholder = {
Text(
text = stringResource(id = R.string.dondebuscas),
style = Typography.h5.copy(color = White)
)
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
onSearchAction()
}
),
modifier = modifier.fillMaxWidth()
)
}
But I don´t know why, but the component Searcher with the placeholder is rendered in two lines.
It´s all about the placeholder that seems to be resized for not having enough space because if I remove the placeholder, the component looks perfect.
Everything is in one line, not having a placeholder of two lines. I m trying to modify the size of every item but I am not able to get the expected result and I don´t know if the problem is just about the placeholder.
How can I solve it? UPDATE -> I found the error
Thanks in advance!
Add maxlines = 1 to the placeholder Text's parameters.
Your field is single -lune but your text is multi-line. I think it creates conflict in implementation.
Okay... I find that the problem is about the trailing icon. Is not visible when there is no text in the TextField but is still occupying some space in the view, that´s why the placeholder cannot occupy the entire space. The solution is the following.
val trailingIconView = #Composable {
IconButton(onClick = {
fieldText = emptyText
}) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = emptyText
)
}
}
Create a variable with the icon and set it to the TextField only when is required
trailingIcon = if (fieldText.isNotEmpty()) trailingIconView else null,
With that, the trailing icon will be "gone" instead of "invisible" (the old way).
Still have a lot to learn.

Content in Dialog not visible

I've wrapped a Dialog in Compose, Android. However, things don't seem to show up. Not sure what I need to do here, to fix this properly for it to work naturally speaking. Because, I plan on using inputs and other stuff e.g., buttons etc.
#SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
#OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
#Composable
fun MyDialog(
openDialog: Boolean,
closeDialog: () -> Unit,
) {
if (openDialog) {
Dialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
onDismissRequest = closeDialog,
content = {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
SmallTopAppBar(
modifier = Modifier.padding(0.dp, 0.dp, 16.dp, 10.dp),
title = {
Text(
text = "Add new item",
style = MaterialTheme.typography.titleMedium,
)
},
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.background
),
navigationIcon = {
IconButton(onClick = {
closeDialog()
}) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = null
)
}
},
actions = {
Text(
"Save",
fontWeight = FontWeight.SemiBold
)
},
)
},
){
Text("Hello world!") // <-- Does not show up
}
}
)
}
}
Produces:
The reason is that you are ignoring the innerPadding values which comes fro Scaffold . You should be using it as the padding for your outer composable as Modifier.padding(it).
Text("Hello world!", modifier = Modifier.padding(it))
Above code should work . for further use Wrap the content in a container in this case Column .
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.padding(it)
.verticalScroll(state = scrollState)
.fillMaxSize()
) {
Text("Hello world!")
}
to Show a dialog you do not have to pass the immutable state to Dialog composable i.e openDialog: Boolean . Here is better example how you should handle Dialog state ..

None if the following functions can be called with the Arguments supplied Jetpack Compose Error

I keep getting this error whenever I try to use a textfield in Compose, I have tried both Textfield implementations, ie one with a String value and TextFieldValue arguments but still get the error, I have also tried using
var text = rememberSaveable{mutableStateOf("")}
and
var text by remember {mutableStateOf("")}. I have also tried hoisting the State which is what I wanted to do in the first place but still get the error
Here's the code
fun SearchAppBar(
query: String,
onQueryChanged: (String) -> Unit,
onExecuteSearch: () -> Unit,
scrollPosition: Int,
selectedCategory: FoodCategory?,
onSelectedCategoryChanged: (String) -> Unit,
onCategoryChangePosition: (Int) -> Unit,
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color.White,
elevation = 8.dp,
) {
Column {
Row(
modifier = Modifier.fillMaxWidth(),
) {
TextField(
modifier = Modifier
.fillMaxWidth(.9f)
.padding(8.dp),
value = query,
onValueChange = {
onQueryChanged(it)
},
label = {
Text(text = "Search")
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done,
),
leadingIcon = {
Icon(Icons.Filled.Search)
},
onImeActionPerformed = { action, softKeyboardController ->
if (action == ImeAction.Done) {
onExecuteSearch()
softKeyboardController?.hideSoftwareKeyboard()
}
},
textStyle = TextStyle(color = MaterialTheme.colors.onSurface),
backgroundColor = MaterialTheme.colors.surface
)
}
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 8.dp, bottom = 8.dp)
.horizontalScroll(scrollState)
) {
scope.launch {
scrollState.scrollTo(
scrollPosition
)
}
for (category in getAllFoodCategories()) {
FoodCategoryChip(
category = category.value,
isSelected = selectedCategory == category,
onSelectedCategoryChanged = {
onSelectedCategoryChanged(it)
onCategoryChangePosition(
getAllFoodCategories().indexOf(selectedCategory)
)
},
onExecuteSearch = {
onExecuteSearch()
}
)
}
}
}
}
}
You can only use the combination of parameters contained in only one of the implementations. You can't, for example, use keyboardOptions alongside onImeActionPerformed.
Turns out the backgroundColor property isnt valid anymore, use colors:. Also as pointed out by Richard Onslow Roper in his answer, look through the constructor of the TextField and see which properties are not part of your the Version of TextField you want to use
Don't do
TextField(
value = query,
onValueChange = {
onQueryChanged(it)
},
label = {
Text(text = "Search")
},
backgroundColor = MaterialTheme.colors.surface
)
do this instead
TextField(
value = query,
onValueChange = {
onQueryChanged(it)
},
label = {
Text(text = "Search")
},
colors= TextFieldDefaults.textFieldColors(
backgroundColor = MaterialTheme.colors.surface
)
)

Jetpack Compose Smart Recomposition

I'm doing experiments to comprehend recomposition and smart recomposition and made a sample
Sorry for the colors, they are generated with Random.nextIn() to observe recomposition visually, setting colors has no effect on recomposition, tried without changing colors either.
What's in gif is composed of three parts
Sample1
#Composable
private fun Sample1() {
Column(
modifier = Modifier
.background(getRandomColor())
.fillMaxWidth()
.padding(4.dp)
) {
var counter by remember { mutableStateOf(0) }
Text("Sample1", color = getRandomColor())
Button(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = {
counter++
}) {
Text("Counter: $counter", color = getRandomColor())
}
}
}
I have no questions here since smart composition works as expected, Text on top is not reading changes in counter so recomposition only occurs for Text inside Button.
Sample2
#Composable
private fun Sample2() {
Column(
modifier = Modifier.background(getRandomColor())
) {
var update1 by remember { mutableStateOf(0) }
var update2 by remember { mutableStateOf(0) }
println("ROOT")
Text("Sample2", color = getRandomColor())
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 4.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = {
update1++
},
shape = RoundedCornerShape(5.dp)
) {
println("🔥 Button1️")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 2.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = { update2++ },
shape = RoundedCornerShape(5.dp)
) {
println("🍏 Button 2️")
Text(
text = "Update2: $update2",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
Column(
modifier = Modifier.background(getRandomColor())
) {
println("🚀 Inner Column")
var update3 by remember { mutableStateOf(0) }
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 2.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = { update3++ },
shape = RoundedCornerShape(5.dp)
) {
println("✅ Button 3️")
Text(
text = "Update2: $update2, Update3: $update3",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
Column() {
println("☕️ Bottom Column")
Text(
text = "Sample2",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
}
It also works as expected each mutableState is updating only the scope they have been observed in. Only Text that observes update2 and update3 is changed when either of these mutableStates are updated.
Sample3
#Composable
private fun Sample3() {
Column(
modifier = Modifier.background(getRandomColor())
) {
var update1 by remember { mutableStateOf(0) }
var update2 by remember { mutableStateOf(0) }
println("ROOT")
Text("Sample3", color = getRandomColor())
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 4.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = {
update1++
},
shape = RoundedCornerShape(5.dp)
) {
println("🔥 Button1️")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 2.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = { update2++ },
shape = RoundedCornerShape(5.dp)
) {
println("🍏 Button 2️")
Text(
text = "Update2: $update2",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
Column {
println("🚀 Inner Column")
var update3 by remember { mutableStateOf(0) }
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 2.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = { update3++ },
shape = RoundedCornerShape(5.dp)
) {
println("✅ Button 3️")
Text(
text = "Update2: $update2, Update3: $update3",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
// 🔥🔥 Reading update1 causes entire composable to recompose
Column(
modifier = Modifier.background(getRandomColor())
) {
println("☕️ Bottom Column")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
}
Only difference between Sample2 and Sample3 is Text at the bottom is reading update1 mutableState which causing entire composable to be recomposed. As you can see in gif changing update1 recomposes or changes entire color schema for Sample3.
What's the reason for recomposing entire composable?
Column(
modifier = Modifier.background(getRandomColor())
) {
println("☕️ Bottom Column")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
To have smart recomposition scopes play a pivotal role. You can check Vinay Gaba's What is “donut-hole skipping” in Jetpack Compose? article.
Leland Richardson explains in this tweet as
The part that is "donut hole skipping" is the fact that a new lambda
being passed into a composable (ie Button) can recompose without
recompiling the rest of it. The fact that the lambda are recompose
scopes are necessary for you to be able to do this, but not
sufficient
In other words, composable lambda are "special" :)
We wanted to do this for a long time but thought it was too
complicated until #chuckjaz had the brilliant realization that if the
lambdas were state objects, and invokes were reads, then this is
exactly the result
You can also check other answers about smart recomposition here, and here.
https://dev.to/zachklipp/scoped-recomposition-jetpack-compose-what-happens-when-state-changes-l78
When a State is read it triggers recomposition in nearest scope. And a scope is a function that is not marked with inline and returns Unit. Column, Row and Box are inline functions and because of that they don't create scopes.
Created RandomColorColumn that take other Composables and its scope content: #Composable () -> Unit
#Composable
fun RandomColorColumn(content: #Composable () -> Unit) {
Column(
modifier = Modifier
.padding(4.dp)
.shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp))
.background(getRandomColor())
.padding(4.dp)
) {
content()
}
}
And replaced
Column(
modifier = Modifier.background(getRandomColor())
) {
println("☕️ Bottom Column")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
with
RandomColorColumn() {
println("☕️ Bottom Column")
/*
🔥🔥 Observing update(mutableState) does NOT causes entire composable to recompose
*/
Text(
text = "🔥 Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
Only this scope gets updated as expected and we have smart recomposition.
What causes Text, or any Composable, inside Column to not have a scope, thus being recomposed when a mutableState value changes is Column having inline keyword in function signature.
#Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: #Composable ColumnScope.() -> Unit
) {
val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
Layout(
content = { ColumnScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
If you add inline to RandomColorColumn function signature you will see that it causes whole Composable to recompose.
Compose uses call sites defined as
The call site is the source code location in which a composable is
called. This influences its place in Composition, and therefore, the
UI tree.
If during a recomposition a composable calls different composables
than it did during the previous composition, Compose will identify
which composables were called or not called and for the composables
that were called in both compositions, Compose will avoid recomposing
them if their inputs haven't changed.
Consider the following example:
#Composable
fun LoginScreen(showError: Boolean) {
if (showError) {
LoginError()
}
LoginInput() // This call site affects where LoginInput is placed in Composition
}
#Composable
fun LoginInput() { /* ... */ }
Call site of a Composable function affects smart recomposition, and having inline keyword in a Composable sets its child Composables call site same level, not one level below.
For anyone interested here is the github repo to play/test recomposition

Categories

Resources