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).
Related
I have a todo list in Jetpack Compose displayed in LazyColumn.
data class TodoItem(val id: Int, val title: String, var urgent: Boolean = false)
val todoList = listOf(
TodoItem(0, "My First Task"),
TodoItem(1, "My Second Task which is really very very long. Super long. I mean longer than a line.", true),
TodoItem(2, "My Third Task"),
)
#Composable
fun MyTodoListView() {
LazyColumn(modifier = Modifier.fillMaxHeight()) {
items(items = todoList, itemContent = { item ->
var checked by remember { mutableStateOf(item.urgent) }
Row(modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f).padding(8.dp),
text = item.title)
Checkbox(
checked = checked,
onCheckedChange = {
checked = it
item.urgent = it
}
)
}
})
}
}
When I plan to update the value (through checkbox), I'll have to update is with a separate mutableState variable
onCheckedChange = {
checked = it
item.urgent = it
}
Is there a way to make it more direct, with only one variable to change instead of having to change both checked and item.urgent?
You can use an observable MutableList (like a SnapshotStateList) and then update the items by creating a copy.
Something like:
val todoList = remember {
listOf<TodoItem>(
TodoItem(0, "My First Task"),
//...
).toMutableStateList()
}
LazyColumn(modifier = Modifier.fillMaxHeight()) {
items(items = todoList, itemContent = { item ->
//...
Checkbox(
checked = item.urgent,
onCheckedChange = {
//checked = it
val index = todoList.indexOf(item)
todoList[index] = todoList[index].copy(urgent = it)
}
)
})
}
How to select multiple items in LazyColumn and finally add the selected items in a seperate list.
GettingTags(tagsContent ={ productTags ->
val flattenList = productTags.flatMap {
it.tags_list
}
Log.i(TAG,"Getting the flattenList $flattenList")
LazyColumn{
items(flattenList){
ListItem(text = {Text(it) })
if(selectedTagItem) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = Color.Green,
modifier = Modifier.size(20.dp)
)
}
}
}
})
Mutable variable state
var selectedTagItem by remember{
mutableStateOf(false)
}
First create a class with selected variable to toggle
#Immutable
data class MyItem(val text: String, val isSelected: Boolean = false)
Then create a SnapshotStateList via mutableStateListOf that contains all of the items, and can trigger recomposition when we update any item with new instance, add or remove items also. I used a ViewModel but it's not mandatory. We can toggle items using index or get selected items by filtering isSelected flag
class MyViewModel : ViewModel() {
val myItems = mutableStateListOf<MyItem>()
.apply {
repeat(15) {
add(MyItem(text = "Item$it"))
}
}
fun getSelectedItems() = myItems.filter { it.isSelected }
fun toggleSelection(index: Int) {
val item = myItems[index]
val isSelected = item.isSelected
if (isSelected) {
myItems[index] = item.copy(isSelected = false)
} else {
myItems[index] = item.copy(isSelected = true)
}
}
}
Create LazyColumn with key, key makes sure that only updated items are recomposed, as can be seen in performance document
#Composable
private fun SelectableLazyListSample(myViewModel: MyViewModel) {
val selectedItems = myViewModel.getSelectedItems().map { it.text }
Text(text = "Selected items: $selectedItems")
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(8.dp)
) {
itemsIndexed(
myViewModel.myItems,
key = { _, item: MyItem ->
item.hashCode()
}
) { index, item ->
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.Red, RoundedCornerShape(8.dp))
.clickable {
myViewModel.toggleSelection(index)
}
.padding(8.dp)
) {
Text("Item $index", color = Color.White, fontSize = 20.sp)
if (item.isSelected) {
Icon(
modifier = Modifier
.align(Alignment.CenterEnd)
.background(Color.White, CircleShape),
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = Color.Green,
)
}
}
}
}
}
Result
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...
)
I've gone through this codelab. In step number 7, when clicking on single row's text it's changing its color, but function will not keep track of it, meaning it will disappear after re-composition.
I want list to remember color of single item thus I've move state hoisting to the NameList function level.
Unfortunately it's not working.
Where's the bug?
#Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
items(items = names) { name, ->
val isSelected = remember { mutableStateOf(false)}
Greeting(name = name,isSelected.value){ newSelected -> isSelected.value = newSelected}
Divider(color = Color.Black)
}
}
}
#Composable
fun Greeting(name: String,isSelected : Boolean, updateSelected : (Boolean) -> Unit) {
val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)
Text(
modifier = Modifier
.padding(24.dp)
.background(color = backgroundColor)
.clickable(onClick = { updateSelected(!isSelected)}),
text = "Hello $name",
)
}
You should hoist your selection state to the caller of NameList function.
#Composable
fun MyScreen() {
// Fake list of names
val namesList = (1..100).map { "Item $it" }
// Here, we're keeping the selected positions.
// At the beginning, all names are not selected.
val selection = remember {
mutableStateListOf(*namesList.map { false }.toTypedArray())
}
NameList(
// list of names
names = namesList,
// list of selected items
selectedItems = selection,
// this function will update the list above
onSelected = { index, selected -> selection[index] = selected },
// just to occupy the whole screen
modifier = Modifier.fillMaxSize()
)
}
Then, your NameList will look like this:
#Composable
fun NameList(
names: List<String>,
selectedItems: List<Boolean>,
onSelected: (index: Int, selected: Boolean) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
itemsIndexed(items = names) { index, name ->
Greeting(
name = name,
isSelected = selectedItems[index],
updateSelected = { onSelected(index, it) }
)
Divider(color = Color.Black)
}
}
}
Nothing changes on Greeting function.
Here is the result:
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