Mutable State Checkbox Not Changing Appearance After Selection - Jetpack Compose - android

There is a similar question here. I tried these solutions along with some others and still not having any success.
I have a checklist for different options in a dialog in Jetpack Compose. When an optionItem is selected I can see that the state has changed, but the checkbox icon won't update.
val _optionItems by remember {
mutableStateOf(
optionItemArray!!.map {
OptionItem(
label = it.label,
value = it.value,
number = it.number,
type = it.type,
subtitle = it.subtitle,
group = it.group,
matchType = it.matchType,
isSelected = it.isSelected,
isHidden = it.isHidden,
)
}
)
}
val optionItems: MutableList<OptionItem> = _optionItems as MutableList<OptionItem>
fun setOptionSelectedAtIndex(index: Int, isSelected: Boolean){
(_optionItems as MutableList<OptionItem>)[index] = _optionItems[index].copy(isSelected = !isSelected)
}
LazyColumn {
items(optionItems.size) { i ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable
{
optionItems.mapIndexed { j, item ->
if(i == j){
setOptionSelectedAtIndex(i, item.isSelected)
} else item
}
}
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
optionItems[i].label?.let { Text(text = it) }
if (optionItems[i].isSelected) {
Icon(
Icons.Filled.CheckBox,
contentDescription = "Selected"
)
} else {
Icon(
Icons.Filled.CheckBoxOutlineBlank,
contentDescription = "Not Selected"
)
}
}
}
}
}
I know that the list is supposed to be called again, which is what I was assuming the setOptionSelectedAtIndex function was supposed to do, but I'm having quite a lot of trouble figuring this out.

Related

Rearranging LazyColumn Recompositing issues

I have a LazyColumn that takes a list from my Room database.
I am creating a button that can re arrange the list from newest first, or oldest first. The problem I'm having is that when I rearrange the list, the LazyColumns view drops to the bottom of the LazyColumn. I do NOT want the list view to change during the list change. I am using a key for the list which is where I suspect my issue is coming from.
When I disable the key, this is not an issue however, that comes with its own issues so I cannot disable it permanently. Does anyone know and easy fix to this?
my composable ->
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun MainScreen(navController: NavController, notesViewModel: NotesViewModel) {
val myUiState by notesViewModel.uiState.collectAsState()
val multiDelete = remember { mutableStateListOf<Note>() }
val scope = rememberCoroutineScope()
val state = rememberLazyListState()
Surface {
Column {
Row {
FloatingActionButton(onClick = { notesViewModel.updateStates(true) }) {}
FloatingActionButton(onClick = { notesViewModel.updateStates(false) }) {}
NewNote(navController)
if(multiDelete.isNotEmpty()){
FloatingActionButton(
onClick = {
scope.launch {
notesViewModel.deleteSelected(multiDelete)
delay(50)
multiDelete.clear()
}
}
) { Image(imageVector = Icons.Filled.Delete, contentDescription = "this") }
}
}
LazyColumn(
state = state,
horizontalAlignment = Alignment.CenterHorizontally,
contentPadding = PaddingValues(vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier
.background(color = Color.Gray)
.fillMaxSize()
.focusRequester(FocusRequester()),
) {
items(
if(myUiState.toggle) myUiState.allNotes else myUiState.allNotes.reversed(),
key = {notes -> notes.uid!!}
) {
notes ->
Column(
modifier = Modifier.animateItemPlacement()
) {
ConsoleCards(
note = notes,
onDeleteClick = {
notesViewModel.delete(notes)
},
onLongPress = {
if(multiDelete.contains(notes)) multiDelete.remove(notes) else multiDelete.add(notes)
},
onEditClick = {
notesViewModel.uid(notes.uid!!)
notesViewModel.header(notes.header!!)
notesViewModel.note(notes.note!!)
navController.navigate(route = PageNav.AddNote.name)
}
)
}
}
}
}
}
}
This may not be the best solution. Theres also a similar issue like this and this
itemsIndexed(
items = checkItems.sortedBy { it.checked.value },
key = { index, item -> if (index == 0) index else item.id }
) { index, entry ->
...
}

Single Selection - DeSelection in Lazy Column

PROBLEM ::: I want to create a lazy column where I can select or deselect only one option at a time. Right now, whenever I click on row component inside lazy column, all the rows get selected.
CODE :::
#Composable
fun LazyColumnWithSelection() {
var isSelected by remember {
mutableStateOf(false)
}
var selectedIndex by remember { mutableStateOf(0) }
val onItemClick = { index: Int -> selectedIndex = index }
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
items(100) { index ->
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
onItemClick.invoke(index)
if (selectedIndex == index) {
isSelected = !isSelected
}
}
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Text(text = "Item $index", modifier = Modifier.padding(12.dp), color = Color.White)
if (isSelected) {
Icon(imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = Color.Green,
modifier = Modifier.size(20.dp))
}
}
}
}
}
CURRENT RESULT :::
Before Clicking ->
After Clicking ->
You can see all the items are getting selected but I should be able to select or deselect one item at a time not all.
I tried to use remember state for selection but I think I'm doing wrong something in the index selection or maybe if statement.
This should probably give you a head start.
So we have 4 components here:
Data Class
Class state holder
Item Composable
ItemList Composable
ItemData
data class ItemData(
val id : Int,
val display: String,
val isSelected: Boolean = false
)
State holder
class ItemDataState {
val itemDataList = mutableStateListOf(
ItemData(1, "Item 1"),
ItemData(2, "Item 2"),
ItemData(3, "Item 3"),
ItemData(4, "Item 4"),
ItemData(5, "Item 5")
)
// were updating the entire list in a single pass using its iterator
fun onItemSelected(selectedItemData: ItemData) {
val iterator = itemDataList.listIterator()
while (iterator.hasNext()) {
val listItem = iterator.next()
iterator.set(
if (listItem.id == selectedItemData.id) {
selectedItemData
} else {
listItem.copy(isSelected = false)
}
)
}
}
}
Item Composable
#Composable
fun ItemDisplay(
itemData: ItemData,
onCheckChanged: (ItemData) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.border(BorderStroke(Dp.Hairline, Color.Gray)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = if (itemData.isSelected) "I'm selected!" else itemData.display)
Checkbox(
checked = itemData.isSelected,
onCheckedChange = {
onCheckChanged(itemData.copy(isSelected = !itemData.isSelected))
}
)
}
}
Finally the ItemList (LazyColumn)
#Composable
fun ItemList() {
val itemDataState = remember { ItemDataState() }
LazyColumn {
items(itemDataState.itemDataList, key = { it.id } ) { item ->
ItemDisplay(
itemData = item,
onCheckChanged = itemDataState::onItemSelected
)
}
}
}
All of these are copy-and-pasteable so you can run it quickly. The codes should be simple enough for you to dissect them easily and use them as a reference for your own use-case.
Notice that we use a data class here which has an id property to be unique and we're using it as a key parameter for LazyColumn's item.
I usually implement my UI collection components with a unique identifier to save me from potential headaches such as UI showing/removing/recycling wrong items.
Remember index instead of Boolean (isSelected).

Jetpack Compose LazyColumn inside Scrollabe Column

here's my situation: I have to show in my app a detail of a record I receive from API. Inside this view, I may or may not need to show some data coming from another viewmodel, based on a field.
Here my code:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun ViewDetail(viewModel: MainViewModel, alias: String?, otherViewModel: OtherViewModel) {
viewModel.get(alias)
Scaffold {
val isLoading by viewModel.isLoading.collectAsState()
val details by viewModel.details.collectAsState()
when {
isLoading -> LoadingUi()
else -> Details(details, otherViewModel)
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun Details(details: Foo?, otherViewModel: OtherViewModel) {
details?.let { sh ->
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState),
) {
Text(sh.title, fontSize = 24.sp, lineHeight = 30.sp)
Text(text = sh.description)
if (sh.other.isNotEmpty()) {
otherViewModel.load(sh.other)
val others by otherViewModel.list.collectAsState()
Others(others)
}
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun Others(others: Flow<PagingData<Other>>) {
val items: LazyPagingItems<Other> = others.collectAsLazyPagingItems()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
contentPadding = PaddingValues(16.dp),
) {
items(items = items) { item ->
if (item != null) {
Text(text = item.title, fontSize = 24.sp)
Spacer(modifier = Modifier.height(4.dp))
Text(text = item.description)
}
}
if (items.itemCount == 0) {
item { EmptyContent() }
}
}
}
All the description here may be very long, both on the main Details body or in the Others (when present), so here's why the scroll behaviour requested.
Problem: I get this error:
Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()).
I hoped that .wrapContentHeight() inside LazyColumn would do the trick, but to no avail.
Is this the right way to do it?
Context: all packages are updated to the latest versions available on maven
The main idea here is to merge your Column with LazyColumn.
As your code is not runnable, I'm giving more a pseudo code, which should theoretically work.
Also calling otherViewModel.load(sh.other) directly from Composable builder is a mistake. According to thinking in compose, to get best performance your view should be side effects free. To solve this issue Compose have special side effect functions. Right now your code is gonna be called on each recomposition.
if (sh.other.isNotEmpty()) {
LaunchedEffect(Unit) {
otherViewModel.load(sh.other)
}
}
val others by otherViewModel.list.collectAsState()
LazyColumn(
modifier = Modifier
.fillMaxSize()
.wrapContentHeight(),
contentPadding = PaddingValues(16.dp),
) {
item {
Text(sh.title, fontSize = 24.sp, lineHeight = 30.sp)
Text(text = sh.description)
}
items(items = items) { item ->
if (item != null) {
Text(text = item.title, fontSize = 24.sp)
Spacer(modifier = Modifier.height(4.dp))
Text(text = item.description)
}
}
if (items.itemCount == 0) {
item { EmptyContent() }
}
}
You can use a system like the following
#Composable
fun Test() {
Box(Modifier.systemBarsPadding()) {
Details()
}
}
#Composable
fun Details() {
LazyColumn(Modifier.fillMaxSize()) {
item {
Box(Modifier.background(Color.Cyan).padding(16.dp)) {
Text(text = "Hello World!")
}
}
item {
Box(Modifier.background(Color.Yellow).padding(16.dp)) {
Text(text = "Another data")
}
}
item {
Others()
}
}
}
#Composable
fun Others() {
val values = MutableList(50) { it }
values.forEach {
Box(
Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(text = "Value = $it")
}
}
}
The result with scroll is:

How to keep only one edit form open at a time in a composable function

I am showing a list of rows with one word in it, inside a LazyColumn. On clicking the row, an edit form opens. The data is coming from a room database.
Since the row is on a separate composable function, I can open many different edit forms together (one in each row). But I want to show only one edit form in the whole list at a time. If I click one row to open an edit form, the rest of the open forms on the other rows should be closed. How can I do that?
Here is the code:
val words: List<Word> by wordViewModel.allWords.observeAsState(listOf())
var newWord by remember { mutableStateOf("") }
val context = LocalContext.current
val keyboardController = LocalSoftwareKeyboardController.current
LazyColumn(
modifier = Modifier
.weight(1f)
.padding(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(words) { word ->
WordItemLayout(
word = word,
onSaveUpdatedWord = { onUpdateWord(it) },
onTrashClicked = { onDeleteWord(it) }
)
}
}
#Composable
fun WordItemLayout(word: Word, onSaveUpdatedWord: (Word) -> Unit, onTrashClicked: (Word) -> Unit) {
var showEditForm by remember { mutableStateOf(false) }
var editedWord by remember { mutableStateOf(word.word) }
val context = LocalContext.current
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.primaryVariant)
.padding(vertical = 12.dp, horizontal = 24.dp)
.clickable {
showEditForm = !showEditForm
editedWord = word.word
},
verticalAlignment = Alignment.CenterVertically,
) {
Image(painter = painterResource(R.drawable.ic_star), contentDescription = null)
Text(
text = word.word,
color = Color.White,
fontSize = 20.sp,
modifier = Modifier
.padding(start = 16.dp)
.weight(1f)
)
// Delete Button
IconButton(
onClick = {
showEditForm = false
onTrashClicked(word)
Toast.makeText(context, "Word deleted", Toast.LENGTH_SHORT).show()
},
modifier = Modifier.size(12.dp)
) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = "Delete Word",
tint = Color.White
)
}
}
// word edit form
if (showEditForm) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Bottom
) {
TextField(
value = editedWord,
onValueChange = { editedWord = it },
modifier = Modifier.weight(1f),
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.White) // TextField Background Color
)
// Update Button
Button(
onClick = {
val updatedWord: Word = word
if (updatedWord.word != editedWord.trim()) {
updatedWord.word = editedWord.trim()
onSaveUpdatedWord(updatedWord)
Toast.makeText(context, "Word updated", Toast.LENGTH_SHORT).show()
}
showEditForm = false
},
modifier = Modifier.padding(start = 8.dp)
) {
Icon(imageVector = Icons.Filled.Done, contentDescription = "Update Word")
}
}
}
}
}
Thanks for your help!
An approach: In your view model, declare an openRowIndex state (this will store the index of the opened row, you can initialize it to -1 for example).
Define a method that can change this state, for example updateOpenRowIndex
I'm not sure what kind of state holder you are using in your view model. I will use StateFlow for this answer. In your view model declare the new state and method:
private val _openRowIndex = MutableStateFlow(-1)
val openRowIndex: StateFlow<Int> = _openRowIndex
fun updateOpenRowIndex(updatedIndex: Int) {
_openRowIndex.value = updatedIndex
}
For each row compisable, pass in the index of it inside the LazyColumn. You can get the indices using the itemsIndexed method. Also collect your openRowIndex, and pass that to the composable as well. Pass in also the method that updates the open row index:
itemsIndexed(words) { index, word ->
//get the current opened row state and collect it (might look different for you if you are not using StateFlow):
val openRowIndex = wordViewModel.openRowIndex.collectAsState()
WordItemLayout(
word = word,
onSaveUpdatedWord = { onUpdateWord(it) },
onTrashClicked = { onDeleteWord(it) },
index = index, //new parameter!
openRowIndex = openRowIndex.value //new parameter!
onUpdateOpenedRow = wordViewModel::updateOpenRowIndex //new parameter!
)
}
Now, in the row composable, simply check if the index and openRowIndex match, and display an opened row only if they match. Now to update the open row: make the Row clickable, and on click use view models updateOpenRowIndex method to update state to index. Compose will handle the rest and recompose when the state changes with the newly opened row!
fun WordItemLayout(
word: Word,
onSaveUpdatedWord: (Word) -> Unit,
onTrashClicked: (Word), -> Unit,
index: Int, //new parameters
openRowIndex: Int,
onUpdateOpenedRow: (Int) -> Unit
) {
if(index == openRowIndex) {
//display this row as opened
} else {
//display this row as closed
}
}
As I said, make the row clickable and call the update function:
Row(
modifier = Modifier.clickable {
onUpdateOpenedRow(index)
//additional instructions for what to happen when row is clicked...
}
//additional row parameters...
)

Compose LazyColumn select one item

I want to select one item of my LazyColumn and change the textcolor.
How is it possible to identify which item is selected?
Code:
val items = listOf(Pair("A", 1), Pair("AA", 144), Pair("BA", 99))
var selectedItem by mutableStateOf(items[0])
LazyColumn {
this.items(items = items) {
Row(modifier = Modifier.clickable(onClick = {selectedItem = it}) {
if (selectedItem == it) {
Text(it.first, color = Color.Red)
} else {
Text(it.first)
}
}
}
}
Depending how I save it (with remember or without) they just highlight both if I click on one and not just the one I clicked the last.
You can use the the .selectable modifier instead of .clickable
Something like:
data class Message(val id: Int,
val message : String)
val messages : List<Message> = listOf(...))
val listState = rememberLazyListState()
var selectedIndex by remember{mutableStateOf(-1)}
LazyColumn(state = listState) {
items(items = messages) { message ->
Text(
text = message.message,
modifier = Modifier
.fillMaxWidth()
.background(
if (message.id == selectedIndex)
Color.Red else Color.Yellow
)
.selectable(
selected = message.id == selectedIndex,
onClick = { if (selectedIndex != message.id)
selectedIndex = message.id else selectedIndex = -1})
)
}
}
In your case you can use:
var selectedItem by remember{mutableStateOf( "")}
LazyColumn {
this.items(items = items) {
Row(modifier = Modifier.selectable(
selected = selectedItem == it.first,
onClick = { selectedItem = it.first}
)
) {
if (selectedItem == it.first) {
Text(it.first, color = Color.Red)
} else {
Text(it.first)
}
}
}
}
Note that in the accepted answer, all the item views will be recomposed every time the selection changes, because the lambdas passed in onClick and content (of Row) are not stable (https://developer.android.com/jetpack/compose/lifecycle#skipping).
Here's one way to do it so that only the deselected and selected items are recomposed:
#Composable
fun ItemView(index: Int, selected: Boolean, onClick: (Int) -> Unit){
Text(
text = "Item $index",
modifier = Modifier
.clickable {
onClick.invoke(index)
}
.background(if (selected) MaterialTheme.colors.secondary else Color.Transparent)
.fillMaxWidth()
.padding(12.dp)
)
}
#Composable
fun LazyColumnWithSelection(){
var selectedIndex by remember { mutableStateOf(0) }
val onItemClick = { index: Int -> selectedIndex = index}
LazyColumn(
modifier = Modifier.fillMaxSize(),
){
items(100){ index ->
ItemView(
index = index,
selected = selectedIndex == index,
onClick = onItemClick
)
}
}
}
Note how the arguments passed to ItemView only change for the items whose selected state changes. This is because the onItemClick lambda is the same all the time.
There is a clickable modifier which allows easy detection of a click, and it also provides accessibility features and displays visual indicators when tapped (such as ripples).
#Composable
fun ClickableSample() {
val count = remember { mutableStateOf(0) }
// content that you want to make clickable
Text(
text = count.value.toString(),
modifier = Modifier.clickable { count.value += 1 }
)
}
For more information see https://developer.android.com/jetpack/compose/gestures

Categories

Resources