diff --git a/app/build.gradle b/app/build.gradle index b586a52a..2e848cf2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,7 +13,7 @@ plugins { apply plugin: 'kotlin-android-extensions' android { - compileSdkVersion 33 + compileSdk 34 buildToolsVersion "31.0.0" defaultConfig { @@ -88,6 +88,8 @@ dependencies { implementation libs.gson implementation libs.moshi implementation libs.kotlin.coroutines.play.services + implementation(libs.kotlinx.immutable) + implementation(libs.glide.compose) implementation platform(libs.firebase.bom) implementation libs.bundles.firebase @@ -101,7 +103,7 @@ dependencies { implementation "io.ktor:ktor-client-logging-jvm:1.6.0" implementation("io.ktor:ktor-client-core:2.2.4") - def composeBom = platform('androidx.compose:compose-bom:2023.05.00') + def composeBom = platform('androidx.compose:compose-bom:2023.09.02') implementation(composeBom) androidTestImplementation(composeBom) diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/AddToCartUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/AddToCartUseCase.kt index f75d54e3..31845ba1 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/AddToCartUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/AddToCartUseCase.kt @@ -2,8 +2,8 @@ package com.hieuwu.groceriesstore.domain.usecases import com.hieuwu.groceriesstore.data.database.entities.LineItem -interface AddToCartUseCase: UseCase { +interface AddToCartUseCase: SuspendUseCase { class Input (val lineItem: LineItem) class Output -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/CreateNewOrderUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/CreateNewOrderUseCase.kt index b947453c..34b3cde4 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/CreateNewOrderUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/CreateNewOrderUseCase.kt @@ -3,8 +3,8 @@ package com.hieuwu.groceriesstore.domain.usecases import com.hieuwu.groceriesstore.data.database.entities.Order interface CreateNewOrderUseCase : - UseCase { + SuspendUseCase { class Input(val order: Order) class Output(result: Unit) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetCategoriesListUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetCategoriesListUseCase.kt index b9d53cf4..1dc11e76 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetCategoriesListUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetCategoriesListUseCase.kt @@ -4,8 +4,8 @@ import com.hieuwu.groceriesstore.domain.models.CategoryModel import kotlinx.coroutines.flow.Flow interface GetCategoriesListUseCase : - UseCase { + SuspendUseCase { class Input class Output(val result: Flow>) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetCurrentCartUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetCurrentCartUseCase.kt index 20b8216d..022aa80e 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetCurrentCartUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetCurrentCartUseCase.kt @@ -4,7 +4,7 @@ import com.hieuwu.groceriesstore.domain.models.OrderModel import kotlinx.coroutines.flow.Flow interface GetCurrentCartUseCase : - UseCase { + SuspendUseCase { class Input data class Output(val result: Flow) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetOrderListUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetOrderListUseCase.kt index bf8adc3e..234e241a 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetOrderListUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetOrderListUseCase.kt @@ -1,12 +1,11 @@ package com.hieuwu.groceriesstore.domain.usecases import com.hieuwu.groceriesstore.domain.models.OrderModel -import kotlinx.coroutines.flow.Flow -interface GetOrderListUseCase : UseCase { +interface GetOrderListUseCase : SuspendUseCase { class Input sealed class Output { class Success(val data: List) : Output() object Failure : Output() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetProductsListUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetProductsListUseCase.kt index ec2c526c..c7ec6042 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetProductsListUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetProductsListUseCase.kt @@ -6,4 +6,4 @@ import kotlinx.coroutines.flow.Flow interface GetProductsListUseCase: UseCase { class Input class Output(val result: Flow>) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetProfileUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetProfileUseCase.kt index 9740ebfe..88874121 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetProfileUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/GetProfileUseCase.kt @@ -3,7 +3,7 @@ package com.hieuwu.groceriesstore.domain.usecases import com.hieuwu.groceriesstore.domain.models.UserModel import kotlinx.coroutines.flow.Flow -interface GetProfileUseCase:UseCase { +interface GetProfileUseCase:SuspendUseCase { class Input open class Output(val result: Flow) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/RefreshAppDataUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/RefreshAppDataUseCase.kt index aaa3cd22..9ef214dc 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/RefreshAppDataUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/RefreshAppDataUseCase.kt @@ -1,3 +1,3 @@ package com.hieuwu.groceriesstore.domain.usecases -interface RefreshAppDataUseCase : UseCase +interface RefreshAppDataUseCase : SuspendUseCase diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SearchProductUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SearchProductUseCase.kt index 7b693642..a6c7e108 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SearchProductUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SearchProductUseCase.kt @@ -3,7 +3,7 @@ package com.hieuwu.groceriesstore.domain.usecases import com.hieuwu.groceriesstore.domain.models.ProductModel import kotlinx.coroutines.flow.Flow -interface SearchProductUseCase : UseCase { +interface SearchProductUseCase : SuspendUseCase { class Input(val name: String? = null) class Output(val result: Flow>) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SignInUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SignInUseCase.kt index a1fb4fce..fddfcec8 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SignInUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SignInUseCase.kt @@ -1,9 +1,9 @@ package com.hieuwu.groceriesstore.domain.usecases -interface SignInUseCase : UseCase { +interface SignInUseCase : SuspendUseCase { data class Input(val email: String, val password: String) open class Output(val result: Boolean) { sealed class Error : Output(false) object AccountNotExistedError : Output(false) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SignOutUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SignOutUseCase.kt index cc353237..b925ec76 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SignOutUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SignOutUseCase.kt @@ -1,6 +1,6 @@ package com.hieuwu.groceriesstore.domain.usecases -interface SignOutUseCase : UseCase { +interface SignOutUseCase : SuspendUseCase { class Input class Output -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SubmitOrderUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SubmitOrderUseCase.kt index 48143f2c..1375b004 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SubmitOrderUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/SubmitOrderUseCase.kt @@ -2,7 +2,7 @@ package com.hieuwu.groceriesstore.domain.usecases import com.hieuwu.groceriesstore.domain.models.OrderModel -interface SubmitOrderUseCase : UseCase { +interface SubmitOrderUseCase : SuspendUseCase { class Input(val order: OrderModel) data class Output(val result: Boolean) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/UpdateProfileUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/UpdateProfileUseCase.kt index 61b9458c..331dcd44 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/UpdateProfileUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/UpdateProfileUseCase.kt @@ -1,6 +1,6 @@ package com.hieuwu.groceriesstore.domain.usecases -interface UpdateProfileUseCase : UseCase { +interface UpdateProfileUseCase : SuspendUseCase { data class Input( val userId: String, val name: String, @@ -9,4 +9,4 @@ interface UpdateProfileUseCase : UseCase { +interface SuspendUseCase { suspend fun execute(input: Input): Output -} \ No newline at end of file +} + +interface UseCase { + fun execute(input: Input): Output +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/UserSettingsUseCase.kt b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/UserSettingsUseCase.kt index 40d279ec..bc24ebc8 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/UserSettingsUseCase.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/domain/usecases/UserSettingsUseCase.kt @@ -1,6 +1,6 @@ package com.hieuwu.groceriesstore.domain.usecases -interface UserSettingsUseCase : UseCase { +interface UserSettingsUseCase : SuspendUseCase { class Input( val id: String, val isOrderCreatedEnabled: Boolean, @@ -9,4 +9,4 @@ interface UserSettingsUseCase : UseCase Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + shape: Shape = IconButtonDefaults.filledShape, + content: @Composable () -> Unit +) { + FilledIconButton( + onClick = onClick, + modifier = modifier.defaultMinSize(minWidth = 48.dp, minHeight = 48.dp), + enabled = enabled, + shape = shape, + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = colorResource(id = R.color.colorPrimary), + disabledContainerColor = colorResource(id = R.color.light_gray), + contentColor = Color.White, + ), + content = content + ) +} + +@Composable +fun PrimarySquareIconButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + content: @Composable () -> Unit +) = PrimaryIconButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + shape = RoundedCornerShape(8.dp), + content = content +) + +@Preview +@Composable +private fun PrimaryIconButtonPreview() { + PrimaryIconButton(onClick = {}) { + Icon(imageVector = Icons.Default.Search, contentDescription = "") + } +} + +@Preview +@Composable +private fun PrimarySquareIconButtonPreview() { + PrimarySquareIconButton(onClick = {}) { + Icon(imageVector = Icons.Default.Search, contentDescription = "") + } +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/core/widgets/WebImage.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/core/widgets/WebImage.kt new file mode 100644 index 00000000..ac8b4a44 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/core/widgets/WebImage.kt @@ -0,0 +1,28 @@ +package com.hieuwu.groceriesstore.presentation.core.widgets + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.bumptech.glide.integration.compose.placeholder +import com.hieuwu.groceriesstore.R + +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +fun WebImage( + model: Any?, + contentDescription: String?, + modifier: Modifier = Modifier, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, +) = GlideImage( + model = model, + contentDescription = contentDescription, + modifier = modifier, + alignment = alignment, + contentScale = contentScale, + loading = placeholder(R.drawable.loading_animation), + failure = placeholder(R.drawable.ic_broken_image) +) diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListFragment.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListFragment.kt index 63c891d0..6857223b 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListFragment.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListFragment.kt @@ -4,23 +4,18 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.databinding.DataBindingUtil +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController -import com.hieuwu.groceriesstore.R -import com.hieuwu.groceriesstore.databinding.FragmentProductListBinding -import com.hieuwu.groceriesstore.presentation.adapters.GridListItemAdapter -import com.hieuwu.groceriesstore.utilities.showMessageSnackBar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @AndroidEntryPoint class ProductListFragment : Fragment() { - lateinit var binding: FragmentProductListBinding private val viewModel: ProductListViewModel by viewModels() override fun onCreateView( @@ -28,68 +23,25 @@ class ProductListFragment : Fragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = DataBindingUtil.inflate( - inflater, R.layout.fragment_product_list, container, false - ) - - val args = ProductListFragmentArgs.fromBundle( - arguments as Bundle - ) - - val categoryName = args.categoryName - binding.toolbar.title = categoryName - - binding.viewModel = viewModel - binding.lifecycleOwner = this - - setUpRecyclerView() - setObserver() - setEventListener() - - return binding.root - } - - private fun setEventListener() { - binding.toolbar.setNavigationOnClickListener { - findNavController().navigateUp() - } - - binding.toolbar.setOnMenuItemClickListener { item -> - when (item.itemId) { - R.id.action_filter -> { - showFilterDialog() - true - } - else -> false + return ComposeView(requireContext()).apply { + setContent { + ProductListScreen( + navigateUp = { findNavController().navigateUp() }, + showFilter = ::showFilterDialog, + openProductDetails = ::navigateToProductDetail, + viewModel = viewModel + ) } + setObserver() } } private fun setObserver() { viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.navigateToSelectedProperty.collect { - if (null != it) { - navigateToProductDetail(it.id) - viewModel.displayProductDetailComplete() - } - } - } launch { viewModel.currentCart.collect {} } - launch { - viewModel.productList.collect { - if (it.isEmpty()) { - binding.productRecyclerview.visibility = View.GONE - binding.emptyLayout.visibility = View.VISIBLE - } else { - binding.productRecyclerview.visibility = View.VISIBLE - binding.emptyLayout.visibility = View.GONE - } - } - } } } } @@ -107,19 +59,4 @@ class ProductListFragment : Fragment() { bottomSheetDialogFragment.tag ) } - - private fun setUpRecyclerView() { - binding.productRecyclerview.adapter = - GridListItemAdapter( - GridListItemAdapter.OnClickListener( - clickListener = { - viewModel.displayProductDetail(it) - }, - addToCartListener = { - viewModel.addToCart(it) - showMessageSnackBar("Added ${it.name}") - } - ) - ) - } } diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListScreen.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListScreen.kt new file mode 100644 index 00000000..4aef43bd --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListScreen.kt @@ -0,0 +1,339 @@ +package com.hieuwu.groceriesstore.presentation.productlist + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieComposition +import com.hieuwu.groceriesstore.R +import com.hieuwu.groceriesstore.domain.models.ProductModel +import com.hieuwu.groceriesstore.presentation.core.widgets.PrimarySquareIconButton +import com.hieuwu.groceriesstore.presentation.core.widgets.WebImage +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.launch + +@Composable +fun ProductListScreen( + navigateUp: () -> Unit, + showFilter: () -> Unit, + openProductDetails: (String) -> Unit, + viewModel: ProductListViewModel = hiltViewModel() +) { + val viewState by viewModel.state.collectAsState() + ProductListScreen( + state = viewState, + navigateUp = navigateUp, + showFilter = showFilter, + openProductDetails = openProductDetails, + addToCart = viewModel::addToCart + ) +} + +@Composable +private fun ProductListScreen( + state: ProductListViewState, + navigateUp: () -> Unit, + showFilter: () -> Unit, + openProductDetails: (String) -> Unit, + addToCart: (ProductModel) -> Unit, +) { + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + Scaffold( + topBar = { + ProductListAppBar( + title = state.categoryTitle, + navigateUp = navigateUp, + showFilter = showFilter + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + modifier = Modifier.fillMaxSize() + ) { paddingValues -> + if (state.productList.isEmpty()) { + EmptyList( + modifier = Modifier.padding(paddingValues) + ) + } else { + ProductList( + products = state.productList, + openProductDetails = openProductDetails, + addToCart = { + addToCart(it) + scope.launch { + snackbarHostState.showSnackbar( + message = "Added ${it.name}", + duration = SnackbarDuration.Long + ) + } + }, + contentPadding = paddingValues + ) + } + } +} + +@Composable +private fun EmptyList( + modifier: Modifier = Modifier +) { + val composition by rememberLottieComposition( + spec = LottieCompositionSpec.Asset("list_empty.json") + ) + + LottieAnimation( + modifier = modifier.size(200.dp), + composition = composition, + iterations = LottieConstants.IterateForever, + ) +} + +@Composable +private fun ProductList( + products: ImmutableList, + openProductDetails: (String) -> Unit, + addToCart: (ProductModel) -> Unit, + contentPadding: PaddingValues, + modifier: Modifier = Modifier +) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + state = rememberLazyGridState(), + contentPadding = contentPadding, + modifier = modifier.fillMaxSize() + ) { + items( + items = products, + key = { it.id } + ) { + Product( + productModel = it, + openDetails = { openProductDetails(it.id) }, + addToCart = { addToCart(it) } + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun Product( + productModel: ProductModel, + openDetails: () -> Unit, + addToCart: () -> Unit, + modifier: Modifier = Modifier +) { + Card( + onClick = openDetails, + modifier = modifier.padding(12.dp), + shape = RoundedCornerShape(4.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.background + ) + ) { + Column( + modifier = Modifier + .border(0.7.dp, Color(0xffaaaaaa), RoundedCornerShape(12.dp)) + .padding(12.dp) + ) { + WebImage( + model = productModel.image, + contentDescription = productModel.name, + modifier = Modifier.size(150.dp), + contentScale = ContentScale.FillBounds + ) + Text( + text = productModel.name.orEmpty(), + modifier = Modifier.fillMaxWidth(), + color = Color.Black, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + Text( + text = productModel.description.orEmpty(), + modifier = Modifier.fillMaxWidth(), + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "${productModel.price} $", + modifier = Modifier.weight(1f), + color = colorResource(id = R.color.colorPrimary), + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + PrimarySquareIconButton(onClick = addToCart) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = stringResource(id = R.string.add_to_basket), + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ProductListAppBar( + title: String, + navigateUp: () -> Unit, + showFilter: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + title = { Text(text = title, color = Color.White) }, + modifier = modifier, + navigationIcon = { + IconButton(onClick = navigateUp) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = stringResource(id = R.string.cd_navigate_back) + ) + } + }, + actions = { + IconButton( + onClick = showFilter, + modifier = Modifier.align(Alignment.CenterVertically) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_filter_list_36), + contentDescription = stringResource(id = R.string.cd_save), + tint = Color.White + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = colorResource(id = R.color.colorPrimary), + navigationIconContentColor = Color.White, + titleContentColor = Color.White + ) + ) +} + +@Preview +@Composable +private fun ProductListAppBarPreview() { + ProductListAppBar(title = "Delivery", navigateUp = {}, showFilter = {}) +} + +@Preview +@Composable +private fun ProductPreview() { + Product( + productModel = ProductModel( + id = "TcF68MpGmIGiq5LOvF7u", + name = "Chili, Local Grass-Fed Beef", + price = 5.8, + image = "https://firebasestorage.googleapis.com/v0/b/groceries-store-ad0eb.appspot.com/o/Vegetables%2Fcsm_peppers-plp-desktop_a9c6971df1.jpg?alt=media&token=f986caa0-d65c-4c76-8186-ff75abd6db1e", + description = "Chili doesn't get much meatier than this; this is the kind you dream about for game days and crisp winter nights.", + nutrition = "Chili peppers (Capsicum annuum) are the fruits of Capsicum pepper plants, notable for their hot flavor. They are members of the nightshade family, related to bell peppers and tomatoes. Many varieties of chili peppers exist, such as cayenne and jalapeño. Chili peppers are primarily used as a spice and can be cooked or dried and powdered. Powdered, red chili peppers are known as paprika." + ), + openDetails = {}, + addToCart = {}) +} + +@Preview +@Composable +private fun ProductListPreview() { + Surface( + modifier = Modifier.height(400.dp) + ) { + ProductList( + products = persistentListOf( + ProductModel( + id = "TcF68MpGmIGiq5LOvF7u", + name = "Chili, Local Grass-Fed Beef", + price = 5.8, + image = "https://firebasestorage.googleapis.com/v0/b/groceries-store-ad0eb.appspot.com/o/Vegetables%2Fcsm_peppers-plp-desktop_a9c6971df1.jpg?alt=media&token=f986caa0-d65c-4c76-8186-ff75abd6db1e", + description = "Chili doesn't get much meatier than this; this is the kind you dream about for game days and crisp winter nights.", + nutrition = "Chili peppers (Capsicum annuum) are the fruits of Capsicum pepper plants, notable for their hot flavor. They are members of the nightshade family, related to bell peppers and tomatoes. Many varieties of chili peppers exist, such as cayenne and jalapeño. Chili peppers are primarily used as a spice and can be cooked or dried and powdered. Powdered, red chili peppers are known as paprika." + ), + ProductModel( + id = "bhuLZpaY5yqEssty5V7m", + name = "Grass-Fed Local Beef Brisket", + price = 3.9, + image = "https://firebasestorage.googleapis.com/v0/b/groceries-store-ad0eb.appspot.com/o/Vegetables%2Fcsm_spaghetti-squash-overview_b846b6fa65.jpg?alt=media&token=1b68952b-26f0-489a-b979-e23a60b52c45", + description = "Braise this lean cut in a seasoned broth alongside vegetables, and you'll be rewarded with the ultimate in comfort food.", + nutrition = "Beef is the meat of cattle (Bos taurus). It is categorized as red meat — a term used for the meat of mammals, which contains higher amounts of iron than chicken or fish. Usually eaten as roasts, ribs, or steaks, beef is also commonly ground or minced. Patties of ground beef are often used in hamburgers. Processed beef products include corned beef, beef jerky, and sausages." + ) + ), + openProductDetails = {}, + addToCart = {}, + contentPadding = PaddingValues(0.dp) + ) + } +} +@Preview +@Composable +private fun ProductListScreenPreview() { + ProductListScreen( + state = ProductListViewState( + "Meat & Fish", + persistentListOf() + ), + navigateUp = {}, + showFilter = {}, + openProductDetails = {}, + addToCart = {} + ) +} diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListViewModel.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListViewModel.kt index 4b55db7f..5f04f64d 100644 --- a/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListViewModel.kt +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListViewModel.kt @@ -15,12 +15,12 @@ import com.hieuwu.groceriesstore.domain.usecases.GetProductsListUseCase import com.hieuwu.groceriesstore.utilities.OrderStatus import dagger.hilt.android.lifecycle.HiltViewModel import java.util.UUID +import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -36,33 +36,30 @@ class ProductListViewModel @Inject constructor( private val args = ProductListFragmentArgs.fromSavedStateHandle(savedStateHandle) private val categoryId = args.categoryId + private val categoryName = args.categoryName // TODO: check type for categoryId - private val _productList: Flow>? = if (categoryId == null) { + private val _productList: Flow> = if (categoryId == null) { getProductLists() } else { getProductLists(categoryId) } - val productList: StateFlow> = _productList - ?.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())!! + + val state = _productList.map { + ProductListViewState( + categoryName, + it.toImmutableList() + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = ProductListViewState.Empty + ) var currentCart: StateFlow = getCurrentCart() ?.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)!! - private val _navigateToSelectedProperty = MutableStateFlow(null) - - val navigateToSelectedProperty: StateFlow - get() = _navigateToSelectedProperty.asStateFlow() - - fun displayProductDetail(product: ProductModel) { - _navigateToSelectedProperty.value = product - } - - fun displayProductDetailComplete() { - _navigateToSelectedProperty.value = null - } - private fun getCurrentCart(): Flow? { var res: Flow? = null viewModelScope.launch { @@ -71,21 +68,12 @@ class ProductListViewModel @Inject constructor( return res } - private fun getProductLists(): Flow>? { - var res: Flow>? = null - viewModelScope.launch { - res = getProductsListUseCase.execute(GetProductsListUseCase.Input()).result - } - return res + private fun getProductLists(): Flow> { + return getProductsListUseCase.execute(GetProductsListUseCase.Input()).result } - private fun getProductLists(categoryId: String): Flow>? { - var res: Flow>? = null - viewModelScope.launch { - res = - getProductsByCategoryUseCase.execute(GetProductsByCategoryUseCase.Input(categoryId)).result - } - return res + private fun getProductLists(categoryId: String): Flow> { + return getProductsByCategoryUseCase.execute(GetProductsByCategoryUseCase.Input(categoryId)).result } fun addToCart(product: ProductModel) { diff --git a/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListViewState.kt b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListViewState.kt new file mode 100644 index 00000000..18ec9e58 --- /dev/null +++ b/app/src/main/java/com/hieuwu/groceriesstore/presentation/productlist/ProductListViewState.kt @@ -0,0 +1,16 @@ +package com.hieuwu.groceriesstore.presentation.productlist + +import androidx.compose.runtime.Immutable +import com.hieuwu.groceriesstore.domain.models.ProductModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +class ProductListViewState( + val categoryTitle: String = "", + val productList: ImmutableList = persistentListOf() +) { + companion object { + val Empty = ProductListViewState() + } +} diff --git a/app/src/main/res/layout/fragment_product_list.xml b/app/src/main/res/layout/fragment_product_list.xml deleted file mode 100644 index ae89c519..00000000 --- a/app/src/main/res/layout/fragment_product_list.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3ede0840..a195633c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,4 +80,6 @@ Get your gorceries in as fast as one hour Update Profile Save - \ No newline at end of file + Navigate back + Save + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5f04a569..7d4fdaf0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ firebaseBomVersion = "30.3.2" fireStoreVersion = "24.2.2" firebaseVersion = "23.0.6" glideVersion = "4.13.2" +glideComposeVersion = "1.0.0-beta01" googleServicesVersion = "4.3.13" gsonVersion = "2.9.0" hiltVersion = '2.42' @@ -18,6 +19,7 @@ jUnitVersion = "4.13.2" kotlinVersion = "1.7.0" kotlinCoroutineAdapter = "0.9.2" kotlinCoroutinePlayServicesVersion = "1.6.4" +kotlinxImmutable = "0.3.5" ktlintGradleVersion = "11.0.0" legacySupportVersion = "1.0.0" lifecycleVersion = "2.5.0" @@ -54,12 +56,14 @@ firebase-firestore = { module = "com.google.firebase:firebase-firestore-ktx", ve firebase-messaging = { module = "com.google.firebase:firebase-messaging", version.ref = "firebaseVersion" } glide = { module = "com.github.bumptech.glide:glide", version.ref = "glideVersion" } glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glideVersion" } +glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "glideComposeVersion" } gson = { module = "com.google.code.gson:gson", version.ref = "gsonVersion" } hilt = { module = "com.google.dagger:hilt-android", version.ref = "hiltVersion" } hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltVersion" } junit = { module = "junit:junit", version.ref = "jUnitVersion" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlinVersion" } kotlin-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinCoroutinePlayServicesVersion" } +kotlinx-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxImmutable" } legacy-support-v4 = { module = "androidx.legacy:legacy-support-v4", version.ref = "legacySupportVersion" } lottie = { module = "com.airbnb.android:lottie", version.ref = "lottieVersion" } lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottieVersion" }