Skip to main content
The Cart Component manages shopping cart operations including adding items, updating quantities, and observing cart state. It uses local caching for persistence and reactive flows for real-time updates.

Overview

The cart component provides:
  • Cart state management with reactive updates
  • Local persistence using JSON cache
  • Quantity management for cart items
  • Subtotal calculation based on cart items
  • User-scoped carts supporting multiple users
The cart component depends on the user-component to get the current user ID and the money-component for currency handling.

Domain Models

The cart component defines two core domain models representing the shopping cart structure.

Cart

Represents a user’s shopping cart with business logic for calculations.
package com.denisbrandi.androidrealca.cart.domain.model

import com.denisbrandi.androidrealca.money.domain.model.Money

data class Cart(val cartItems: List<CartItem>) {
    fun getSubtotal(): Money? {
        return if (cartItems.isNotEmpty()) {
            val currency = cartItems[0].money.currencySymbol
            var subtotal = 0.0
            cartItems.forEach {
                subtotal += it.money.amount * it.quantity
            }
            Money(subtotal, currency)
        } else {
            null
        }
    }

    fun getNumberOfItems(): Int {
        return cartItems.sumOf { it.quantity }
    }
}
getSubtotal(): Calculates the total price of all items in the cart by multiplying each item’s price by its quantity. Returns null if cart is empty.getNumberOfItems(): Returns the total count of items, summing up all quantities (e.g., 2 apples + 3 oranges = 5 items).

CartItem

Represents a single product in the shopping cart.
package com.denisbrandi.androidrealca.cart.domain.model

import com.denisbrandi.androidrealca.money.domain.model.Money

data class CartItem(
    val id: String,
    val name: String,
    val money: Money,
    val imageUrl: String,
    val quantity: Int
)
id
String
Unique identifier for the product
name
String
Display name of the product
money
Money
Price information including amount and currency symbol
imageUrl
String
URL to the product image
quantity
Int
Number of units in the cart (0 means remove from cart)

Repository Interface

The repository defines the contract for cart data operations.
package com.denisbrandi.androidrealca.cart.domain.repository

import com.denisbrandi.androidrealca.cart.domain.model.*
import kotlinx.coroutines.flow.Flow

internal interface CartRepository {
    fun updateCartItem(userId: String, cartItem: CartItem)
    fun observeCart(userId: String): Flow<Cart>
    fun getCart(userId: String): Cart
}
Reactive Updates: The observeCart() method returns a Flow<Cart> that emits new cart state whenever items are added, removed, or updated.

Use Cases

The cart component provides three main use cases for cart operations.

AddCartItemUseCase

Adds a product to the cart, handling duplicate items intelligently.
package com.denisbrandi.androidrealca.cart.domain.usecase

import com.denisbrandi.androidrealca.cart.domain.model.CartItem
import com.denisbrandi.androidrealca.cart.domain.repository.CartRepository
import com.denisbrandi.androidrealca.user.domain.usecase.GetUser

internal class AddCartItemUseCase(
    private val getUser: GetUser,
    private val cartRepository: CartRepository,
    private val updateCartItem: UpdateCartItem
) : AddCartItem {
    override fun invoke(cartItem: CartItem) {
        val cartItemInCart = cartRepository.getCart(getUser().id).cartItems.find {
            it.id == cartItem.id
        }
        if (cartItemInCart != null) {
            updateCartItem(cartItemInCart.copy(quantity = cartItemInCart.quantity + cartItem.quantity))
        } else {
            updateCartItem(cartItem)
        }
    }
}
1

Get current user

Retrieves the user ID to ensure cart operations are user-specific
2

Check for existing item

Searches the cart for an item with the same ID
3

Update or add

If item exists, increments the quantity. Otherwise, adds as new item

UpdateCartItemUseCase

Updates a cart item (typically for quantity changes).
package com.denisbrandi.androidrealca.cart.domain.usecase

import com.denisbrandi.androidrealca.cart.domain.model.CartItem
import com.denisbrandi.androidrealca.cart.domain.repository.CartRepository
import com.denisbrandi.androidrealca.user.domain.usecase.GetUser

internal class UpdateCartItemUseCase(
    private val getUser: GetUser,
    private val cartRepository: CartRepository
) : UpdateCartItem {
    override fun invoke(cartItem: CartItem) {
        cartRepository.updateCartItem(getUser().id, cartItem)
    }
}
Set quantity = 0 to remove an item from the cart. The repository implementation handles this automatically.

ObserveUserCartUseCase

Observes cart changes in real-time.
package com.denisbrandi.androidrealca.cart.domain.usecase

import com.denisbrandi.androidrealca.cart.domain.model.Cart
import com.denisbrandi.androidrealca.cart.domain.repository.CartRepository
import com.denisbrandi.androidrealca.user.domain.usecase.GetUser
import kotlinx.coroutines.flow.Flow

internal class ObserveUserCartUseCase(
    private val getUser: GetUser,
    private val cartRepository: CartRepository
) : ObserveUserCart {
    override fun invoke(): Flow<Cart> {
        return cartRepository.observeCart(getUser().id)
    }
}

Use Case Interfaces

All use cases implement functional interfaces:
fun interface UpdateCartItem {
    operator fun invoke(cartItem: CartItem)
}

fun interface ObserveUserCart {
    operator fun invoke(): Flow<Cart>
}

fun interface AddCartItem {
    operator fun invoke(cartItem: CartItem)
}

Data Layer

The data layer implements cart persistence using local JSON caching.

RealCartRepository

Implements the CartRepository interface with caching support.
package com.denisbrandi.androidrealca.cart.data.repository

import com.denisbrandi.androidrealca.cache.*
import com.denisbrandi.androidrealca.cart.data.model.*
import com.denisbrandi.androidrealca.cart.domain.model.*
import com.denisbrandi.androidrealca.cart.domain.repository.CartRepository
import com.denisbrandi.androidrealca.money.domain.model.Money
import kotlinx.coroutines.flow.*

internal class RealCartRepository(
    private val cacheProvider: CacheProvider
) : CartRepository {

    private val flowCachedObject: FlowCachedObject<JsonCartCacheDto> by lazy {
        cacheProvider.getFlowCachedObject(
            fileName = "cart-cache",
            serializer = JsonCartCacheDto.serializer(),
            defaultValue = JsonCartCacheDto(emptyMap())
        )
    }

    override fun updateCartItem(userId: String, cartItem: CartItem) {
        val updatedCache = getUpdatedCacheForUser(userId) { userCart ->
            val cartItemInCache = userCart.find { it.id == cartItem.id }
            val cartItemDto = mapToDto(cartItem)
            if (cartItemInCache != null) {
                if (cartItem.quantity == 0) {
                    userCart.remove(cartItemInCache)
                } else {
                    val index = userCart.indexOf(cartItemInCache)
                    userCart[index] = cartItemDto
                }
            } else {
                userCart.add(cartItemDto)
            }
        }
        flowCachedObject.put(updatedCache)
    }

    override fun observeCart(userId: String): Flow<Cart> {
        return flowCachedObject.observe().map { cachedDto ->
            mapToCart(userId, cachedDto)
        }
    }

    override fun getCart(userId: String): Cart {
        return mapToCart(userId, flowCachedObject.get())
    }
}
FlowCachedObject: Provides reactive caching that emits updates when data changesUser-scoped storage: Stores carts in a map keyed by user ID, supporting multiple usersAutomatic removal: Setting quantity to 0 removes the item from cacheDTO mapping: Converts between domain models and cache DTOs

Cache DTOs

Data transfer objects for JSON serialization.
package com.denisbrandi.androidrealca.cart.data.model

import kotlinx.serialization.*

@Serializable
data class JsonCartCacheDto(
    @SerialName("usersWishlist") val usersCart: Map<String, List<JsonCartItemCacheDto>>
)

@Serializable
data class JsonCartItemCacheDto(
    @SerialName("id") val id: String,
    @SerialName("name") val name: String,
    @SerialName("price") val price: Double,
    @SerialName("currency") val currency: String,
    @SerialName("imageUrl") val imageUrl: String,
    @SerialName("quantity") val quantity: Int
)

Usage Example

Here’s how the cart component is used in the Cart UI layer:
// CartViewModel observes cart state
internal interface CartViewModel : StateViewModel<CartScreenState> {
    fun updateCartItemQuantity(cartItem: CartItem)
}

internal data class CartScreenState(val cart: Cart)

// In ViewModel implementation:
class RealCartViewModel(
    private val observeUserCart: ObserveUserCart,
    private val updateCartItem: UpdateCartItem
) : CartViewModel {
    
    init {
        // Observe cart changes
        observeUserCart()
            .onEach { cart ->
                updateState { CartScreenState(cart) }
            }
            .launchIn(viewModelScope)
    }
    
    override fun updateCartItemQuantity(cartItem: CartItem) {
        updateCartItem(cartItem)
    }
}

Product Component

Learn about product models used in cart items

Money Component

Understand currency handling in the cart

User Component

See how user context is managed

Architecture Overview

Understand the overall architecture

Build docs developers (and LLMs) love