From 91ca4a5dcd5083c9292db8342d7c07a02d5b0efa Mon Sep 17 00:00:00 2001 From: "artyom.romanov" Date: Mon, 19 Jan 2026 12:12:12 +0000 Subject: [PATCH] Add Database for wishlist IDs, and flows to get and set them III --- .../ecomm/data/database/WishlistDatabase.kt | 16 +++++++ .../ecomm/data/database/di/DatabaseModule.kt | 14 ++++++ .../data/database/wishlist/WishlistDao.kt | 21 +++++++++ .../database/wishlist/model/WishlistEntity.kt | 10 ++++ .../data/wishlist/WishlistRepositoryImpl.kt | 47 +++++++++---------- .../component/gallery/EndlessGallery.kt | 14 +++++- .../designsystem/component/gallery/Gallery.kt | 2 + .../component/productcard/ProductCard.kt | 9 ++-- .../productcard/size/ProductCardLarge.kt | 13 +++-- .../productcard/size/ProductCardMedium.kt | 18 +++++-- .../ecomm/repository/result/ErrorType.kt | 3 +- .../repository/wishlist/WishlistRepository.kt | 10 ++-- .../usecase/wishlist/AddToWishlistUseCase.kt | 21 +++++---- .../usecase/wishlist/GetWishlistIdsUseCase.kt | 15 ++++++ .../usecase/wishlist/GetWishlistUseCase.kt | 36 +++++++++++--- .../wishlist/RemoveFromWishlistUseCase.kt | 16 +------ .../ecomm/feature/pdp/ProductDetailsScreen.kt | 3 +- .../feature/pdp/ProductDetailsViewModel.kt | 46 ++++++++++++++++-- .../feature/pdp/model/ProductDetailsEvent.kt | 2 +- .../feature/pdp/model/ProductDetailsUI.kt | 3 +- .../ecomm/feature/plp/ProductListScreen.kt | 3 +- .../ecomm/feature/plp/ProductListViewModel.kt | 30 +++++++++--- .../ecomm/feature/plp/model/ProductListUI.kt | 8 ++-- .../feature/plp/ProductListViewModelTest.kt | 4 +- .../factory/ProductListEntryUIFactoryTest.kt | 6 +-- .../feature/wishlist/WishlistViewModel.kt | 16 ++----- 26 files changed, 275 insertions(+), 111 deletions(-) create mode 100644 data/database/src/main/java/au/com/alfie/ecomm/data/database/WishlistDatabase.kt create mode 100644 data/database/src/main/java/au/com/alfie/ecomm/data/database/wishlist/WishlistDao.kt create mode 100644 data/database/src/main/java/au/com/alfie/ecomm/data/database/wishlist/model/WishlistEntity.kt create mode 100644 domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/GetWishlistIdsUseCase.kt diff --git a/data/database/src/main/java/au/com/alfie/ecomm/data/database/WishlistDatabase.kt b/data/database/src/main/java/au/com/alfie/ecomm/data/database/WishlistDatabase.kt new file mode 100644 index 0000000..d00b632 --- /dev/null +++ b/data/database/src/main/java/au/com/alfie/ecomm/data/database/WishlistDatabase.kt @@ -0,0 +1,16 @@ +package au.com.alfie.ecomm.data.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import au.com.alfie.ecomm.data.database.wishlist.WishlistDao +import au.com.alfie.ecomm.data.database.wishlist.model.WishlistEntity + +@Database( + entities = [WishlistEntity::class], + version = 1, + exportSchema = true +) +internal abstract class WishlistDatabase : RoomDatabase() { + + abstract fun wishlistDao(): WishlistDao +} diff --git a/data/database/src/main/java/au/com/alfie/ecomm/data/database/di/DatabaseModule.kt b/data/database/src/main/java/au/com/alfie/ecomm/data/database/di/DatabaseModule.kt index 8be78e6..8a75ec9 100644 --- a/data/database/src/main/java/au/com/alfie/ecomm/data/database/di/DatabaseModule.kt +++ b/data/database/src/main/java/au/com/alfie/ecomm/data/database/di/DatabaseModule.kt @@ -5,9 +5,11 @@ import androidx.room.Room import au.com.alfie.ecomm.data.database.FeatureToggleDatabase import au.com.alfie.ecomm.data.database.InMemoryDatabase import au.com.alfie.ecomm.data.database.PersistentDatabase +import au.com.alfie.ecomm.data.database.WishlistDatabase import au.com.alfie.ecomm.data.database.navigation.NavigationEntryDao import au.com.alfie.ecomm.data.database.search.FeatureToggleDao import au.com.alfie.ecomm.data.database.search.RecentSearchDao +import au.com.alfie.ecomm.data.database.wishlist.WishlistDao import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -47,6 +49,15 @@ internal object DatabaseModule { name = "feature-toggle-database" ).build() + @Provides + @Singleton + fun provideWishlistDatabase(@ApplicationContext context: Context): WishlistDatabase = + Room.databaseBuilder( + context = context, + klass = WishlistDatabase::class.java, + name = "wishlist-database" + ).build() + @Provides fun provideRecentSearchDao(database: PersistentDatabase): RecentSearchDao = database.recentSearchDao() @@ -55,4 +66,7 @@ internal object DatabaseModule { @Provides fun provideFeatureToggleDao(database: FeatureToggleDatabase): FeatureToggleDao = database.featureToggleDao() + + @Provides + fun provideWishlistDao(database: WishlistDatabase): WishlistDao = database.wishlistDao() } diff --git a/data/database/src/main/java/au/com/alfie/ecomm/data/database/wishlist/WishlistDao.kt b/data/database/src/main/java/au/com/alfie/ecomm/data/database/wishlist/WishlistDao.kt new file mode 100644 index 0000000..055c63f --- /dev/null +++ b/data/database/src/main/java/au/com/alfie/ecomm/data/database/wishlist/WishlistDao.kt @@ -0,0 +1,21 @@ +package au.com.alfie.ecomm.data.database.wishlist + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import au.com.alfie.ecomm.data.database.wishlist.model.WishlistEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface WishlistDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun addToWishlist(product: WishlistEntity) + + @Query("DELETE FROM wishlist WHERE id = :productId") + suspend fun removeFromWishlist(productId: String) + + @Query("SELECT * FROM wishlist") + fun getWishlistIds(): Flow> +} \ No newline at end of file diff --git a/data/database/src/main/java/au/com/alfie/ecomm/data/database/wishlist/model/WishlistEntity.kt b/data/database/src/main/java/au/com/alfie/ecomm/data/database/wishlist/model/WishlistEntity.kt new file mode 100644 index 0000000..5ed0c67 --- /dev/null +++ b/data/database/src/main/java/au/com/alfie/ecomm/data/database/wishlist/model/WishlistEntity.kt @@ -0,0 +1,10 @@ +package au.com.alfie.ecomm.data.database.wishlist.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "wishlist") +data class WishlistEntity( + @PrimaryKey + val id: String +) \ No newline at end of file diff --git a/data/src/main/java/au/com/alfie/ecomm/data/wishlist/WishlistRepositoryImpl.kt b/data/src/main/java/au/com/alfie/ecomm/data/wishlist/WishlistRepositoryImpl.kt index ec1f58d..faa077a 100644 --- a/data/src/main/java/au/com/alfie/ecomm/data/wishlist/WishlistRepositoryImpl.kt +++ b/data/src/main/java/au/com/alfie/ecomm/data/wishlist/WishlistRepositoryImpl.kt @@ -1,40 +1,35 @@ package au.com.alfie.ecomm.data.wishlist +import au.com.alfie.ecomm.data.database.wishlist.WishlistDao +import au.com.alfie.ecomm.data.database.wishlist.model.WishlistEntity import au.com.alfie.ecomm.data.toRepositoryResult -import au.com.alfie.ecomm.repository.product.model.Product -import au.com.alfie.ecomm.repository.result.RepositoryResult import au.com.alfie.ecomm.repository.wishlist.WishlistRepository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map +import timber.log.Timber import javax.inject.Inject -class WishlistRepositoryImpl @Inject constructor() : WishlistRepository { +class WishlistRepositoryImpl @Inject constructor( + private val wishlistDao: WishlistDao +) : WishlistRepository { - // TODO consider removing this property when the wishlist of product is saved on database or api - private val _wishlist = MutableStateFlow>(listOf()) - - // TODO change this implementation to a proper implementation using data base or api to save the products on wishlist - override fun addToWishlist(product: Product): RepositoryResult { - if (_wishlist.value.none { it.id == product.id }) { - _wishlist.value = buildList { - addAll(_wishlist.value) - add(product) - } + override fun getWishlist(): Flow> = + wishlistDao.getWishlistIds().map { wishlist -> + Timber.tag("WishlistTesting").d("Fetching wishlist: $wishlist") + wishlist.map { it.id } } - return RepositoryResult.Success(true) - } - // TODO change this implementation to a proper implementation using data base or api to save the products on wishlist - override fun removeFromWishlist(product: Product): RepositoryResult { - _wishlist.value = _wishlist.value.filter { it.id != product.id }.toMutableList() - return RepositoryResult.Success(true) - } + override suspend fun addToWishlist(productId: String) = + runCatching { + Timber.tag("WishlistTesting").d("Adding to wishlist: $productId") + wishlistDao.addToWishlist(WishlistEntity(productId)) + } + .toRepositoryResult() - // TODO change this implementation to a proper implementation using data base or api to get the wishlist - override fun getWishlist(): Flow>> { - return _wishlist.map { list -> - Result.success(list).toRepositoryResult() + override suspend fun removeFromWishlist(productId: String) = + runCatching { + Timber.tag("WishlistTesting").d("Removing from wishlist: $productId") + wishlistDao.removeFromWishlist(productId) } - } + .toRepositoryResult() } diff --git a/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/gallery/EndlessGallery.kt b/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/gallery/EndlessGallery.kt index d847c6f..ea82567 100644 --- a/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/gallery/EndlessGallery.kt +++ b/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/gallery/EndlessGallery.kt @@ -37,6 +37,7 @@ private const val PAGES_COUNT_MINIMUM = 1 @Composable internal fun EndlessGallery( gallery: GalleryUI, + isWishlisted: Boolean = false, startPosition: Int, isZoomable: Boolean, onPositionChange: (Int) -> Unit, @@ -65,6 +66,7 @@ internal fun EndlessGallery( pagerState = pagerState, itemsCount = itemsCount, gallery = gallery, + isWishlisted = isWishlisted, content = content ) } else { @@ -72,6 +74,7 @@ internal fun EndlessGallery( pagerState = pagerState, itemsCount = itemsCount, gallery = gallery, + isWishlisted = isWishlisted, onFavoriteClick = onFavoriteClick, content = content ) @@ -84,6 +87,7 @@ private fun ZoomableEndlessGallery( pagerState: PagerState, itemsCount: Int, gallery: GalleryUI, + isWishlisted: Boolean = false, content: @Composable EndlessGalleryScope.() -> Unit ) { Box( @@ -94,6 +98,7 @@ private fun ZoomableEndlessGallery( pagerState = pagerState, itemsCount = itemsCount, gallery = gallery, + isWishlisted = isWishlisted, content = content ) if (itemsCount > 1) { @@ -112,6 +117,7 @@ private fun NonZoomableEndlessGallery( pagerState: PagerState, itemsCount: Int, gallery: GalleryUI, + isWishlisted: Boolean = false, onFavoriteClick: ClickEvent, content: @Composable EndlessGalleryScope.() -> Unit ) { @@ -120,6 +126,7 @@ private fun NonZoomableEndlessGallery( pagerState = pagerState, itemsCount = itemsCount, gallery = gallery, + isWishlisted = isWishlisted, onFavoriteClick = onFavoriteClick, content = content ) @@ -138,6 +145,7 @@ private fun ZoomablePager( pagerState: PagerState, itemsCount: Int, gallery: GalleryUI, + isWishlisted: Boolean = false, content: @Composable EndlessGalleryScope.() -> Unit ) { HorizontalPager(state = pagerState) { index -> @@ -165,6 +173,7 @@ private fun NonZoomablePager( pagerState: PagerState, itemsCount: Int, gallery: GalleryUI, + isWishlisted: Boolean = false, onFavoriteClick: ClickEvent, content: @Composable EndlessGalleryScope.() -> Unit ) { @@ -197,8 +206,11 @@ private fun NonZoomablePager( .size(Theme.iconSize.xLarge), onClick = onFavoriteClick ) { + val iconRes = if(isWishlisted) R.drawable.ic_action_heart_fill else + R.drawable.ic_action_heart_outline + Icon( - painter = painterResource(id = R.drawable.ic_action_heart_outline), + painter = painterResource(id = iconRes), contentDescription = null, modifier = Modifier.size(Theme.iconSize.medium) ) diff --git a/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/gallery/Gallery.kt b/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/gallery/Gallery.kt index fa71531..a2f142b 100644 --- a/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/gallery/Gallery.kt +++ b/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/gallery/Gallery.kt @@ -44,6 +44,7 @@ import kotlin.math.roundToInt @Composable fun Gallery( gallery: GalleryUI, + isWishlisted: Boolean = false, ratio: Ratio, constraint: DimensionConstraint, modifier: Modifier = Modifier, @@ -87,6 +88,7 @@ fun Gallery( EndlessGallery( gallery = gallery, + isWishlisted = isWishlisted, startPosition = selectedIndex, isZoomable = false, onPositionChange = onPositionChange, diff --git a/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/productcard/ProductCard.kt b/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/productcard/ProductCard.kt index f0ffcb6..c36dbe4 100644 --- a/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/productcard/ProductCard.kt +++ b/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/productcard/ProductCard.kt @@ -16,7 +16,8 @@ fun ProductCard( productCardType: ProductCardType, onClick: ClickEvent, modifier: Modifier = Modifier, - isLoading: Boolean = false + isLoading: Boolean = false, + isWishlisted: Boolean = false ) { when (productCardType) { is ProductCardType.XSmall -> ProductCardXSmall( @@ -35,13 +36,15 @@ fun ProductCard( productCard = productCardType, onClick = onClick, modifier = modifier, - isLoading = isLoading + isLoading = isLoading, + isWishlisted = isWishlisted ) is ProductCardType.Large -> ProductCardLarge( productCard = productCardType, onClick = onClick, modifier = modifier, - isLoading = isLoading + isLoading = isLoading, + isWishlisted = isWishlisted ) } } diff --git a/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/productcard/size/ProductCardLarge.kt b/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/productcard/size/ProductCardLarge.kt index ced5d6d..ae089e3 100644 --- a/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/productcard/size/ProductCardLarge.kt +++ b/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/productcard/size/ProductCardLarge.kt @@ -40,7 +40,8 @@ internal fun ProductCardLarge( productCard: ProductCardType.Large, onClick: ClickEvent, modifier: Modifier = Modifier, - isLoading: Boolean = false + isLoading: Boolean = false, + isWishlisted: Boolean = false ) { Column( modifier = modifier then Modifier @@ -49,7 +50,8 @@ internal fun ProductCardLarge( ) { ProductImage( productCard = productCard, - isLoading = isLoading + isLoading = isLoading, + isWishlisted = isWishlisted ) Spacer(modifier = Modifier.size(Theme.spacing.spacing16)) ProductDescription( @@ -62,7 +64,8 @@ internal fun ProductCardLarge( @Composable private fun ProductImage( productCard: ProductCardType.Large, - isLoading: Boolean + isLoading: Boolean, + isWishlisted: Boolean ) { Box( contentAlignment = Alignment.TopEnd @@ -80,8 +83,10 @@ private fun ProductImage( modifier = Modifier.size(Theme.iconSize.large), onClick = productCard.onFavoriteClick ) { + val iconRes = + if (isWishlisted) R.drawable.ic_action_heart_fill else R.drawable.ic_action_heart_outline Icon( - painter = painterResource(id = R.drawable.ic_action_heart_outline), + painter = painterResource(iconRes), contentDescription = null, modifier = Modifier.size(Theme.iconSize.medium) ) diff --git a/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/productcard/size/ProductCardMedium.kt b/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/productcard/size/ProductCardMedium.kt index 1db1400..e9779e5 100644 --- a/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/productcard/size/ProductCardMedium.kt +++ b/designsystem/src/main/java/au/com/alfie/ecomm/designsystem/component/productcard/size/ProductCardMedium.kt @@ -41,7 +41,8 @@ internal fun ProductCardMedium( productCard: ProductCardType.Medium, onClick: ClickEvent, modifier: Modifier = Modifier, - isLoading: Boolean = false + isLoading: Boolean = false, + isWishlisted: Boolean = false ) { Column( modifier = modifier then Modifier @@ -50,7 +51,8 @@ internal fun ProductCardMedium( ) { ProductImage( productCard = productCard, - isLoading = isLoading + isLoading = isLoading, + isWishlisted = isWishlisted ) Spacer(modifier = Modifier.size(Theme.spacing.spacing12)) ProductDescription( @@ -63,7 +65,8 @@ internal fun ProductCardMedium( @Composable private fun ProductImage( productCard: ProductCardType.Medium, - isLoading: Boolean + isLoading: Boolean, + isWishlisted: Boolean ) { Box( contentAlignment = Alignment.TopEnd @@ -77,10 +80,13 @@ private fun ProductImage( ratio = Ratio.RATIO3x4 ) if (isLoading.not()) { + val iconRes = + if (isWishlisted) R.drawable.ic_action_heart_fill else R.drawable.ic_action_heart_outline + if (productCard.onFavoriteClick != null) { ActionIconButton( productCard.onFavoriteClick, - R.drawable.ic_action_heart_outline + iconRes ) } else if (productCard.onRemoveClick != null) { ActionIconButton( @@ -186,7 +192,9 @@ private fun ActionIconButton( ) { onClick?.let { IconButton( - modifier = Modifier.padding(8.dp).size(Theme.iconSize.large), + modifier = Modifier + .padding(8.dp) + .size(Theme.iconSize.large), onClick = it ) { Icon( diff --git a/domain/repository/src/main/java/au/com/alfie/ecomm/repository/result/ErrorType.kt b/domain/repository/src/main/java/au/com/alfie/ecomm/repository/result/ErrorType.kt index 3fa625e..0def375 100644 --- a/domain/repository/src/main/java/au/com/alfie/ecomm/repository/result/ErrorType.kt +++ b/domain/repository/src/main/java/au/com/alfie/ecomm/repository/result/ErrorType.kt @@ -10,5 +10,6 @@ enum class ErrorType { INVALID_REQUEST, BAD_REQUEST, UN_PROCESSABLE_ENTITY, - METHOD_NOT_ALLOWED + METHOD_NOT_ALLOWED, + DATABASE_ERROR } diff --git a/domain/repository/src/main/java/au/com/alfie/ecomm/repository/wishlist/WishlistRepository.kt b/domain/repository/src/main/java/au/com/alfie/ecomm/repository/wishlist/WishlistRepository.kt index ca7e98d..46a9e89 100644 --- a/domain/repository/src/main/java/au/com/alfie/ecomm/repository/wishlist/WishlistRepository.kt +++ b/domain/repository/src/main/java/au/com/alfie/ecomm/repository/wishlist/WishlistRepository.kt @@ -1,14 +1,10 @@ package au.com.alfie.ecomm.repository.wishlist -import au.com.alfie.ecomm.repository.product.model.Product import au.com.alfie.ecomm.repository.result.RepositoryResult import kotlinx.coroutines.flow.Flow interface WishlistRepository { - - fun addToWishlist(product: Product): RepositoryResult - - fun removeFromWishlist(product: Product): RepositoryResult - - fun getWishlist(): Flow>> + fun getWishlist(): Flow> + suspend fun addToWishlist(productId: String): RepositoryResult + suspend fun removeFromWishlist(productId: String): RepositoryResult } diff --git a/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/AddToWishlistUseCase.kt b/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/AddToWishlistUseCase.kt index 0297900..5749007 100644 --- a/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/AddToWishlistUseCase.kt +++ b/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/AddToWishlistUseCase.kt @@ -1,21 +1,22 @@ package au.com.alfie.ecomm.domain.usecase.wishlist import au.com.alfie.ecomm.domain.UseCaseInteractor -import au.com.alfie.ecomm.domain.UseCaseResult -import au.com.alfie.ecomm.domain.doOnResult import au.com.alfie.ecomm.repository.product.ProductRepository +import au.com.alfie.ecomm.repository.product.model.Product +import au.com.alfie.ecomm.repository.result.onSuccess import au.com.alfie.ecomm.repository.wishlist.WishlistRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import javax.inject.Inject class AddToWishlistUseCase @Inject constructor( - private val wishlistRepository: WishlistRepository, - private val productRepository: ProductRepository + private val wishlistRepository: WishlistRepository ) : UseCaseInteractor { - suspend operator fun invoke(productId: String) = - productRepository.getProduct(productId = productId) - .doOnResult( - onSuccess = { run(wishlistRepository.addToWishlist(it)) }, - onError = { UseCaseResult.Error(it) } - ) + @OptIn(ExperimentalCoroutinesApi::class) + suspend operator fun invoke(productId: String) = wishlistRepository.addToWishlist(productId) } diff --git a/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/GetWishlistIdsUseCase.kt b/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/GetWishlistIdsUseCase.kt new file mode 100644 index 0000000..4c70a34 --- /dev/null +++ b/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/GetWishlistIdsUseCase.kt @@ -0,0 +1,15 @@ +package au.com.alfie.ecomm.domain.usecase.wishlist + +import au.com.alfie.ecomm.domain.UseCaseInteractor +import au.com.alfie.ecomm.repository.wishlist.WishlistRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class GetWishlistIdsUseCase @Inject constructor( + private val wishlistRepository: WishlistRepository +) : UseCaseInteractor { + + @OptIn(ExperimentalCoroutinesApi::class) + operator fun invoke(): Flow> = wishlistRepository.getWishlist() +} \ No newline at end of file diff --git a/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/GetWishlistUseCase.kt b/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/GetWishlistUseCase.kt index b7715fa..564275a 100644 --- a/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/GetWishlistUseCase.kt +++ b/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/GetWishlistUseCase.kt @@ -1,19 +1,41 @@ package au.com.alfie.ecomm.domain.usecase.wishlist import au.com.alfie.ecomm.domain.UseCaseInteractor -import au.com.alfie.ecomm.domain.UseCaseResult +import au.com.alfie.ecomm.repository.product.ProductRepository import au.com.alfie.ecomm.repository.product.model.Product +import au.com.alfie.ecomm.repository.result.onSuccess import au.com.alfie.ecomm.repository.wishlist.WishlistRepository +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest import javax.inject.Inject class GetWishlistUseCase @Inject constructor( - private val wishlistRepository: WishlistRepository + private val wishlistRepository: WishlistRepository, + private val productRepository: ProductRepository ) : UseCaseInteractor { - operator fun invoke(): Flow>> = - wishlistRepository.getWishlist().map { repositoryResult -> - run(repositoryResult) - } + @OptIn(ExperimentalCoroutinesApi::class) + operator fun invoke(): Flow> = + wishlistRepository + .getWishlist() + .mapLatest { ids -> + coroutineScope { + val deferredProducts: List> = ids.map { id -> + async(Dispatchers.IO) { + var product: Product? = null + productRepository.getProduct(id) + .onSuccess { product = it } + product + } + } + deferredProducts.awaitAll().filterNotNull() + } + } + } \ No newline at end of file diff --git a/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/RemoveFromWishlistUseCase.kt b/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/RemoveFromWishlistUseCase.kt index 2d8ac3d..0b915f8 100644 --- a/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/RemoveFromWishlistUseCase.kt +++ b/domain/src/main/java/au/com/alfie/ecomm/domain/usecase/wishlist/RemoveFromWishlistUseCase.kt @@ -1,24 +1,12 @@ package au.com.alfie.ecomm.domain.usecase.wishlist import au.com.alfie.ecomm.domain.UseCaseInteractor -import au.com.alfie.ecomm.domain.UseCaseResult -import au.com.alfie.ecomm.domain.doOnResult -import au.com.alfie.ecomm.repository.product.ProductRepository import au.com.alfie.ecomm.repository.wishlist.WishlistRepository import javax.inject.Inject class RemoveFromWishlistUseCase @Inject constructor( - private val wishlistRepository: WishlistRepository, - private val productRepository: ProductRepository + private val wishlistRepository: WishlistRepository ) : UseCaseInteractor { suspend operator fun invoke(productId: String) = - productRepository.getProduct(productId = productId) - .doOnResult( - onSuccess = { - run(wishlistRepository.removeFromWishlist(it)) - }, - onError = { - UseCaseResult.Error(it) - } - ) + wishlistRepository.removeFromWishlist(productId) } \ No newline at end of file diff --git a/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/ProductDetailsScreen.kt b/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/ProductDetailsScreen.kt index 091cfb4..0282fbc 100644 --- a/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/ProductDetailsScreen.kt +++ b/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/ProductDetailsScreen.kt @@ -256,13 +256,14 @@ private fun ProductDetailsGallery( Gallery( gallery = state.details.gallery, + isWishlisted = state.details.isWishlisted, ratio = RATIO3x4, constraint = ParentWidth, isLoading = isLoading, isFullscreen = isFullscreen, onClick = { isFullscreen = true }, onDismissFullscreen = { isFullscreen = false }, - onFavoriteClick = { onEvent(ProductDetailsEvent.OnFavoriteClick) } + onFavoriteClick = { onEvent(ProductDetailsEvent.OnFavoriteClick(state.details.id)) } ) } diff --git a/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/ProductDetailsViewModel.kt b/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/ProductDetailsViewModel.kt index 8fa0c23..d18bc05 100644 --- a/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/ProductDetailsViewModel.kt +++ b/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/ProductDetailsViewModel.kt @@ -9,6 +9,9 @@ import au.com.alfie.ecomm.core.navigation.arguments.webview.webViewNavArgs import au.com.alfie.ecomm.domain.doOnResult import au.com.alfie.ecomm.domain.usecase.bag.AddToBagUseCase import au.com.alfie.ecomm.domain.usecase.product.GetProductUseCase +import au.com.alfie.ecomm.domain.usecase.wishlist.AddToWishlistUseCase +import au.com.alfie.ecomm.domain.usecase.wishlist.GetWishlistIdsUseCase +import au.com.alfie.ecomm.domain.usecase.wishlist.RemoveFromWishlistUseCase import au.com.alfie.ecomm.feature.pdp.model.ProductDetailsEvent import au.com.alfie.ecomm.feature.pdp.model.ProductDetailsSectionItem import au.com.alfie.ecomm.feature.pdp.model.ProductDetailsUIState @@ -22,13 +25,18 @@ import au.com.alfie.ecomm.feature.uievent.UIEventEmitterDelegate import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel internal class ProductDetailsViewModel @Inject constructor( private val addToBagUseCase: AddToBagUseCase, private val getProductUseCase: GetProductUseCase, + private val getWishlistIds: GetWishlistIdsUseCase, + private val addToWishlistUseCase: AddToWishlistUseCase, + private val removeWishlistUseCase: RemoveFromWishlistUseCase, private val uiFactory: ProductDetailsUIFactory, savedStateHandle: SavedStateHandle, uiEventEmitterDelegate: UIEventEmitterDelegate @@ -42,6 +50,7 @@ internal class ProductDetailsViewModel @Inject constructor( init { loadDetails() + collectWishlistIds() } fun handleEvent(event: ProductDetailsEvent) { @@ -50,7 +59,7 @@ internal class ProductDetailsViewModel @Inject constructor( ProductDetailsEvent.OnShareClick -> onShareClick() is ProductDetailsEvent.OnColorClick -> onColorSelected(event.index) is ProductDetailsEvent.OnSectionClick -> onSectionClick(event.item) - is ProductDetailsEvent.OnFavoriteClick -> onFavoriteClick() + is ProductDetailsEvent.OnFavoriteClick -> onFavoriteClick(event.productId) is ProductDetailsEvent.OnSizeSelect -> onSizeSelect(event.sizeUI) } } @@ -110,8 +119,39 @@ internal class ProductDetailsViewModel @Inject constructor( } } - private fun onFavoriteClick() { - // TODO + private fun collectWishlistIds() { + viewModelScope.launch { + getWishlistIds().collect { wishlistIds -> + _state.update { state -> + when (state) { + is Loaded -> { + Timber.tag("WishlistTesting") + .d("isWishlisted: ${state.details.isWishlisted}") + state.copy( + details = state.details.copy( + isWishlisted = wishlistIds.contains(state.details.id) + ) + ) + } + + else -> state + } + } + } + } + } + + private fun onFavoriteClick(productId: String) { + viewModelScope.launch { + Timber.tag("WishlistTesting").d("FAv clicked: $productId") + (_state.value as? ProductDetailsUIState.Data)?.details?.let { + if (it.isWishlisted.not()) { + addToWishlistUseCase(productId) + } else { + removeWishlistUseCase(productId) + } + } + } } private fun onSizeSelect(sizeUI: SizeUI) { diff --git a/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/model/ProductDetailsEvent.kt b/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/model/ProductDetailsEvent.kt index ed72b07..03bbb89 100644 --- a/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/model/ProductDetailsEvent.kt +++ b/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/model/ProductDetailsEvent.kt @@ -10,7 +10,7 @@ internal sealed interface ProductDetailsEvent { data class OnSectionClick(val item: ProductDetailsSectionItem) : ProductDetailsEvent - data object OnFavoriteClick : ProductDetailsEvent + data class OnFavoriteClick(val productId: String) : ProductDetailsEvent data class OnSizeSelect(val sizeUI: SizeUI) : ProductDetailsEvent } diff --git a/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/model/ProductDetailsUI.kt b/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/model/ProductDetailsUI.kt index bc64a1a..0fdf55b 100644 --- a/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/model/ProductDetailsUI.kt +++ b/feature/pdp/src/main/java/au/com/alfie/ecomm/feature/pdp/model/ProductDetailsUI.kt @@ -20,5 +20,6 @@ internal data class ProductDetailsUI( val shareInfo: ProductDetailsShareInfo, val gallery: GalleryUI, val sizeSectionUI: SizeSectionUI, - val selectedColorUI: ColorUI? = null + val selectedColorUI: ColorUI? = null, + val isWishlisted: Boolean = false ) diff --git a/feature/plp/src/main/java/au/com/alfie/ecomm/feature/plp/ProductListScreen.kt b/feature/plp/src/main/java/au/com/alfie/ecomm/feature/plp/ProductListScreen.kt index 44ac902..ddd4cb0 100644 --- a/feature/plp/src/main/java/au/com/alfie/ecomm/feature/plp/ProductListScreen.kt +++ b/feature/plp/src/main/java/au/com/alfie/ecomm/feature/plp/ProductListScreen.kt @@ -181,7 +181,8 @@ private fun ProductListGrid( modifier = Modifier.productListEntryPadding( index = index, columnCount = columnCount - ) + ), + isWishlisted = state.wishlistIds.contains(entry.id), ) } } diff --git a/feature/plp/src/main/java/au/com/alfie/ecomm/feature/plp/ProductListViewModel.kt b/feature/plp/src/main/java/au/com/alfie/ecomm/feature/plp/ProductListViewModel.kt index 602cf0b..faefeda 100644 --- a/feature/plp/src/main/java/au/com/alfie/ecomm/feature/plp/ProductListViewModel.kt +++ b/feature/plp/src/main/java/au/com/alfie/ecomm/feature/plp/ProductListViewModel.kt @@ -17,6 +17,9 @@ import au.com.alfie.ecomm.domain.usecase.productlist.GetPaginatedProductListUseC import au.com.alfie.ecomm.domain.usecase.productlist.GetProductListLayoutModeUseCase import au.com.alfie.ecomm.domain.usecase.productlist.UpdateProductListLayoutModeUseCase import au.com.alfie.ecomm.domain.usecase.wishlist.AddToWishlistUseCase +import au.com.alfie.ecomm.domain.usecase.wishlist.GetWishlistIdsUseCase +import au.com.alfie.ecomm.domain.usecase.wishlist.GetWishlistUseCase +import au.com.alfie.ecomm.domain.usecase.wishlist.RemoveFromWishlistUseCase import au.com.alfie.ecomm.feature.plp.factory.ProductListEntryUIFactory import au.com.alfie.ecomm.feature.plp.factory.ProductListUIFactory import au.com.alfie.ecomm.feature.plp.model.ProductListEntryUI @@ -43,7 +46,9 @@ internal class ProductListViewModel @Inject constructor( private val getPaginatedProductList: GetPaginatedProductListUseCase, private val getProductListLayoutMode: GetProductListLayoutModeUseCase, private val updateProductListLayoutMode: UpdateProductListLayoutModeUseCase, - private val addWishlistUseCase: AddToWishlistUseCase, + private val getWishlistIds: GetWishlistIdsUseCase, + private val addToWishlistUseCase: AddToWishlistUseCase, + private val removeWishlistUseCase: RemoveFromWishlistUseCase, private val productListEntryUIFactory: ProductListEntryUIFactory, private val productListUIFactory: ProductListUIFactory, savedStateHandle: SavedStateHandle, @@ -64,7 +69,8 @@ internal class ProductListViewModel @Inject constructor( private val args: ProductListNavArgs = savedStateHandle.navArgs() private val listType = args.type - private val _productPager = MutableStateFlow>(PagingData.empty(initialPagerLoadState)) + private val _productPager = + MutableStateFlow>(PagingData.empty(initialPagerLoadState)) val productPager: Flow> = _productPager private val _state = MutableStateFlow(ProductListUI.EMPTY) @@ -72,13 +78,16 @@ internal class ProductListViewModel @Inject constructor( init { collectPaginatedProductList() + collectWishlistIds() checkLayoutModePreference() } fun handleEvent(event: ProductListEvent) { when (event) { is ProductListEvent.OpenProduct -> navigateToProduct(event.productId) - is ProductListEvent.OpenFilters -> { /* TODO */ } + is ProductListEvent.OpenFilters -> { /* TODO */ + } + is ProductListEvent.ChangeLayoutMode -> changeLayoutMode(event.layoutMode) } } @@ -110,7 +119,7 @@ internal class ProductListViewModel @Inject constructor( productListEntryUIFactory( entry = entry, layoutMode = uiState.layoutMode, - onFavoriteClick = { onFavoriteClick(entry) } + onFavoriteClick = { onFavoriteClick(entry.id) } ) } } @@ -150,9 +159,18 @@ internal class ProductListViewModel @Inject constructor( } } - private fun onFavoriteClick(product: ProductListEntry) { + private fun collectWishlistIds() { + viewModelScope.launch { + getWishlistIds.invoke().collect { + _state.update { oldState -> oldState.copy(wishlistIds = it) } + } + } + } + + private fun onFavoriteClick(productId: String) { viewModelScope.launch { - addWishlistUseCase(product.id) + if (_state.value.wishlistIds.contains(productId).not()) addToWishlistUseCase(productId) + else removeWishlistUseCase(productId) } } } diff --git a/feature/plp/src/main/java/au/com/alfie/ecomm/feature/plp/model/ProductListUI.kt b/feature/plp/src/main/java/au/com/alfie/ecomm/feature/plp/model/ProductListUI.kt index 38f50be..d277c9e 100644 --- a/feature/plp/src/main/java/au/com/alfie/ecomm/feature/plp/model/ProductListUI.kt +++ b/feature/plp/src/main/java/au/com/alfie/ecomm/feature/plp/model/ProductListUI.kt @@ -8,7 +8,8 @@ internal data class ProductListUI( val isLoadingMetadata: Boolean, val layoutMode: ProductListLayoutMode, val compactColumnCount: Int, - val nonCompactColumnCount: Int + val nonCompactColumnCount: Int, + val wishlistIds: List ) { companion object { @@ -17,8 +18,9 @@ internal data class ProductListUI( resultCount = 0, isLoadingMetadata = true, layoutMode = ProductListLayoutMode.GRID, - compactColumnCount = 0, - nonCompactColumnCount = 0 + compactColumnCount = 1, + nonCompactColumnCount = 1, + wishlistIds = emptyList() ) } } diff --git a/feature/plp/src/test/java/au/com/alfie/ecomm/feature/plp/ProductListViewModelTest.kt b/feature/plp/src/test/java/au/com/alfie/ecomm/feature/plp/ProductListViewModelTest.kt index 4bf3796..6ed36b5 100644 --- a/feature/plp/src/test/java/au/com/alfie/ecomm/feature/plp/ProductListViewModelTest.kt +++ b/feature/plp/src/test/java/au/com/alfie/ecomm/feature/plp/ProductListViewModelTest.kt @@ -75,8 +75,8 @@ class ProductListViewModelTest { type = ProductListType.Search("query") ) products.forEachIndexed { index, product -> - coEvery { entryUiFactory(product, ProductListLayoutMode.GRID, any()) } returns productsMediumUI[index] - coEvery { entryUiFactory(product, ProductListLayoutMode.COLUMN, any()) } returns productsLargeUI[index] + coEvery { entryUiFactory(product, ProductListLayoutMode.GRID, any(),) } returns productsMediumUI[index] + coEvery { entryUiFactory(product, ProductListLayoutMode.COLUMN, any(),) } returns productsLargeUI[index] } every { getPaginatedProductListUseCase(any(), any(), any(), any(), any()) } returns Pager( config = pagerConfig, diff --git a/feature/plp/src/test/java/au/com/alfie/ecomm/feature/plp/factory/ProductListEntryUIFactoryTest.kt b/feature/plp/src/test/java/au/com/alfie/ecomm/feature/plp/factory/ProductListEntryUIFactoryTest.kt index caa8d97..e324883 100644 --- a/feature/plp/src/test/java/au/com/alfie/ecomm/feature/plp/factory/ProductListEntryUIFactoryTest.kt +++ b/feature/plp/src/test/java/au/com/alfie/ecomm/feature/plp/factory/ProductListEntryUIFactoryTest.kt @@ -39,7 +39,7 @@ class ProductListEntryUIFactoryTest { val result = uiFactory( entry = product, layoutMode = ProductListLayoutMode.GRID, - onFavoriteClick = { } + onFavoriteClick = { }, ) assertEquals(expected.id, result.id) @@ -56,7 +56,7 @@ class ProductListEntryUIFactoryTest { val result = uiFactory( entry = product, layoutMode = ProductListLayoutMode.GRID, - onFavoriteClick = { } + onFavoriteClick = { }, ) assertIs(result.productCardData) @@ -69,7 +69,7 @@ class ProductListEntryUIFactoryTest { val result = uiFactory( entry = product, layoutMode = ProductListLayoutMode.COLUMN, - onFavoriteClick = { } + onFavoriteClick = { }, ) assertIs(result.productCardData) diff --git a/feature/wishlist/src/main/java/au/com/alfie/ecomm/feature/wishlist/WishlistViewModel.kt b/feature/wishlist/src/main/java/au/com/alfie/ecomm/feature/wishlist/WishlistViewModel.kt index 632f970..cfca84b 100644 --- a/feature/wishlist/src/main/java/au/com/alfie/ecomm/feature/wishlist/WishlistViewModel.kt +++ b/feature/wishlist/src/main/java/au/com/alfie/ecomm/feature/wishlist/WishlistViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import au.com.alfie.ecomm.core.navigation.arguments.wishlist.WishlistNavArgs -import au.com.alfie.ecomm.domain.doOnResult import au.com.alfie.ecomm.domain.usecase.wishlist.GetWishlistUseCase import au.com.alfie.ecomm.domain.usecase.wishlist.RemoveFromWishlistUseCase import au.com.alfie.ecomm.feature.wishlist.WishlistUiState.Data.Loading @@ -37,18 +36,11 @@ class WishlistViewModel @Inject constructor( private suspend fun getWishlistList() { getWishlistUseCase().collectLatest { result -> - result.doOnResult( - onSuccess = { productList -> - val wishlist = wishlistUiFactory( - products = productList, - onRemoveClick = ::onRemoveClicked - ) - _state.value = WishlistUiState.Data.Loaded(wishlist) - }, - onError = { - _state.value = WishlistUiState.Error - } + val wishlist = wishlistUiFactory( + products = result, + onRemoveClick = ::onRemoveClicked ) + _state.value = WishlistUiState.Data.Loaded(wishlist) } }