Compose: LazyColumn recomposes all items on single item update
Asked Answered
C

4

7

I am trying to show a list of Orders in a list using LazyColumn. Here is the code:

@Composable
private fun MyOrders(
    orders: List<Order>?,
    onClick: (String, OrderStatus) -> Unit
) {
    orders?.let {
        LazyColumn {
            items(
                items = it,
                key = { it.id }
            ) {
                OrderDetails(it, onClick)
            }
        }
    }
}

@Composable
private fun OrderDetails(
    order: Order,
    onClick: (String, OrderStatus) -> Unit
) {
    println("Composing Order Item")
    // Item Code Here
}

Here is the way, I call the composable:

orderVm.fetchOrders()
val state by orderVm.state.collectAsState(OrderState.Empty)

if (state.orders.isNotEmpty()) {
    MyOrders(state.orders) {
        // Handle status change click listener
    }
}

I fetch all my orders and show in the LazyColumn. However, when a single order is updated, the entire LazyColumn gets rrecomposed. Here is my ViewModel looks like:

class OrderViewModel(
    fetchrderUseCase: FetechOrdersUseCase,
    updateStatusUseCase: UpdateorderUseCase
) {

    val state = MutableStateFlow(OrderState.Empty)

    fun fetchOrders() {
        fetchrderUseCase().collect {
            state.value = state.value.copy(orders = it.data)
        }
    }

    fun updateStatus(newStatus: OrderStatus) {
        updateStatusUseCase(newStatus).collect {
            val oldOrders = status.value.orders
            status.value = status.value.copy(orders = finalizeOrders(oldOrders))
        }
    }
}

NOTE: The finalizeOrders() does some list manipulation based on orderId to update one order with the updated one.

This is how my state looks like:

data class OrderState(
    val orders: List<Order> = listOf(),
    val isLoading: Boolean = false,
    val error: String = ""
) {
    companion object {
        val Empty = FetchOrdersState()
    }
}

If I have 10 orders in my DB and I update one's status (let's say 5th item), then OrderDetails gets called for 20 times. Not sure why. Caan I optimize it to make sure only the 5th indexed item will be recomposed and the OrderDetals gets called only with the new order.

Cnidoblast answered 10/7, 2021 at 14:54 Comment(1)
Did you find a way how to avoid all items recomposition in LazyColumn?Ressieressler
I
2

Is the Orderclasss stable? If not it could be the reason why all the items get recomposed:

Compose skips the recomposition of a composable if all the inputs are stable and haven't changed. The comparison uses the equals method

This section in the compose's doc explains what are stable types and how to skip recomposition.

Note: If you scroll a lazy list, all invisible items will be destroyed. That means if you scroll back they will be recreated not recomposed (you can't skip recreation even if the input is stable).

Intermix answered 10/7, 2021 at 18:17 Comment(0)
R
1

Try to use Rebugger tool to understand why your view was recomposed:

@Composable
private fun OrderDetails(
    order: Order,
    onClick: (String, OrderStatus) -> Unit
) {
    Rebugger(mapOf("order" to order, "onClick" to onClick))
    ...
}

The issue was in onClick in my case. Compose decided, that onClick is mutable, because it contains link to mutable object.

Ressieressler answered 18/8, 2023 at 11:44 Comment(0)
J
0

Blockquote If I have 10 orders in my DB and I update one's status (let's say 5th item), then OrderDetails gets called for 20 times. Not sure why. Caan I optimize it to make sure only the 5th indexed item will be recomposed and the OrderDetals gets called only with the new order.

if you are calling orderVm.fetchOrders() in a composable, you are registering a flow collector at every recomposition, and, when they collect, they replace the state value and call another recomposition and so on.

Move the orderVm.fetchOrders() call to the ViewModel init(){} block, so, it will be registered only once.

If i am right, this is messing with the list...

orderVm.fetchOrders()
val state by orderVm.state.collectAsState(OrderState.Empty)

if (state.orders.isNotEmpty()) {
    MyOrders(state.orders) {
        // Handle status change click listener
    }
}

By the way, ideally, collect the flow inside a viewModelScope, so, it will be cancelled when the hosting viewmodel is cleared, avoiding memory leaks.

Hope it helps...

Janeanjaneczka answered 16/4, 2023 at 20:4 Comment(0)
Z
0

This can happen due to using a List instead of SnaphshotStateList, viewModel lambda not being stable or function itself not being stable.

For the ViewModel part you can instead of calling

viewModel.updateStatus or viewMode::updateStatus

you might need to call

val onClick = remember {
    { orderStatus: OrderStatus ->
        viewModel.updateOrderStatus(orderStatus)
    }
}

In this answer explained possible issues and how to solve them.

https://mcmap.net/q/851197/-jetpack-compose-lazy-column-all-items-recomposes-when-a-single-item-update

Zurn answered 21/8, 2023 at 14:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.