diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cf07133..65fef7f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,9 +17,15 @@ android:supportsRtl="true" android:theme="@style/Theme.Woka" tools:targetApi="31"> + + android:exported="false" + android:screenOrientation="portrait" /> > + + @GET("cart_listing") + suspend fun cartListing(): Response> + + @POST("remove_cart") + suspend fun removeCartItem(@Body formBody: FormBody): Response> + + @GET("coupon_listing") + suspend fun couponsListing(): Response> } \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/ShopRepository.kt b/app/src/main/java/com/woka/shop/ShopRepository.kt index 58e09e2..52af212 100644 --- a/app/src/main/java/com/woka/shop/ShopRepository.kt +++ b/app/src/main/java/com/woka/shop/ShopRepository.kt @@ -3,7 +3,9 @@ package com.woka.shop import com.woka.networking.ApiResult import com.woka.networking.RetrofitHelper import com.woka.networking.RetrofitHelper.handleApiCall +import com.woka.shop.models.cartlisting.CartResponse import com.woka.shop.models.categorylisting.CategoryResponse +import com.woka.shop.models.couponlisting.CouponsResponse import com.woka.shop.models.subcategorylisting.SubCategoryResponse import com.woka.shop.models.superlisting.SuperCategoryResponse import okhttp3.FormBody @@ -12,7 +14,7 @@ object ShopRepository { private val apiService = RetrofitHelper.getRetrofit().create(ShopApiService::class.java) suspend fun superCategoryListing(): ApiResult { - return handleApiCall{ + return handleApiCall { apiService.superCategoryListing( FormBody.Builder() .add("module_id", "5") @@ -22,7 +24,7 @@ object ShopRepository { } suspend fun categoryListing(superCategoryId: String): ApiResult { - return handleApiCall{ + return handleApiCall { apiService.categoryListing( FormBody.Builder() .add("module_id", "5") @@ -33,7 +35,7 @@ object ShopRepository { } suspend fun subCategoryListing(categoryId: String): ApiResult { - return handleApiCall{ + return handleApiCall { apiService.subcategoryListing( FormBody.Builder() .add("category_id", categoryId) @@ -41,4 +43,67 @@ object ShopRepository { ) } } + + // cart listing with loose caching + private var cartResponse: CartResponse? = null + + suspend fun cartListing(): ApiResult { + if (cartResponse != null) { + return ApiResult.Success(cartResponse) + } + + val response = handleApiCall { + apiService.cartListing() + } + + when (response) { + is ApiResult.Error -> {} + is ApiResult.Loading -> {} + is ApiResult.Success -> { + cartResponse = response.data + } + } + + return response + } + + suspend fun removeCartItem(id: Int): ApiResult { + val response = handleApiCall { + apiService.removeCartItem( + FormBody.Builder() + .add("shop_master_id", "$id") + .build() + ) + } + + when (response) { + is ApiResult.Error -> { + return ApiResult.Error(response.errorMessage, response.error) + } + is ApiResult.Loading -> { + return ApiResult.Loading() + } + is ApiResult.Success -> { + // removing from cache + cartResponse?.let { cartData -> + cartData.result?.let { cartItems -> + cartItems.find { it?.id == id }?.let {item -> + val remainingAmount = (cartData.total_amount?:0.0) - (item.product_final_price?:0.0) + cartData.total_amount = maxOf(0.0, remainingAmount) + } + + cartItems.removeIf { it?.id == id } + } + } + + return ApiResult.Success(cartResponse?.total_amount, response.message) + } + } + } + + suspend fun couponsListing(): ApiResult { + return handleApiCall { + apiService.couponsListing() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/adapters/CartAdapter.kt b/app/src/main/java/com/woka/shop/adapters/CartAdapter.kt new file mode 100644 index 0000000..5a83e06 --- /dev/null +++ b/app/src/main/java/com/woka/shop/adapters/CartAdapter.kt @@ -0,0 +1,65 @@ +package com.woka.shop.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.woka.databinding.CartItemViewHolderBinding +import com.woka.shop.models.cartlisting.CartItem +import com.woka.utils.show +import java.util.concurrent.Executors + +class CartAdapter(private val showFullData: Boolean = true): ListAdapter(ASYNC_DIFF_UTIL) { + + companion object{ + private val DIFF_UTIL = object :DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem: CartItem, newItem: CartItem): Boolean = oldItem.id == newItem.id + override fun areContentsTheSame(oldItem: CartItem, newItem: CartItem): Boolean = oldItem == newItem + } + + private val ASYNC_DIFF_UTIL = AsyncDifferConfig.Builder(DIFF_UTIL) + .setBackgroundThreadExecutor(Executors.newSingleThreadExecutor()) + .build() + } + + inner class CartViewHolder(val binding: CartItemViewHolderBinding): ViewHolder(binding.root) + + var onCartItemDeleteListener: ((Int, Int) -> Unit)? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartViewHolder { + return CartViewHolder( + CartItemViewHolderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: CartViewHolder, position: Int) { + val cartItem = getItem(holder.absoluteAdapterPosition) + + with(holder.binding){ + if (cartItem.shop_image?.isNotEmpty() == true) + image.loadImage(cartItem.shop_image.first()) + + title.text = cartItem.product_name + price.text = "${cartItem.product_price}" + + if (showFullData){ + quantityTag.show() + quantity.text = "${cartItem.product_quantity}" + + statusTag.show() + status.text = "${cartItem.stock_status}" + + delete.show() + delete.setOnClickListener { + cartItem.id?.let { id -> onCartItemDeleteListener?.invoke(id, holder.absoluteAdapterPosition) } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/adapters/CouponAdapter.kt b/app/src/main/java/com/woka/shop/adapters/CouponAdapter.kt new file mode 100644 index 0000000..3cb3c25 --- /dev/null +++ b/app/src/main/java/com/woka/shop/adapters/CouponAdapter.kt @@ -0,0 +1,52 @@ +package com.woka.shop.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.woka.databinding.CouponViewHolderrBinding +import com.woka.shop.models.couponlisting.CouponData +import java.util.concurrent.Executors + +class CouponAdapter : ListAdapter(DIFF_CONFIG) { + + companion object { + private val DIFF_UTIL = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CouponData, newItem: CouponData): Boolean = + oldItem.id == newItem.id + + override fun areContentsTheSame(oldItem: CouponData, newItem: CouponData): Boolean = + oldItem == newItem + } + + private val DIFF_CONFIG = AsyncDifferConfig.Builder(DIFF_UTIL) + .setBackgroundThreadExecutor(Executors.newSingleThreadExecutor()) + .build() + } + + inner class CouponViewHolder(val binding: CouponViewHolderrBinding) : ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CouponViewHolder { + return CouponViewHolder( + CouponViewHolderrBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: CouponViewHolder, position: Int) { + val coupon = getItem(holder.absoluteAdapterPosition) + + holder.binding.apply { + val off = if (coupon.discount_type == 1) "${coupon.discount_value}%" + else "Rs. ${coupon.discount_value}" + + val finalTxt = "$off OFF with coupon code ${coupon.coupon_code}" + textView.text = finalTxt + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/models/cartlisting/CartItem.kt b/app/src/main/java/com/woka/shop/models/cartlisting/CartItem.kt new file mode 100644 index 0000000..7f403b3 --- /dev/null +++ b/app/src/main/java/com/woka/shop/models/cartlisting/CartItem.kt @@ -0,0 +1,18 @@ +package com.woka.shop.models.cartlisting + +data class CartItem( + val category_master_id: Int?, + val id: Int?, + val product_name: String?, + val product_price: String?, + var product_final_price: Double?, + val product_quantity: Int?, + val remain_stock_quantity: Int?, + val shop_image: List?, + val shop_master_detail: ShopMasterDetail?, + val sku_id: String?, + val stock_status: String?, + val sub_category_master_id: Int?, + val tax_category: Any?, + val tax_value: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/models/cartlisting/CartResponse.kt b/app/src/main/java/com/woka/shop/models/cartlisting/CartResponse.kt new file mode 100644 index 0000000..0affb63 --- /dev/null +++ b/app/src/main/java/com/woka/shop/models/cartlisting/CartResponse.kt @@ -0,0 +1,11 @@ +package com.woka.shop.models.cartlisting + +data class CartResponse( + val address_available: Boolean?, + val cart_quantity: Int?, + val cart_total_amount: String?, + val result: MutableList?, + var total_amount: Double?, + val total_records: Int?, + val user_type: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/models/cartlisting/ShopMasterDetail.kt b/app/src/main/java/com/woka/shop/models/cartlisting/ShopMasterDetail.kt new file mode 100644 index 0000000..75a1a6b --- /dev/null +++ b/app/src/main/java/com/woka/shop/models/cartlisting/ShopMasterDetail.kt @@ -0,0 +1,10 @@ +package com.woka.shop.models.cartlisting + +data class ShopMasterDetail( + val description_english: String?, + val description_hindi: String?, + val id: Int?, + val product_id: Int?, + val product_name_english: String?, + val product_name_hindi: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/models/couponlisting/Category.kt b/app/src/main/java/com/woka/shop/models/couponlisting/Category.kt new file mode 100644 index 0000000..29074b8 --- /dev/null +++ b/app/src/main/java/com/woka/shop/models/couponlisting/Category.kt @@ -0,0 +1,6 @@ +package com.woka.shop.models.couponlisting + +data class Category( + val category_name: String?, + val id: Int? +) \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/models/couponlisting/CouponData.kt b/app/src/main/java/com/woka/shop/models/couponlisting/CouponData.kt new file mode 100644 index 0000000..0a1a044 --- /dev/null +++ b/app/src/main/java/com/woka/shop/models/couponlisting/CouponData.kt @@ -0,0 +1,13 @@ +package com.woka.shop.models.couponlisting + +data class CouponData( + val category: Category?, + val category_master_id: Int?, + val coupon_code: String?, + val discount_type: Int?, + val discount_value: String?, + val end_date: String?, + val id: Int?, + val start_from: String?, + val usage_limit: Int? +) \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/models/couponlisting/CouponsResponse.kt b/app/src/main/java/com/woka/shop/models/couponlisting/CouponsResponse.kt new file mode 100644 index 0000000..8358e34 --- /dev/null +++ b/app/src/main/java/com/woka/shop/models/couponlisting/CouponsResponse.kt @@ -0,0 +1,6 @@ +package com.woka.shop.models.couponlisting + +data class CouponsResponse( + val result: List?, + val total_records: Int? +) \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/viewmodels/CartViewModel.kt b/app/src/main/java/com/woka/shop/viewmodels/CartViewModel.kt new file mode 100644 index 0000000..7379ddb --- /dev/null +++ b/app/src/main/java/com/woka/shop/viewmodels/CartViewModel.kt @@ -0,0 +1,39 @@ +package com.woka.shop.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.woka.networking.ApiResult +import com.woka.shop.ShopRepository +import com.woka.shop.models.cartlisting.CartResponse +import com.woka.shop.models.couponlisting.CouponsResponse +import kotlinx.coroutines.launch + +class CartViewModel: ViewModel() { + + // ui callbacks + var onToolBarTitleChange: ((String) -> Unit)? = null + + // data callbacks + private val repository = ShopRepository + + private val _cartListLiveData = MutableLiveData>() + val cartListLiveData: LiveData> + get() = _cartListLiveData + + fun loadCartList(){ + viewModelScope.launch { + _cartListLiveData.postValue(ApiResult.Loading()) + _cartListLiveData.postValue(repository.cartListing()) + } + } + + suspend fun removeItem(id: Int): ApiResult { + return repository.removeCartItem(id) + } + + suspend fun loadCoupons(): ApiResult{ + return repository.couponsListing() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/views/CartActivity.kt b/app/src/main/java/com/woka/shop/views/CartActivity.kt new file mode 100644 index 0000000..1da950c --- /dev/null +++ b/app/src/main/java/com/woka/shop/views/CartActivity.kt @@ -0,0 +1,48 @@ +package com.woka.shop.views + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.ViewModelProvider +import com.woka.databinding.ActivityCartBinding +import com.woka.shop.viewmodels.CartViewModel +import com.woka.utils.WokaBaseActivity + +class CartActivity : WokaBaseActivity() { + + private lateinit var binding: ActivityCartBinding + private lateinit var viewModel: CartViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + binding = ActivityCartBinding.inflate(layoutInflater) + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + viewModel = ViewModelProvider(this)[CartViewModel::class.java] + + clickEvents() + + setObservers() + } + + private fun clickEvents() { + binding.apply { + toolbar.backBtn.setOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + } + } + + private fun setObservers(){ + viewModel.onToolBarTitleChange = { + binding.toolbar.title.text = it + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/views/ShopActivity.kt b/app/src/main/java/com/woka/shop/views/ShopActivity.kt index 9eafc58..28dfb36 100644 --- a/app/src/main/java/com/woka/shop/views/ShopActivity.kt +++ b/app/src/main/java/com/woka/shop/views/ShopActivity.kt @@ -1,5 +1,6 @@ package com.woka.shop.views +import android.content.Intent import android.os.Bundle import androidx.activity.enableEdgeToEdge import androidx.core.view.ViewCompat @@ -46,6 +47,12 @@ class ShopActivity : WokaBaseActivity() { backBtn.setOnClickListener { onBackPressedDispatcher.onBackPressed() } + + cart.setOnClickListener { + startActivity( + Intent(this@ShopActivity, CartActivity::class.java) + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/views/fragments/CartFragment.kt b/app/src/main/java/com/woka/shop/views/fragments/CartFragment.kt new file mode 100644 index 0000000..6fc8a57 --- /dev/null +++ b/app/src/main/java/com/woka/shop/views/fragments/CartFragment.kt @@ -0,0 +1,156 @@ +package com.woka.shop.views.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.woka.R +import com.woka.databinding.FragmentCartBinding +import com.woka.networking.ApiResult +import com.woka.shop.adapters.CartAdapter +import com.woka.shop.viewmodels.CartViewModel +import com.woka.utils.ProgressView +import com.woka.utils.hide +import com.woka.utils.show +import com.woka.utils.toast +import kotlinx.coroutines.launch + +class CartFragment: Fragment() { + + private lateinit var binding: FragmentCartBinding + + private lateinit var adapter: CartAdapter + private lateinit var viewModel: CartViewModel + + private lateinit var progressDialog: ProgressView + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentCartBinding.inflate(inflater, container, false) + + viewModel = ViewModelProvider(requireActivity())[CartViewModel::class.java] + adapter = CartAdapter() + progressDialog = ProgressView(requireContext()) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initViews() + + clickEvents() + + setObservers() + + if (!viewModel.cartListLiveData.isInitialized || viewModel.cartListLiveData.value !is ApiResult.Success){ + viewModel.loadCartList() + } + } + + private fun initViews() { + binding.apply { + viewModel.onToolBarTitleChange?.invoke(getString(R.string.shop)) + rvCart.adapter = adapter + } + } + + private fun clickEvents() { + binding.apply { + + adapter.onCartItemDeleteListener = {id, position -> + lifecycleScope.launch { + progressDialog.show(getString(R.string.removing_item)) + + when (val response = viewModel.removeItem(id)){ + is ApiResult.Error -> { + progressDialog.hide() + toast(response.errorMessage) + } + is ApiResult.Loading -> {} + is ApiResult.Success -> { + progressDialog.hide() + toast(response.message) + + try { + adapter.notifyItemRemoved(position) + } finally { + response.data?.let {cartValue -> + if (cartValue > 0){ + val finalAmount = "₹ $cartValue" + totalAmount.text = finalAmount + }else{ + rvCart.hide() + progressView.hide() + checkoutView.hide() + + noDataView.show() + } + } + } + } + } + } + } + + binding.checkout.setOnClickListener { + findNavController().navigate(R.id.action_cartFragment_to_orderSummaryFragment) + } + } + } + + private fun setObservers(){ + viewModel.cartListLiveData.observe(viewLifecycleOwner){ + binding.apply { + when (it){ + is ApiResult.Error -> { + rvCart.hide() + progressView.hide() + checkoutView.hide() + + noDataView.show() + } + is ApiResult.Loading -> { + rvCart.hide() + progressView.show() + checkoutView.hide() + noDataView.hide() + } + is ApiResult.Success -> { + it.data?.let {cartData -> + cartData.result?.let {cartList -> + if (cartList.isNotEmpty()){ + adapter.submitList(cartList){ + progressView.hide() + noDataView.hide() + + rvCart.show() + + val finalAmount = "₹ ${cartData.total_amount}" + totalAmount.text = finalAmount + + checkoutView.show() + } + }else{ + // cart is empty + rvCart.hide() + progressView.hide() + checkoutView.hide() + + noDataView.show() + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/views/fragments/OrderSummaryFragment.kt b/app/src/main/java/com/woka/shop/views/fragments/OrderSummaryFragment.kt new file mode 100644 index 0000000..fbef976 --- /dev/null +++ b/app/src/main/java/com/woka/shop/views/fragments/OrderSummaryFragment.kt @@ -0,0 +1,102 @@ +package com.woka.shop.views.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.woka.R +import com.woka.databinding.FragmentOrderSummaryBinding +import com.woka.networking.ApiResult +import com.woka.shop.adapters.CartAdapter +import com.woka.shop.adapters.CouponAdapter +import com.woka.shop.viewmodels.CartViewModel +import com.woka.utils.hide +import com.woka.utils.show +import kotlinx.coroutines.launch + +class OrderSummaryFragment : Fragment() { + + private lateinit var binding: FragmentOrderSummaryBinding + private lateinit var viewModel: CartViewModel + private lateinit var adapter: CartAdapter + private lateinit var couponAdapter: CouponAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentOrderSummaryBinding.inflate(inflater, container, false) + + viewModel = ViewModelProvider(requireActivity())[CartViewModel::class.java] + adapter = CartAdapter(false) + couponAdapter = CouponAdapter() + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initViews() + + setObservers() + + loadCoupons() + } + + private fun initViews(){ + binding.apply { + viewModel.onToolBarTitleChange?.invoke(getString(R.string.payment_options)) + + rvCart.adapter = adapter + rvCoupons.adapter = couponAdapter + } + } + + private fun loadCoupons(){ + lifecycleScope.launch { + when (val response = viewModel.loadCoupons()){ + is ApiResult.Error -> {} + is ApiResult.Loading -> {} + is ApiResult.Success -> { + response.data?.result?.filterNotNull()?.let { + couponAdapter.submitList(it) + } + } + } + } + } + + private fun setObservers() { + viewModel.cartListLiveData.observe(viewLifecycleOwner){ + binding.apply { + when (it){ + is ApiResult.Error -> { + rvCart.hide() + } + is ApiResult.Loading -> { + rvCart.hide() + } + is ApiResult.Success -> { + it.data?.let {cartData -> + cartData.result?.let {cartList -> + if (cartList.isNotEmpty()){ + mainLayout.show() + adapter.submitList(cartList){ + rvCart.show() + } + }else{ + // cart is empty + rvCart.hide() + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000..cdc2f86 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_cart.xml b/app/src/main/res/layout/activity_cart.xml new file mode 100644 index 0000000..d1ccd9a --- /dev/null +++ b/app/src/main/res/layout/activity_cart.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cart_item_view_holder.xml b/app/src/main/res/layout/cart_item_view_holder.xml new file mode 100644 index 0000000..910afeb --- /dev/null +++ b/app/src/main/res/layout/cart_item_view_holder.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/coupon_view_holderr.xml b/app/src/main/res/layout/coupon_view_holderr.xml new file mode 100644 index 0000000..0a83a16 --- /dev/null +++ b/app/src/main/res/layout/coupon_view_holderr.xml @@ -0,0 +1,31 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_cart.xml b/app/src/main/res/layout/fragment_cart.xml new file mode 100644 index 0000000..bb5e577 --- /dev/null +++ b/app/src/main/res/layout/fragment_cart.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +