ordering a list on jetpack compose Android - android

I'm using a lazy column to show a list, the items in the list have the attribute isWeekTopic which is a Boolean, in the list I need to show first those elements that have isWeekTopic = true, and then show the ones that have isWeekTopic = false, the items that has isWeekTopic = true are going to change every week.
I managed to print the ones that has isWeekTopic = true with a header on the design, but I can't make them the first elements to show. this is my code
LazyColumn(
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(18.dp)
) {
items(
items = topicsskill,
key = { topicsSkill ->
topicsSkill.topicsId
}
) { topicsSkill ->
TopicsItemComposable(
modifier = Modifier
.fillMaxWidth(),
topicsItem = topicsSkill,
onItemClicked = { onItemClicked(it) }
)
}
}

I believe that sortedBy can do what you need.
Sample data class:
data class Something(
val title: String,
val isWeekTopic: Boolean
)
Extension to create dummy list:
fun createList(size: Int): List<Something> = List(size = size) { index ->
Something(
title = "something $index",
isWeekTopic = Random.nextBoolean()
)
}
Extension to order the list:
fun List<Something>.ordered(
weekTopicFirst: Boolean
): List<Something> =
if (weekTopicFirst) this.sortedBy { !it.isWeekTopic }
else this.sortedByDescending { !it.isWeekTopic }
Usage:
val items = createList(size = 15)
println("printing true first")
items.ordered(weekTopicFirst = true).forEach { println(it) }
println("printing false first")
items.ordered(weekTopicFirst = false).forEach { println(it) }
You can try this code here.
In Compose, just pass the list normally:
// just an example scenario, ideally it comes from a viewmodel
val dummyItems = createList(size = 15).ordered(weekTopicFirst = true)
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(
items = dummyItems,
key = { it.title } // should be unique
) { something ->
Column(modifier = Modifier.fillMaxWidth()) {
Text(text = something.title)
Text(text = "week topic: ${something.isWeekTopic}")
}
}
}

Related

How to update an item in array with a lazy column item change?

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)
}
)
})
}

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 ->
...
}

How to select multiple items in LazyColumn in JetpackCompose

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

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).

animation o a lazycolumn android

I have a list with 10 items one of them have this elements "rankingCurrentPlace", "rankingPastPlace" and "isUser:true".
What i need to do its an animation on the lazycolumn if the api esponse is like this
"isUser:true", "rankingPastPlace:3" , "rankingCurrentPlace:7"
i need to show an animation in the list where the row starts in the third place and descend to the seventh place
is there a way to do this?
this is what I actually have
LazyColumn(
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
) {
items(
items = leaderboard,
key = { leaderBoard ->
leaderBoard.rankingPlace
}
) { leaderBoard ->
RowComposable( modifier = Modifier
.fillMaxWidth(),
topicsItem = leaderBoard,)
}
This answer works except when swapping first item with any item even with basic swap function without animation. I think it would be better to ask a new question about why swapping first item doesn't work or if it is bug. Other than that works as expected. If you need to move to items that are not in screen you can lazyListState.layoutInfo.visibleItemsInfo and compare with initial item and scroll to it before animation
1.Have a SnapshotStateList of data to trigger recomposition when we swap 2 items
class MyData(val uuid: String, val value: String)
val items: SnapshotStateList<MyData> = remember {
mutableStateListOf<MyData>().apply {
repeat(20) {
add(MyData( uuid = UUID.randomUUID().toString(), "Row $it"))
}
}
}
2.Function to swap items
private fun swap(list: SnapshotStateList<MyData>, from: Int, to: Int) {
val size = list.size
if (from in 0 until size && to in 0 until size) {
val temp = list[from]
list[from] = list[to]
list[to] = temp
}
}
3.Function to swap items one by one. There is a bug with swapping first item. Even if it's with function above when swapping first item other one moves up without showing animation via Modififer.animateItemPlacement().
#Composable
private fun animatedSwap(
lazyListState: LazyListState,
items: SnapshotStateList<MyData>,
from: Int,
to: Int,
onFinish: () -> Unit
) {
LaunchedEffect(key1 = Unit) {
val difference = from - to
val increasing = difference < 0
var currentValue: Int = from
repeat(abs(difference)) {
val temp = currentValue
if (increasing) {
currentValue++
} else {
currentValue--
}
swap(items, temp, currentValue)
if (!increasing && currentValue == 0) {
delay(300)
lazyListState.scrollToItem(0)
}
delay(350)
}
onFinish()
}
}
4.List with items that have Modifier.animateItemPlacement()
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
state = lazyListState,
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(
items = items,
key = {
it.uuid
}
) {
Row(
modifier = Modifier
.animateItemPlacement(
tween(durationMillis = 200)
)
.shadow(1.dp, RoundedCornerShape(8.dp))
.background(Color.White)
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentScale = ContentScale.FillBounds,
contentDescription = null
)
Spacer(modifier = Modifier.width(10.dp))
Text(it.value, fontSize = 18.sp)
}
}
}
Demo
#OptIn(ExperimentalFoundationApi::class)
#Composable
private fun AnimatedList() {
Column(modifier = Modifier.fillMaxSize()) {
val items: SnapshotStateList<MyData> = remember {
mutableStateListOf<MyData>().apply {
repeat(20) {
add(MyData(uuid = UUID.randomUUID().toString(), "Row $it"))
}
}
}
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
state = lazyListState,
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(
items = items,
key = {
it.uuid
}
) {
Row(
modifier = Modifier
.animateItemPlacement(
tween(durationMillis = 200)
)
.shadow(1.dp, RoundedCornerShape(8.dp))
.background(Color.White)
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentScale = ContentScale.FillBounds,
contentDescription = null
)
Spacer(modifier = Modifier.width(10.dp))
Text(it.value, fontSize = 18.sp)
}
}
}
var fromString by remember {
mutableStateOf("7")
}
var toString by remember {
mutableStateOf("3")
}
var animate by remember { mutableStateOf(false) }
if (animate) {
val from = try {
Integer.parseInt(fromString)
} catch (e: Exception) {
0
}
val to = try {
Integer.parseInt(toString)
} catch (e: Exception) {
0
}
animatedSwap(
lazyListState = lazyListState,
items = items,
from = from,
to = to
) {
animate = false
}
}
Row(modifier = Modifier.fillMaxWidth()) {
TextField(
value = fromString,
onValueChange = {
fromString = it
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
TextField(
value = toString,
onValueChange = {
toString = it
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
Button(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
onClick = {
animate = true
}
) {
Text("Swap")
}
}
}
Edit: Animating with Animatable
Another method for animating is using Animatable with Integer vector.
val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
val coroutineScope = rememberCoroutineScope()
val animatable = remember { Animatable(0, IntToVector) }
And can be used as
private fun alternativeAnimate(
from: Int,
to: Int,
coroutineScope: CoroutineScope,
animatable: Animatable<Int, AnimationVector1D>,
items: SnapshotStateList<MyData>
) {
val difference = from - to
var currentValue: Int = from
coroutineScope.launch {
animatable.snapTo(from)
animatable.animateTo(to,
tween(350 * abs(difference), easing = LinearEasing),
block = {
val nextValue = this.value
if (abs(currentValue -nextValue) ==1) {
swap(items, currentValue, nextValue)
currentValue = nextValue
}
}
)
}
}
on button click, i'm getting values from TextField fo i convert from String
Button(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
onClick = {
val from = try {
Integer.parseInt(fromString)
} catch (e: Exception) {
0
}
val to = try {
Integer.parseInt(toString)
} catch (e: Exception) {
0
}
alternativeAnimate(from, to, coroutineScope, animatable, items)
}
) {
Text("Swap")
}
Result
I suggest you to get your items from a data class. If your other items does not contain the variables you mentioned you can make them nullable in data class and put a condition checker in your lazycolumn
Like this
data class Items(
val otherItems: Other,
val rankingCurrentPlace: Int?,
val rankingLastPlace: Int?,
val isUser: Boolean?
)
Then you can make a list from this data class and pass it to lazycolumn
LazyColumn{
items(list){
(elements with condition)
}
}

Categories

Resources