LazyColumn - Items Key parameter purpose? - android

Can someone explain me what's the main purpose of the 'key' parameter inside items/itemsIndexed function of LazyListScope? What do we get or don't get if we specify that parameter? I'm not sure that I understand the official docs related to this parameter:
key - a factory of stable and unique keys representing the item. Using
the same key for multiple items in the list is not allowed. Type of
the key should be saveable via Bundle on Android. If null is passed
the position in the list will represent the key. When you specify the
key the scroll position will be maintained based on the key, which
means if you add/remove items before the current visible item the item
with the given key will be kept as the first visible one.

I think the best answer is provided by the official doc:
By default, each item's state is keyed against the position of the item in the list. However, this can cause issues if the data set changes, since items which change position effectively lose any remembered state. If you imagine the scenario of LazyRow within a LazyColumn, if the row changes item position, the user would then lose their scroll position within the row.
To combat this, you can provide a stable and unique key for each item, providing a block to the key parameter. Providing a stable key enables item state to be consistent across data-set changes:
#Composable
fun MessageList(messages: List<Message>) {
LazyColumn {
items(
items = messages,
key = { message ->
// Return a stable + unique key for the item
message.id
}
) { message ->
MessageRow(message)
}
}
}

you can use this way
#Composable
fun A(list: MutableList<Model>) {
Column {
LazyColumn {
items(
count = list.size,
key = {
list[it].id
}, itemContent = { index ->
Text(text = list[index].text)
}
)
}
}
}

Related

LazyColumn is showing wrong display when deleting an item

I'm creating an app that, among other things, enables the user to add a Consumer, and then remove him later. The consumers are shown in cards with a remove button in the end.
Adding a consumer works fine. However, when I try to remove a consumer, the one removed in the app screen is always the last one. I know this is not a logic implementation mistake, because I stopped the Debugger right before the items() call, and in any recomposition the list holding the consumers has the correct consumer removed! The following image shows the result after clicking the Remove button from the "B" card (the card removed is "C"!):
Look what the debugger shows right before the recomposition takes place:
The relevant code is below.
The ViewModel and Model (relevant part) definitions:
class ConsumidoresViewModel : ViewModel() {
var lista = mutableStateListOf<Consumidor>()
fun add(consumidor: Consumidor){
lista += consumidor
}
fun remove(consumidor: Consumidor){
lista.remove(consumidor)
}
}
data class Consumidor(var nome: String)
...
The main composable, called directly from .onCreate():
fun UsersView() {
var consumidores: ConsumidoresViewModel = viewModel()
...
LazyColumn() {
items(items = consumidores.lista) { consumidor ->
CardNome(consumidor, consumidores)
}
}
The fucntion call of the Remove button:
IconButton(onClick = { consumidorViewModel.remove(consumidor) }) { ... }
I can't figure out what I'm doing wrong. I'm fairily new with Android Programming / Compose, but I have been programming for decades (not professionaly). Can someone point me to a direction? It probably has something to do with my Sates / View Model implementation, but I can't find out what, as the SnapshotStateList on the debugger clearly shows "A" and "C" cards present, and "B" gone!
Based on the official docs.
By default, each item's state is keyed against the position of the item in the list or grid. However, this can cause issues if the data set changes, since items which change position effectively lose any remembered state. If you imagine the scenario of LazyRow within a LazyColumn, if the row changes item position, the user would then lose their scroll position within the row.
So it's usually a good set up when your data class has a unique property like an id if you plan to manipulate a collection of it (like your removal operation), you can then use it as a key = {...} for the LazyColumn so it knows not to use the index as a unique identifier for its item elements, and that could be the reason why your'e having a wrong display of items after removing an element from the list.
LazyColumn() {
items(items = consumidorList, key = { it.id }) { consumidorItem ->
...
}
}
Update:
Linking my another answer for a movableContentOf{...} sample.

How can I optimize a search for elements of an array within another array

I have an initial list of elements in common
val commonList = mutableListOf<String>
I have the following object
ClassObject(val title: String, val subjects: List<String>)
Assuming you have such a list with initial data
val list = listOf<ClassObject> /* With a size of 1000 samples */
How can I iterate the initial list "list" and look in the list of "subjects" for the elements that are not in "commonList" and add them to this last list.
I had thought of a nested for loop, but it would require a lot of power and time.
list.forEach { c ->
c.subjects.forEach { s ->
if (s !in commonList)
commonList.add(s)
}
}
You should use MutableSet instead of MutableList for this purpose. Using an in check on a Set does not require iteration so it is far more performant. It would also make your code a lot simpler, because it won't redundantly add items, so you can simply use an addAll call on it to achieve the same thing as if you had first checked if each item is in the set first.
val commonSet = mutableSetOf<String>()
val list = listOf<ClassObject>( /*...*/ )
for (classObject in list) {
commonSet.addAll(classObject.subjects)
}
In Big-O notation, the above is O(n), whereas your original code is O(n^2).
So to paraphrase, it seems you want to accumulate all unique entries from your nested list into a flat list commonList.
In which case, using a MutableSet would be a good idea. I'm not familiar with the Kotlin implementation of sets, but traditionally they are like hashmaps that don't map keys to values but instead hold unique keys. The difference from a simple List being that they are internally more sophisticated than a plain array, and benefit from faster collision look-ups.
In conclusion, you can't really avoid the nested loop, but you can eliminate the wasteful !in check. If you must end up with a list at the end, you can always call toList on the set.

Should the index passed to notifyItemChanged consider placeholder items?

PagingDataAdapter from Paging3 library internally manages two semantically different lists. adapter.snapshot() returns a ItemSnapshotList which includes placeholder elements. adapter.snapshot().items return a List which excludes placeholder items.
Now I had to update an element using it's id. Should find and pass the index based on ItemSnapshotList or List? Eg:
adapter.notifyItemChanged(
adapter.snapshot().indexOfFirst { it!!.id == id } // is this correct ?
adapter.snapshot().items.indexOfFirst { it.id == id } // or is this ?
, PAYLOAD_STATUS to Status.Active // payloads
)
I think it's impossible. Any change on your item,you need to destroy and rebuild the pagingList.
I think the best practice is change in your room, and the list observe the room.

Equivalent to adapter.notifydatasetchange or Diffutils in Jetpack Compose Lazy Column/Row

How to update, delete or add item in a LazyColumn in Jetpack Compose ?
Which is easily achieve by adapter.notifydatasetchange or diffutils in recyclerview
Just do exactly that on the collection you are passing to items function. This is simple as that. If your class is stable (just use data class) you are fine, it will just work. Welcome to Compose magic.
If you want the best performance on update/delete/add then expose a stable key in the items function, see here for more details: https://developer.android.com/jetpack/compose/lists#item-keys
Example:
#Composable
fun MessageList(messages: List<Message>) {
LazyColumn {
items(
items = messages,
key = { message ->
// Return a stable + unique key for the item
message.id
}
) { message ->
// Display entry here
MessageRow(message)
}
}
}
Here, if you provide the key lambda, the compose will know that this entry is the same entry - just with different content. If you do not provide this lambda - index in list will be used as key. So any addition other than at the end of the list would trigger a lot of recompositions. So it is more or less like a diff utils. You only have to provide this, because content equality is handled by compose implicitly - via equals of the Message object.
So if you want to remove one message from the list - remove it and pass new list to MessageList. Compose will handle the rest for you

How can we mutate items loaded using Paging 3 android?

I am loading posts from network and for this i'm using Paging 3, but now problem is that my list items contains Like/Dislike button, suppose Like is clicked then how can i update data for that item without reloading whole dataset?
i have read this android-pagedlist-updates, but it seems that this is for older Paging 2 or 1, So what is perfect way to to achieve this in Paging 3
In Paging3 you still need to rely on PagingSource.invalidate to submit updates, this isn't so much about immutability, but more about having a single source of truth.
In general, the correct way to do this is to update the backing dataset and call invalidate, which will trigger a REFRESH + DiffUtil that shouldn't cause any UI changes, but guarantees that if that page is dropped and refetched, the loaded pages will still be up-to-date. The easiest way to do this is to use a PagingSource implementation that already has self-invalidation built-in, like the one provided by Room, and just update the corresponding row onClick of the like / dislike button.
There is an open bug tracking the work to support highly frequent, granular updates to the list with a Flow<>, which you can follow here if this is your use case: https://issuetracker.google.com/160232968
I overcome this challenge with below mechanism. Maintain the internal Hashmap to hold key and object, keep this internal hashmap inside your pagedlist adapter. As the list scrolls , you will add remote like/dislike into internal hashmap as initial status by using its something unique key, since the key is unique, you will not going to duplicate and then you refer this internal hashmap for your update UI.
onClick listener of like and dislike will update this internal hashmap. again internal hashmap is reference for UI update.
Solution is simple - collecting helpful data on another internal hashmap for later manipulation.
I found a work-around with which u can achieve this, giving some of the background of the code-base that I am working with:
I have a PagingDataAdapter (Pagination-3.0 adapter) as a recycler view adapter.
I have a viewmodel for fragment
I have a repository which returns flow of PaginationData
And exposing this flow as liveData to fragment
Code for repository:
override fun getPetsList(): Flow<PagingData<Pets>> {
return Pager(
config = PagingConfig(
pageSize = 15,
enablePlaceholders = false,
prefetchDistance = 4
),
pagingSourceFactory = {
PetDataSource(petService = petService)
}
).flow
}
Code for viewmodel:
// create a hashmap that stores the (key, value) pair for the object that have changed like (id:3, pet: fav=true .... )
viewModelScope.launch {
petRepository.getPetsList()
.cachedIn(viewModelScope)
.collect {
_petItems.value = it
}
}
Now the code for fragment where mapping and all the magic happens
viewModel.petItems.observe(viewLifecycleOwner) { pagingData ->
val updatedItemsHashMap = viewModel.updatedPetsMap
val updatedPagingData = pagingData.map { pet ->
if (updatedItemsHashMap.containsKey(pet.id))
updatedItemsHashMap.getValue(pet.id)
else
pet
}
viewLifecycleOwner.lifecycleScope.launch {
petAdapter.submitData(updatedPagingData)
}
}
So that is how you can achieve this, the crux is to do mapping of pagingData which is emitted from repository.
Things which won't work:
_petItems.value = PagingData.from(yourList)
This won't work because as per docs this is used for static list, and you would loose the pagination power that comes with pagination 3.0. So mapping pagingData seems the only way.

Categories

Resources