From 851b4dbee4b0ca2452b075c244c0ab8e859fe7b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Wed, 5 Nov 2025 15:58:04 +0100 Subject: [PATCH] Improve loading by doing it lazily --- .../ui/woopos/orders/WooPosOrdersDetails.kt | 20 ++-- .../ui/woopos/orders/WooPosOrdersScreen.kt | 14 +-- .../ui/woopos/orders/WooPosOrdersState.kt | 76 +++++++----- .../ui/woopos/orders/WooPosOrdersViewModel.kt | 111 ++++++++++++------ 4 files changed, 142 insertions(+), 79 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersDetails.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersDetails.kt index fff78d5da0a1..fe9a19707ace 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersDetails.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersDetails.kt @@ -45,7 +45,7 @@ import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTyp @Composable fun WooPosOrderDetails( modifier: Modifier = Modifier, - details: OrderDetailsViewState, + details: OrderDetailsViewState.Computed.Details, onEmailReceiptButtonClicked: (Long) -> Unit ) { Column( @@ -89,7 +89,7 @@ fun WooPosOrderDetails( } @Composable -private fun OrdersHeader(details: OrderDetailsViewState) { +private fun OrdersHeader(details: OrderDetailsViewState.Computed.Details) { Column(modifier = Modifier.fillMaxWidth()) { WooPosText( text = details.dateTime, @@ -113,7 +113,7 @@ private fun OrdersHeader(details: OrderDetailsViewState) { } @Composable -private fun OrdersProducts(lineItems: List) { +private fun OrdersProducts(lineItems: List) { WooPosCard(shadowType = ShadowType.Soft) { Column(Modifier.padding(WooPosSpacing.Medium.value)) { WooPosText( @@ -137,7 +137,7 @@ private fun OrdersProducts(lineItems: List) { @Composable @Suppress("DestructuringDeclarationWithTooManyEntries") -private fun OrderProductItem(row: OrderDetailsViewState.LineItemRow) { +private fun OrderProductItem(row: OrderDetailsViewState.Computed.Details.LineItemRow) { ConstraintLayout( modifier = Modifier .fillMaxWidth() @@ -189,7 +189,7 @@ private fun OrderProductItem(row: OrderDetailsViewState.LineItemRow) { } @Composable -private fun OrdersTotals(details: OrderDetailsViewState) { +private fun OrdersTotals(details: OrderDetailsViewState.Computed.Details) { WooPosCard(shadowType = ShadowType.Soft) { Column(Modifier.padding(WooPosSpacing.Medium.value)) { WooPosText( @@ -348,18 +348,18 @@ private fun DividerWithSpacing() { @WooPosPreview @Composable fun WooPosOrderDetailsPreview() { - val orderDetails = OrderDetailsViewState( + val orderDetails = OrderDetailsViewState.Computed.Details( id = 1L, number = "#014", dateTime = "Aug 28, 2025 at 10:31 AM", customerEmail = "johndoe@mail.com", status = PosOrderStatus(text = "Completed", colorKey = OrderStatusColorKey.COMPLETED), lineItems = listOf( - OrderDetailsViewState.LineItemRow(101, "Cup", "2 x $4.00", "$8.00", null), - OrderDetailsViewState.LineItemRow(102, "Coffee Container", "1 x $10.00", "$10.00", null), - OrderDetailsViewState.LineItemRow(103, "Paper Filter", "1 x $5.00", "$5.00", null) + OrderDetailsViewState.Computed.Details.LineItemRow(101, "Cup", "2 x $4.00", "$8.00", null), + OrderDetailsViewState.Computed.Details.LineItemRow(102, "Coffee Container", "1 x $10.00", "$10.00", null), + OrderDetailsViewState.Computed.Details.LineItemRow(103, "Paper Filter", "1 x $5.00", "$5.00", null) ), - breakdown = OrderDetailsViewState.TotalsBreakdown( + breakdown = OrderDetailsViewState.Computed.Details.TotalsBreakdown( products = "$23.00", discount = "-$5.00", discountCode = "SAVE5", diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt index 693b6bd709c0..1e7a4d84b50d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt @@ -524,8 +524,8 @@ fun WooPosOrdersScreenPreview() { state = WooPosOrdersState.Content( items = WooPosOrdersState.Content.Items.Loaded( items = mapOf( - item1 to details1, - item2 to details2 + item1 to OrderDetailsViewState.Computed(orderId = 1L, details = details1), + item2 to OrderDetailsViewState.Computed(orderId = 2L, details = details2) ) ), pullToRefreshState = WooPosPullToRefreshState.Enabled, @@ -617,18 +617,18 @@ fun WooPosOrdersNothingFoundStatePreview() { private fun sampleOrderDetails( id: Long = 1L, number: String = "#014" -) = OrderDetailsViewState( +) = OrderDetailsViewState.Computed.Details( id = id, number = number, dateTime = "Aug 28, 2025 at 10:31 AM", customerEmail = "johndoe@mail.com", status = PosOrderStatus(text = "Completed", colorKey = OrderStatusColorKey.COMPLETED), lineItems = listOf( - OrderDetailsViewState.LineItemRow(101, "Cup", "1 x $8.50", "$15.00", null), - OrderDetailsViewState.LineItemRow(102, "Coffee Container", "1 x $10.00", "$8.00", null), - OrderDetailsViewState.LineItemRow(103, "Paper Filter", "1 x $4.50", "$8.00", null) + OrderDetailsViewState.Computed.Details.LineItemRow(101, "Cup", "1 x $8.50", "$15.00", null), + OrderDetailsViewState.Computed.Details.LineItemRow(102, "Coffee Container", "1 x $10.00", "$8.00", null), + OrderDetailsViewState.Computed.Details.LineItemRow(103, "Paper Filter", "1 x $4.50", "$8.00", null) ), - breakdown = OrderDetailsViewState.TotalsBreakdown( + breakdown = OrderDetailsViewState.Computed.Details.TotalsBreakdown( products = "$23.00", discount = "-$5.00", discountCode = "8qew4mnq", diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersState.kt index c28acdc9ba57..0b831df237fa 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersState.kt @@ -1,44 +1,62 @@ package com.woocommerce.android.ui.woopos.orders import androidx.compose.runtime.Immutable +import com.woocommerce.android.model.Order import com.woocommerce.android.model.Order.Status import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearchInputState import com.woocommerce.android.ui.woopos.home.items.WooPosPaginationState import com.woocommerce.android.ui.woopos.home.items.WooPosPullToRefreshState @Immutable -data class OrderDetailsViewState( - val id: Long, - val number: String, - val dateTime: String, - val customerEmail: String?, - val status: PosOrderStatus, +sealed class OrderDetailsViewState { + abstract val orderId: Long - val lineItems: List, - val breakdown: TotalsBreakdown, - val total: String, - val totalPaid: String, - val paymentMethodTitle: String?, -) { @Immutable - data class LineItemRow( - val id: Long, - val name: String, - val qtyAndUnitPrice: String, - val lineTotal: String, - val imageUrl: String?, - ) + data class Lazy( + override val orderId: Long, + val order: Order + ) : OrderDetailsViewState() @Immutable - data class TotalsBreakdown( - val products: String, - val discount: String?, - val discountCode: String?, - val taxes: String, - val shipping: String?, - val refunds: List, - val netPayment: String? - ) + data class Computed( + override val orderId: Long, + val details: Details + ) : OrderDetailsViewState() { + @Immutable + data class Details( + val id: Long, + val number: String, + val dateTime: String, + val customerEmail: String?, + val status: PosOrderStatus, + + val lineItems: List, + val breakdown: TotalsBreakdown, + val total: String, + val totalPaid: String, + val paymentMethodTitle: String?, + ) { + @Immutable + data class LineItemRow( + val id: Long, + val name: String, + val qtyAndUnitPrice: String, + val lineTotal: String, + val imageUrl: String?, + ) + + @Immutable + data class TotalsBreakdown( + val products: String, + val discount: String?, + val discountCode: String?, + val taxes: String, + val shipping: String?, + val refunds: List, + val netPayment: String? + ) + } + } } @Immutable @@ -64,7 +82,7 @@ sealed class WooPosOrdersState { val items: Items, override val pullToRefreshState: WooPosPullToRefreshState, override val searchInputState: WooPosSearchInputState, - val selectedDetails: OrderDetailsViewState, + val selectedDetails: OrderDetailsViewState.Computed.Details, val paginationState: WooPosPaginationState ) : WooPosOrdersState() { sealed class Items { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModel.kt index ed5bacef37f1..94869534319a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModel.kt @@ -18,6 +18,9 @@ import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import com.woocommerce.android.viewmodel.ResourceProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -94,18 +97,26 @@ class WooPosOrdersViewModel @Inject constructor( } } - val updatedItems = loadedItems.items.mapKeys { (item, _) -> - item.copy(isSelected = item.id == orderId) - } + viewModelScope.launch { + val details = getOrComputeDetails(orderId) - val selectedEntry = updatedItems.entries.first { (item, _) -> item.isSelected } + val updatedItems = loadedItems.items.mapKeys { (item, _) -> + item.copy(isSelected = item.id == orderId) + }.mapValues { (item, orderDetails) -> + if (item.id == orderId && orderDetails is OrderDetailsViewState.Lazy) { + OrderDetailsViewState.Computed(orderId = orderId, details = details) + } else { + orderDetails + } + } - _state.value = current.copy( - items = WooPosOrdersState.Content.Items.Loaded( - items = updatedItems - ), - selectedDetails = selectedEntry.value - ) + _state.value = current.copy( + items = WooPosOrdersState.Content.Items.Loaded( + items = updatedItems + ), + selectedDetails = details + ) + } } fun onRefresh() { @@ -282,7 +293,11 @@ class WooPosOrdersViewModel @Inject constructor( val selectedId = loaded.items.keys.firstOrNull { it.isSelected }?.id val newItem = mapOrderItem(updated, selectedId) - val newDetails = mapOrderDetails(updated) + val newDetailsViewState = mapOrderDetails(updated) + val newDetails = OrderDetailsViewState.Computed( + orderId = updated.id, + details = newDetailsViewState + ) val newMap = loaded.items.entries.associate { (item, details) -> if (item.id == updated.id) newItem to newDetails else item to details @@ -290,7 +305,7 @@ class WooPosOrdersViewModel @Inject constructor( _state.value = current.copy( items = WooPosOrdersState.Content.Items.Loaded(newMap), - selectedDetails = if (selectedId == updated.id) newDetails else current.selectedDetails + selectedDetails = if (selectedId == updated.id) newDetailsViewState else current.selectedDetails ) } @@ -406,6 +421,19 @@ class WooPosOrdersViewModel @Inject constructor( loadingMoreOrdersJob?.cancel() } + private suspend fun getOrComputeDetails(orderId: Long): OrderDetailsViewState.Computed.Details { + val current = _state.value as? WooPosOrdersState.Content ?: error("State is not Content") + val loadedItems = current.items as? WooPosOrdersState.Content.Items.Loaded ?: error("Items not loaded") + + val orderDetails = loadedItems.items.values.firstOrNull { it.orderId == orderId } + ?: error("Order $orderId not found in state") + + return when (orderDetails) { + is OrderDetailsViewState.Lazy -> mapOrderDetails(orderDetails.order) + is OrderDetailsViewState.Computed -> orderDetails.details + } + } + private suspend fun replaceOrders( orders: List, paginationState: WooPosPaginationState = WooPosPaginationState.None @@ -413,13 +441,17 @@ class WooPosOrdersViewModel @Inject constructor( val newSelectedId = requireNotNull(orders.firstOrNull()?.id) { "Content requires at least one order" } val items = buildItemsMap(orders, newSelectedId) val selectedEntry = items.entries.first { (item, _) -> item.isSelected } + val selectedDetails = when (val details = selectedEntry.value) { + is OrderDetailsViewState.Computed -> details.details + is OrderDetailsViewState.Lazy -> error("Selected order should have computed details") + } _state.value = WooPosOrdersState.Content( items = WooPosOrdersState.Content.Items.Loaded( items = items ), pullToRefreshState = WooPosPullToRefreshState.Enabled, - selectedDetails = selectedEntry.value, + selectedDetails = selectedDetails, paginationState = paginationState, searchInputState = _state.value.searchInputState ) @@ -449,12 +481,20 @@ class WooPosOrdersViewModel @Inject constructor( private suspend fun buildItemsMap( orders: List, selectedId: Long? - ): Map { - return orders.associate { order -> - val item = mapOrderItem(order, selectedId) - val details = mapOrderDetails(order) - item to details - } + ): Map = coroutineScope { + orders.map { order -> + async { + val item = mapOrderItem(order, selectedId) + val details: OrderDetailsViewState = if (order.id == selectedId) { + val fullDetails = mapOrderDetails(order) + OrderDetailsViewState.Computed(orderId = order.id, details = fullDetails) + } else { + OrderDetailsViewState.Lazy(orderId = order.id, order = order) + } + + item to details + } + }.awaitAll().toMap() } private suspend fun mapOrderItem(order: Order, selectedId: Long?): OrderItemViewState { @@ -478,7 +518,7 @@ class WooPosOrdersViewModel @Inject constructor( ) } - private suspend fun mapOrderDetails(order: Order): OrderDetailsViewState { + private suspend fun mapOrderDetails(order: Order): OrderDetailsViewState.Computed.Details = coroutineScope { val statusText = order.status.localizedLabel(resourceProvider, locale) val status = PosOrderStatus( @@ -486,21 +526,26 @@ class WooPosOrdersViewModel @Inject constructor( colorKey = OrderStatusColorKey.fromStatus(order.status) ) - val lineItems = order.items.map { item -> - val unitPrice = if (item.quantity == 0f) item.total else item.total / item.quantity.toBigDecimal() - val product = getProductById(item.productId) - OrderDetailsViewState.LineItemRow( - id = item.itemId, - name = item.name, - qtyAndUnitPrice = "${item.quantity.toInt()} x ${formatPrice(unitPrice)}", - lineTotal = formatPrice(item.total), - imageUrl = product?.firstImageUrl - ) + val lineItemsDeferred = order.items.map { item -> + async { + val unitPrice = if (item.quantity == 0f) item.total else item.total / item.quantity.toBigDecimal() + val product = getProductById(item.productId) + OrderDetailsViewState.Computed.Details.LineItemRow( + id = item.itemId, + name = item.name, + qtyAndUnitPrice = "${item.quantity.toInt()} x ${formatPrice(unitPrice)}", + lineTotal = formatPrice(item.total), + imageUrl = product?.firstImageUrl + ) + } } val discountCode = order.couponLines.firstOrNull()?.code - val refunds = getOrderRefunds(order.id) + val refundsDeferred = async { getOrderRefunds(order.id) } + + val lineItems = lineItemsDeferred.awaitAll() + val refunds = refundsDeferred.await() val refundAmounts = refunds.map { "-${formatPrice(it.amount)}" } val totalRefunded = refunds.sumOf { it.amount } val netPayment = if (totalRefunded > BigDecimal.ZERO) { @@ -509,7 +554,7 @@ class WooPosOrdersViewModel @Inject constructor( null } - val breakdown = OrderDetailsViewState.TotalsBreakdown( + val breakdown = OrderDetailsViewState.Computed.Details.TotalsBreakdown( products = formatPrice(order.productsTotal), discount = order.discountTotal.takeIf { it != BigDecimal.ZERO }?.let { "-${formatPrice(it)}" }, discountCode = discountCode, @@ -519,7 +564,7 @@ class WooPosOrdersViewModel @Inject constructor( netPayment = netPayment ) - return OrderDetailsViewState( + OrderDetailsViewState.Computed.Details( id = order.id, number = "#${order.number}", dateTime = order.dateCreated.formatToMMMddYYYYAtHHmm(