How to minimize number of recompositions of `AsyncImage` from Coli

491 Views Asked by At

I am writing simple item displaying image and some text, and also having functionality to add item to favorite.

@Composable
fun ItemView(
    item: Item,
    modifier: Modifier = Modifier,
    onFavoriteClick: (Item) -> Unit = {},
) {
    Row(modifier = modifier) {
        AsyncImage(
            model = remember (item.id) {item.url},
            contentDescription = "img",
            contentScale = ContentScale.Crop,
            placeholder = painterResource(id = R.drawable.restaurant_placeholder),
            error = painterResource(id = R.drawable.restaurant_placeholder),
            modifier = Modifier
                .size(48.dp)
                .clip(RoundedCornerShape(10)),
        )
        Text(text = item.name)
        IconToggleButton(
            checked = isFavourite,
            onCheckedChange = remember { { onFavoriteClick(item) } }
        ) {
            Icon(
                imageVector = if (item.isFavourite) {
                    Icons.Filled.Favorite
                } else {
                    Icons.Default.FavoriteBorder
                },
                contentDescription = null,
            )
        }
    }
}

Without AsyncImage I noticed that ItemView is not recomposed when being added to favorite (only icon is new), but when I add AsyncImage whole ItemView recompose and AsyncImage as well.

I tried to wrap ContentScale and Modifier into remember, but it still didn't work out.

AsyncImage(
            model = remember (item.id) {item.url},
            contentDescription = "img",
            contentScale = remember {ContentScale.Crop},
            placeholder = painterResource(id = R.drawable.placeholder),
            error = painterResource(id = R.drawable.placeholder),
            modifier = remember {Modifier
                .size(48.dp)
                .clip(RoundedCornerShape(10)) },
)

Also I noticed that when I removed placeholder and error I got one recomposition less. I thought is is normal that AsyncImage will recompose when image is loaded but ItemView shouldn't. What do I do wrong?

I create that items this way (I think using id as a key makes more sense than hash to not create new item each time it's favorite status change):

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MainView(vm: ItemViewModel) {
    LazyColumn {
        items(
            count = vm.items.size,
            key = { idx -> vm.items[idx].id },
        ) { idx ->
            ItemView(
                item = vm.items[idx],
                modifier = remember { Modifier.animateItemPlacement() },
                onFavoriteClick = remember { vm::onFavoriteClick },
            )
        }
    }
}
1

There are 1 best solutions below

0
On

The reason why the ItemView is recomposed when using the AsyncImage is that the AsyncImage composable is a stateful composable, which means that it creates and manages its own state. When the image finishes loading, the AsyncImage composable updates its state, which causes a recomposition of the entire ItemView.

To avoid unnecessary recompositions, you can try using the remember function to memoize the AsyncImage composable, like this:

val memoizedAsyncImage = remember(item.id, item.url) {
    AsyncImage(
        model = item.url,
        contentDescription = "img",
        contentScale = ContentScale.Crop,
        placeholder = painterResource(id = R.drawable.restaurant_placeholder),
        error = painterResource(id = R.drawable.restaurant_placeholder),
        modifier = Modifier
            .size(48.dp)
            .clip(RoundedCornerShape(10)),
    )
}

Row(modifier = modifier) {
    memoizedAsyncImage()
    // The rest of the code here
}

This should memoize the AsyncImage composable, so it is not recomposed unnecessarily.

Regarding the recomposition issue with the favorite icon, you can try wrapping the IconToggleButton in a rememberUpdatedState function to memoize the checked state of the toggle button, like this:

val checkedState = rememberUpdatedState(isFavourite)
IconToggleButton(
    checked = checkedState.value,
    onCheckedChange = remember { { onFavoriteClick(item) } }
) {
    Icon(
        imageVector = if (checkedState.value) {
            Icons.Filled.Favorite
        } else {
            Icons.Default.FavoriteBorder
        },
        contentDescription = null,
    )
}

This should ensure that the IconToggleButton only recomposes when the checked state changes.