Chandra Sekhar
Chandra Sekhar

Reputation: 19492

Compose: LazyColumn recomposes all items on single item update

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.

Upvotes: 7

Views: 4131

Answers (4)

Thracian
Thracian

Reputation: 66674

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://stackoverflow.com/a/74700668/5457853

Upvotes: 0

Alexey Nikitin
Alexey Nikitin

Reputation: 664

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.

Upvotes: 1

Ygor Fraz&#227;o
Ygor Fraz&#227;o

Reputation: 143

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...

Upvotes: 0

sitatech
sitatech

Reputation: 1486

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).

Upvotes: 2

Related Questions