User wishlist management with reactive state and local persistence
The Wishlist Component manages user favorites, allowing users to save products they’re interested in. It provides reactive state management with Flow-based caching for real-time updates.
package com.denisbrandi.androidrealca.wishlist.domain.modelimport com.denisbrandi.androidrealca.money.domain.model.Moneydata class WishlistItem( val id: String, val name: String, val money: Money, val imageUrl: String)
WishlistItem is structurally similar to Product but represents a different concept in the domain. Products come from the catalog, wishlist items are user-specific.
Observes just the wishlist item IDs for efficient lookups.
package com.denisbrandi.androidrealca.wishlist.domain.usecaseimport kotlinx.coroutines.flow.*internal class ObserveUserWishlistIdsUseCase( private val observeUserWishlist: ObserveUserWishlist) : ObserveUserWishlistIds { override fun invoke(): Flow<List<String>> { return observeUserWishlist().map { list -> list.map { item -> item.id } } }}
Why separate ID observation?
Performance: When you only need to check “is this item in the wishlist?”, you don’t need full item dataUI efficiency: Product lists can show heart icons without loading full wishlist detailsComposition: Built on top of ObserveUserWishlist, demonstrating use case composition
Implements the WishlistRepository interface with Flow-based caching.
package com.denisbrandi.androidrealca.wishlist.data.repositoryimport com.denisbrandi.androidrealca.cache.*import com.denisbrandi.androidrealca.money.domain.model.Moneyimport com.denisbrandi.androidrealca.wishlist.data.model.*import com.denisbrandi.androidrealca.wishlist.domain.model.WishlistItemimport com.denisbrandi.androidrealca.wishlist.domain.repository.WishlistRepositoryimport kotlinx.coroutines.flow.*internal class RealWishlistRepository( private val cacheProvider: CacheProvider) : WishlistRepository { private val flowCachedObject: FlowCachedObject<JsonWishlistCacheDto> by lazy { cacheProvider.getFlowCachedObject( fileName = "wishlist-cache", serializer = JsonWishlistCacheDto.serializer(), defaultValue = JsonWishlistCacheDto(emptyMap()) ) } override fun addToWishlist(userId: String, wishlistItem: WishlistItem) { val updatedCache = getUpdatedCacheForUser(userId) { userWishlist -> if (userWishlist.find { it.id == wishlistItem.id } == null) { userWishlist.add(mapToDto(wishlistItem)) } } flowCachedObject.put(updatedCache) } override fun removeFromWishlist(userId: String, wishlistItemId: String) { val updatedCache = getUpdatedCacheForUser(userId) { userWishlist -> userWishlist.find { it.id == wishlistItemId }?.let { itemToRemove -> userWishlist.remove(itemToRemove) } } flowCachedObject.put(updatedCache) } private fun getUpdatedCacheForUser( userId: String, onUserWishlist: (MutableList<JsonWishlistItemCacheDTO>) -> Unit ): JsonWishlistCacheDto { val usersWishlist = flowCachedObject.get().usersWishlist val userWishlist = usersWishlist[userId].orEmpty().toMutableList() return JsonWishlistCacheDto( usersWishlist = usersWishlist.toMutableMap().apply { put( userId, userWishlist.apply { onUserWishlist(this) }.toList() ) } ) } override fun observeWishlist(userId: String): Flow<List<WishlistItem>> { return flowCachedObject.observe().map { cachedDto -> mapToWishlistItems(cachedDto.usersWishlist[userId] ?: emptyList()) } } private fun mapToWishlistItems(dtos: List<JsonWishlistItemCacheDTO>): List<WishlistItem> { return dtos.map { dto -> WishlistItem( id = dto.id, name = dto.name, money = Money(dto.price, dto.currency), imageUrl = dto.imageUrl ) } } private fun mapToDto(wishlistItem: WishlistItem): JsonWishlistItemCacheDTO { return JsonWishlistItemCacheDTO( id = wishlistItem.id, name = wishlistItem.name, price = wishlistItem.money.amount, currency = wishlistItem.money.currencySymbol, imageUrl = wishlistItem.imageUrl ) }}
1
Get current cache
Retrieves the current cached wishlist state
2
Update user's list
Modifies only the specific user’s wishlist (immutable update pattern)
3
Write to cache
Persists the updated cache, triggering Flow emissions
4
Observers notified
All subscribers to observeWishlist() receive the updated list
Idempotent adds: The implementation checks for duplicates before adding. Calling addToWishlist() multiple times with the same item won’t create duplicates.
package com.denisbrandi.androidrealca.wishlist.data.modelimport kotlinx.serialization.*@Serializabledata class JsonWishlistCacheDto( @SerialName("usersWishlist") val usersWishlist: Map<String, List<JsonWishlistItemCacheDTO>>)@Serializabledata class JsonWishlistItemCacheDTO( @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)
The cache structure uses a Map<String, List<...>> to support multiple users, where the key is the user ID.
Here’s how the wishlist component is used in the Product List Page:
// In PLPViewModelinternal interface PLPViewModel : StateViewModel<PLPScreenState> { fun isFavourite(productId: String): Boolean fun addProductToWishlist(product: Product) fun removeProductFromWishlist(productId: String)}internal data class PLPScreenState( val fullName: String, val wishlistIds: List<String> = emptyList(), val displayState: DisplayState? = null)// In ViewModel implementation:class RealPLPViewModel( private val observeUserWishlistIds: ObserveUserWishlistIds, private val addToWishlist: AddToWishlist, private val removeFromWishlist: RemoveFromWishlist) : PLPViewModel { init { // Observe wishlist IDs for showing heart icons observeUserWishlistIds() .onEach { wishlistIds -> updateState { currentState -> currentState.copy(wishlistIds = wishlistIds) } } .launchIn(viewModelScope) } override fun isFavourite(productId: String): Boolean { return state.value.wishlistIds.contains(productId) } override fun addProductToWishlist(product: Product) { val wishlistItem = WishlistItem( id = product.id, name = product.name, money = product.money, imageUrl = product.imageUrl ) addToWishlist(wishlistItem) } override fun removeProductFromWishlist(productId: String) { removeFromWishlist(productId) }}// In Compose UI:@Composablefun ProductCard(product: Product, viewModel: PLPViewModel) { Card { // ... product details ... IconButton( onClick = { if (viewModel.isFavourite(product.id)) { viewModel.removeProductFromWishlist(product.id) } else { viewModel.addProductToWishlist(product) } } ) { Icon( imageVector = if (viewModel.isFavourite(product.id)) { Icons.Filled.Favorite } else { Icons.Outlined.FavoriteBorder }, contentDescription = "Wishlist" ) } }}
Using ObserveUserWishlistIds instead of the full wishlist makes the UI more efficient - you only load IDs for checking favorites, not full item details.