Animated LazyColumn and LazyRow - android

Animating items in LazyColumn and LazyRow in Compose is not yet supported:
https://developer.android.com/jetpack/compose/lists#item-animations
Follow issue tracker:
https://issuetracker.google.com/issues/150812265
However I created a small POC on a potential workaround until it's officially supported (Check answer), it's far from production ready and definitely contains bugs but just thought of sharing my small playground project

EDIT: The issue tracker now has an update with a solution using a modifier
Just made a small POC of a workaround on animating items in LazyColumn and LazyRow until a proper support is added:
https://github.com/RoudyK/AnimatedLazyColumn
DEF not production ready and happy to get any feedback
EDIT:
Example usage:
data class MainItem(
val id: String,
val text: String
)
val items = List(10) { MainItem(UUID.randomUUID().toString(), UUID.randomUUID().toString()) }
val state = rememberLazyListState()
AnimatedLazyColumn(
state = state,
items = items.map {
AnimatedLazyListItem(key = it.id, value = it.text) {
TextItem(viewModel, it)
}
}
)
AnimatedLazyRow(
state = state,
items = items.map {
AnimatedLazyListItem(key = it.id, value = it.text) {
TextItem(viewModel, it)
}
}
)

Related

Android Jetpack Compose: VM not updating data structure when modified

I’ve got a problem with a LazyColumn of elements that have a favourite button: basically when I tap the favourite button, the item that is being favourited (a document in my case) is changed in the underlying data structure in the VM, but the view isn’t updated, so I never see any change in the button state.
class MainViewModel(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) : ViewModel() {
var documentList = emptyList<PDFDocument>().toMutableStateList()
....
fun toggleFavoriteDocument(pdfDocument: PDFDocument) {
documentList.find {
it == pdfDocument
}?.let {
it.favorite = !it.favorite
}
}
}
The composables are:
#Composable
fun DocumentRow(
document: PDFDocument,
onDocumentClicked: (String, Boolean) -> Unit,
onFavoriteValueChange: (Uri) -> Unit
) {
HeartIcon(
isFavorite = document.favorite,
onValueChanged = { onFavoriteValueChange(document.uri) }
)
}
#Composable
fun HeartIcon(
isFavorite: Boolean,
color: Color = Color(0xffE91E63),
onValueChanged: (Boolean) -> Unit
) {
IconToggleButton(
checked = isFavorite,
onCheckedChange = {
onValueChanged()
}
) {
Icon(
tint = color,
imageVector = if (isFavorite) {
Icons.Filled.Favorite
} else {
Icons.Default.FavoriteBorder
},
contentDescription = null
)
}
}
Am I doing something wrong? because when I call the toggleFavouriteDocument in the ViewModel, I see it’s marked or unmarked as favorite but there is no recomposition at all anywhere.
I might be missing it because you didn't post the rest of your code, but your documentList in the VM isn't observable, so how would the Composable know that it got changed? It needs to be something like Flow or LiveData, and it needs to be observed in the Composable. Something like this:
in ViewModel:
val documentList = MutableLiveData<List<PDFDocument>>()
in Composable:
val documentList by viewModel.documentList.observeAsState(List<PDFDocument>())
And you'll probably have to change the way you modify items in documentList. LiveData is weird about mutable collections inside MutableLiveData, and modifying individual items doesn't trigger a state change. You have to create a copy of the list with the modified items, and then re-port the whole list to the LiveData variable:
fun toggleFavoriteDocument(pdfDocument: PDFDocument) {
documentList.value?.let { oldList ->
// create a copy of existing list
val newList = mutableListOf<PDFDocument>()
newList.addAll(oldList)
// modify the item in the new list
newList.find {
it == pdfDocument
}?.let {
it.favorite = !it.favorite
}
// update the observable
documentList.postValue(newList)
}
}
Edit: There's also a potential problem with the way that you're trying to update the favorite value in the existing list. Without knowing how PDFDocument is implemented, I don't know if you can use the = operator. You should test that to make sure that newList.find { it == pdfDocument } actually finds the document

What is the best way to avoid accidental repeated clicks on Any btn?

My problem was accidentally repeated clicks on LazyVerticalGrid element which is resolved by using:
var enabled by rememberSaveable { mutableStateOf(true) } and val scope = LocalLifecycleOwner.current.lifecycleScope.
LazyVerticalGrid(
state = lazyVGState,
cells = GridCells.Fixed(3),
contentPadding = PaddingValues(bottom = 100.dp)
) {
items(groupMap.keys.toList().sorted()) { item ->
Column(
modifier = Modifier.clickable(
enabled = enabled,
) {
enabled = false
navController.currentBackStackEntry?.savedStateHandle?.set(
CITY_WEATHER_LIST,
cityList
)
navController.navigate(Screen.CityForecastScreen.route)
scope.launchWhenStarted {
delay(10)
enabled = true
}
},
) {
// some elements
}
}
}
If i don't use enabled state, user may open an element for couple times.
Looking for community opinion.
THX.
The navigation framework provides an up to date and synchronous view of the navigation state in your app, so the safest way to prevent multiple clicks is by checking if you are still in the navigation destination hosting your LazyList by using
navController.currentDestination
and comparing that against the LazyList screen identifier.
​fun​ NavController.​safeNavigate​(​direction​:​ ​NavDirections​) {​
currentDestination?.getAction(direction.actionId)?.​run​ { navigate(direction) }
​}
And instead of navController.navigate, use navController.safeNavigate with the same arguments.

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

Why need the author to add the keyword remember in this #Composable?

The Code A is from the project ThemingCodelab, you can see full code here.
I think that the keyword remember is not necessary in Code A.
I have tested the Code B, it seems that I can get the same result just like Code A.
Why need the author to add the keyword remember in this #Composable ?
Code A
#Composable
fun Home() {
val featured = remember { PostRepo.getFeaturedPost() }
val posts = remember { PostRepo.getPosts() }
MaterialTheme {
Scaffold(
topBar = { AppBar() }
) { innerPadding ->
LazyColumn(contentPadding = innerPadding) {
item {
Header(stringResource(R.string.top))
}
item {
FeaturedPost(
post = featured,
modifier = Modifier.padding(16.dp)
)
}
item {
Header(stringResource(R.string.popular))
}
items(posts) { post ->
PostItem(post = post)
Divider(startIndent = 72.dp)
}
}
}
}
}
Code B
#Composable
fun Home() {
val featured =PostRepo.getFeaturedPost()
val posts = PostRepo.getPosts()
...//It's the same with the above code
}
You need to use remember to prevent recomputation during recomposition.
Your example works without remember because this view will not recompose while you scroll through it.
But if you use animations, add state variables or use a view model, your view can be recomposed many times(when animating up to once a frame), in which case getting data from the repository will be repeated many times, so you need to use remember to save the result of the computation between recompositions.
So always use remember inside a view builder if the calculations are at least a little heavy, even if right now it looks like the view is not gonna be recomposed.
You can read more about the state in compose in documentation, including this youtube video, which explains the basic principles.

LazyColumnFor is not smooth scrolling

So, I have implemented a lazycolumnfor to work with a list of recipe elements, the thing is that it does not smooth scroll, if I just scroll fast it stutters till the last element appears and not smooth scroll.
Is this an error from my side or do I need to add something else?
data class Recipe(
#DrawableRes val imageResource: Int,
val title: String,
val ingredients: List<String>
)
val recipeList = listOf(
Recipe(R.drawable.header,"Cake1", listOf("Cheese","Sugar","water")),
Recipe(R.drawable.header,"Cake2", listOf("Cheese1","Sugar1","Vanilla")),
Recipe(R.drawable.header,"Cake3", listOf("Bread","Sugar2","Apple")))
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RecipeList(recipeList = recipeList)
}
}
}
#Composable
fun RecipeCard(recipe:Recipe){
val image = imageResource(R.drawable.header)
Surface(shape = RoundedCornerShape(8.dp),elevation = 8.dp,modifier = Modifier.padding(8.dp)) {
Column(modifier = Modifier.padding(16.dp)) {
val imageModifier = Modifier.preferredHeight(150.dp).fillMaxWidth().clip(shape = RoundedCornerShape(8.dp))
Image(asset = image,modifier = imageModifier,contentScale = ContentScale.Crop)
Spacer(modifier = Modifier.preferredHeight(16.dp))
Text(text = recipe.title,style = typography.h6)
for(ingredient in recipe.ingredients){
Text(text = ingredient,style = typography.body2)
}
}
}
}
#Composable
fun RecipeList(recipeList:List<Recipe>){
LazyColumnFor(items = recipeList) { item ->
RecipeCard(recipe = item)
}
}
#Preview
#Composable
fun RecipePreview(){
RecipeCard(recipeList[0])
}
Currently (version 1.0.0-alpha02) Jetpack Compose has 2 Composable functions for loading image resources:
imageResource(): this Composable function, load an image resource synchronously.
loadImageResource(): this function loads the image in a background thread, and once the loading finishes, recompose is scheduled and this function will return deferred image resource with LoadedResource or FailedResource
So your lazyColumn is not scrolling smoothly since you are loading images synchronously.
So you should either use loadImageResource() or a library named Accompanist by Chris Banes, which can fetch and display images from external sources, such as network, using the Coil image loading library.
UPDATE:
Using CoilImage :
First, add Accompanist Gradle dependency, then simply use CoilImage composable function:
CoilImage(data = R.drawable.header)
Using loadImageResource() :
val deferredImage = loadImageResource(
id = R.drawable.header,
)
val imageModifier = Modifier.preferredHeight(150.dp).fillMaxWidth()
.clip(shape = RoundedCornerShape(8.dp))
deferredImage.resource.resource?.let {
Image(
asset = it,
modifier = imageModifier
)
}
Note: I tried both ways in a LazyColumnFor, and although loadImageResource() performed better than imageResource() but still it didn't scroll smoothly.
So I highly recommend using CoilImage
Note 2: To use Glide or Picasso, check this repository by Vinay Gaba
On the other note, LazyColumn haven't been optimised for scrolling performance yet, but I've just tested on 1.0.0-beta07 release and can confirm it's way smoother than 1.0.0-beta06
Compose.UI 1.0.0-beta07 relevant change log:
LazyColumn/Row will now keep up to 2 previously visible items active (not disposed) even when they are scrolled out already. This allows the component to reuse the active subcompositions when we will need to compose a new item which improves the scrolling performance. (Ie5555)

Categories

Resources