Storing emitted Flow values in a list - android

Storing emitted Flow values in a list
I have a list of URLs for each of them. I am emitting objects I need to save and display. Getting is working fine, as they say in a lazy grid. But, I lose the last one and would like to store it in a list that updates the UI.
This happens in the ViewModel
val pokemonFlowList: Flow<Pokemon> = *flow* { pokemonAddressResponse
.forEach(){ address ->
emit(repo.getListItems(address.url))
}
This happens in the UI
val poke = viewModel.pokemonFlowList.collectAsState(initial = "Loading....")
//by remember list to store
LazyVerticalGrid(
cells = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
){
// here I need to be able to foreach and display the all items
item(span = { *GridItemSpan*(itemColumn) } ) {
Text("Item is ${poke.value}", itemModifier)
}
}

Related

Is Compose's swipe-to-dismiss state always remember the old item based on id, even the list has been refresh to newer one?

I have a simple example app that can
Load a new list (of 2 items, with id 0 and 1, and random text for each)
It can swipe to dismiss any item.
If I
load the new list for the first time
swipe to delete the first item
load a new list (that has same ID, but different random text)
swipe to delete the second item
It will crash as shown in the GIF below
(You can get the code design from here https://github.com/elye/issue_android_jetpack_compose_swipe_to_dismiss_different_data_same_id)
The reason is crashes because, upon Swipe-to-Dismiss the 2 item (of the 2nd time loaded data), the item it found is still the 2 item of the 1st time loaded data.
It does seems dismissState (as shown code below) always remember the 1st time loaded data (instead of the new data loaded)
val dismissState = rememberDismissState(
confirmStateChange = {
Log.d("Track", "$item\n${myListState.value.toMutableList()}")
viewModel.removeItem(item)
true
}
)
Hence this causes the deletion to send the wrong item in for deletion, and thus causes the failure and crash.
The complete LazyColumn and SwipeToDismiss code is as below
LazyColumn(modifier = Modifier.fillMaxHeight()) {
items(
items = myListState.value,
key = { todoItem -> todoItem.id }
) { item ->
val dismissState = rememberDismissState(
confirmStateChange = {
viewModel.removeItem(item)
true
}
)
SwipeToDismiss(
state = dismissState,
background = {
dismissState.dismissDirection ?: return#SwipeToDismiss
Box(modifier = Modifier.fillMaxSize().background(Color.Red))
},
dismissContent = {
// The row view of each item
}
)
}
}
Is this
My issue, is that I miss out on anything to refresh the dismissState upon loading of new data?
A Google Bug, where SwipeToDismiss will always have to work with a list of Unique IDs . Even if the list is refreshed to a new list, it cannot have the same ID that colide with any item of the previous list
i.e. if I replace key = { todoItem -> todoItem.id } with key = { todoItem -> todoItem.title }, then it will all be good
rememberDismissState() will remember the confirmStateChange lambda, which is part of the DismissState. In your case, item can change, but the lambda only captures the initial item value, leading to the crash.
You can use rememberUpdatedState to solve this:
val currentItem by rememberUpdatedState(item)
val dismissState = rememberDismissState(
confirmStateChange = {
viewModel.removeItem(currentItem)
true
}
)

Jetpack Compose rememberSwipeableState recycling issue

I have a LazyColumn that's generating items from a list data structure, every item has a swipeable state. Once an item is deleted from the list (Data structure), it is also reflected in UI, a recomposition is initiated and the LazyColumn is updated - not showing the deleted item - Correctly.
The problem is, all the swipe state variables of the LazyColumn items remain as before the deletion, For example, if the list was red, green, blue and green is deleted, the swipe state of green which was 2nd in the list is now the swipe state of blue which is now second in the list. All the items shift left, but the states remain in place.
Here's the code:
var dailyItems= viewModel.getItems().observeAsState(initial = emptyList())
LazyColumn(...) {
items(dailyItems) { item ->
SomeItem(
item = item,
)
}
}
SomeItem has a swipeable sub component in it
#Composable
private fun SomeItem(
item: Item
) {
val swipeState = rememberSwipeableState(
initialValue = ItemState.HIDDEN,
confirmStateChange = {
onActionsReveal(item.id) // Note the use if item instance
true
}
)
Box(
Modifier.swipeable(
state = swipeState,
anchors = anchors,
orientation = Orientation.Horizontal,
enabled = true,
reverseDirection = isRtl
)
) {
...
}
}
val swipeState = rememberSwipeableState()
val swipeState is recreated when SomeItem is re-composed, I see a new memory address for assigned to it, I also see that item.id is different.
But either confirmStateChange is not being overridden or the previous instance of swipeState is referenced somehow in future invocations - When the confirmStateChange is invoked - it always refers to the initial item.id
Issue was solve by applying rememberUpdatedState to item.id
val id by rememberUpdatedState(item.id)
val swipeState = rememberSwipeableState(
initialValue = ItemState.HIDDEN,
confirmStateChange = {
onActionsReveal(id)
true
}
)

Material Swipe To Dismiss in Compose maks incorrect items for dismissal

I'm implementing drag/swipe to dismiss functionality in a simple notepad app implemented in Compose. I've run into a strange issue where SwipeToDismiss() in a LazyColumn dismisses not only the selected item but those after it as well.
Am I doing something wrong or is this a bug with SwipeToDismiss()? (I'm aware that it's marked ExperimentalMaterialApi)
I've used the Google recommended approach from here:
https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#swipetodismiss
this is where it happens:
/* ...more code... */
LazyColumn {
items(items = results) { result ->
Card {
val dismissState = rememberDismissState()
//for some reason the dismmissState is EndToStart for all the
//items after the deleted item, even adding new items becomes impossible
if (dismissState.isDismissed(EndToStart)) {
val scope = rememberCoroutineScope()
scope.launch {
dismissed(result)
}
}
SwipeToDismiss(
state = dismissState,
modifier = Modifier.padding(vertical = 4.dp),
/* ...more code... */
and here is my project with the file in question
https://github.com/davida5/ComposeNotepad/blob/main/app/src/main/java/com/anotherday/day17/ui/NotesList.kt
You need to provide key for the LazyColumn's items.
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.
Example
LazyColumn {
items(
items = stateList,
key = { _, listItem ->
listItem.hashCode()
},
) { item ->
// As it is ...
}
}
Reference

Removed From the lazy column list

Whenever I try to remove from the lazy column list I get arrayIndexOutOfBoundException
This is the array that
var productsList = remember { mutableStateListOf<Product>() }//I load products in this list
Whenever the user presses a certain button I do the following
productsList.remove(item)
I get array arrayIndexOutOfBoundException this is how I loop as well
itemsIndexed(productsList) { index, item ->
Anyway to avoid that error
Whole code for those interested:
fun MyProducts(navController: NavController,myProductsViewModel: MyProductsViewModel= viewModel()) {
var productsList = remember { mutableStateListOf<Product>() }
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
var currentImage = remember { mutableStateListOf<Int>() }
LaunchedEffect(key1 = Unit){
myProductsViewModel.getShop()
productsList.addAll(myProductsViewModel.productsList)
currentImage.addAll(List(productsList.size) {0})
}
var pickedImage: MutableState<String?> =remember { mutableStateOf("") }
BackHandler() {
navController.popBackStack()
}
LazyColumn(
Modifier
.fillMaxSize()
.padding(start = 16.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(8.dp)
,state = listState
) {
itemsIndexed(productsList) { index, item ->
.clickable {when(icon){
Icons.Default.Delete->{
scope.launch {
myProductsViewModel.removeProducts(item.product_id,item.shop_id,item)
productsList.remove(item)
}
}
Icons.Default.Edit->{
}
I am accessing the same list in the rest of the code but I don't think it is relevant to the problem
You have not provided a proper code example but it sounds like you are not updating the list and the lazy column properly so the item is removed from the list but the lazy column does not know about it.
From my point of view you are generally having some bad practice in your code. For example:
LaunchedEffect(key1 = Unit){
myProductsViewModel.getShop()
productsList.addAll(myProductsViewModel.productsList)
currentImage.addAll(List(productsList.size) {0})
}
This all looks like stuff that should be done in the viewmodel. Generally a composable is supposed to convert state to UI, that means, it shouldnt contain any business logic. The code snipped I just showed looks like something that can be done in the viewmodel.
Having something like this
var productsList = remember { mutableStateListOf<Product>() }
and then adding the elements with a LaunchedEffect is not how composables are supposed to be used.
First your myProductsViewModel.productsList should be a LiveData object thats holding your product list. Then you are supposed to do the following:
val productList by myProductsViewModel.productsList.observeAsState(emptyList())
Then you show it in your composable. If you want to change the list content, you should call a method for that on the viewmodel, which then updates the livedata object of your list accordingly.
I you hope I explained in clearly enough. Let me know if you have questions.

Jetpack compose list wrong item selected after reordering or filtering

I have a ViewModel that produces a StateFlow like this:
private val _profiles = MutableStateFlow<List<ProfileSnap>>(listOf())
val profiles: StateFlow<List<ProfileSnap>>
get() = _profiles
Values are updated in another fun:
private fun loadProfiles() = viewModelScope.launch {
_profiles.value = profileDao.getAll(profilesSearch, profilesSort)
}
Finally, in Compose I list all values (this is a simplified version of my code):
#Composable
fun SetContent(viewModel: ProfilesViewModel){
val profiles = viewModel.profiles.collectAsState()
LazyColumn(
modifier = Modifier
.fillMaxHeight()
) {
itemsIndexed(items = profiles.value) { _, profile ->
Text(
text = "(${profile.profileId}) ${profile.label}",
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
Log.d(TAG, "onLongPress: ${profile.profileId}")
},
onTap = {
Log.d(TAG, "onTap: ${profile.profileId}")
},
)
}
)
}
}
}
At the beginning, when I reach the list fragment and I click on an element, I get the correct corresponding profileId. But, when I apply a filter or I change the list sorting and the loadProfiles() function is called:
the list correctly changes accordingly to the new filtered and/sorted profiles
when I click on an element I get the wrong profileId, I seems the one of the previous list disposition!
What am I doing wrong? profiles are not up to date? But if they are not updated, why the list is "graphically" correct? Here what happens:
(1) A
-----
(2) B
-----
(3) C <== CLICK - onTap: 3 / LONGPRESS - onLongPress: 3
Change sort order:
(3) C
-----
(2) B
-----
(1) A <== CLICK - onTap: 3 [should has been 1] / LONGPRESS - onLongPress: 3 [should has been 1]
Thank you very much
You can check 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:
LazyColumn() {
items(
items = profiles.value,
key = { profile ->
// Return a stable + unique key for the item
profile.profileId
}
) { profile ->
//....
}
}
Following Gabriele's hint, that's a working version (I couldn't find the same signature for items function):
LazyColumn() {
items(
count = profiles.value.size,
key = { index -> profiles.value[index].profileId }
) { index ->
val profile = profiles.value[index]
Item(profile)
Line()
}
}

Categories

Resources