Integrated add_to_cart api and ui updates.

Cart count live data updates caching and handling cart data.

Text updates wrt to subcategory in product listing.

adding to cart data caching issue - problem solving
This commit is contained in:
2024-07-30 21:16:10 +05:30
parent 86f0aa47dc
commit 4d393b0fcf
27 changed files with 352 additions and 57 deletions

View File

@@ -42,6 +42,9 @@ interface ShopApiService {
@POST("remove_cart")
suspend fun removeCartItem(@Body formBody: FormBody): Response<ApiResponse<Any>>
@POST("add_cart")
suspend fun addToCart(@Body formBody: FormBody): Response<ApiResponse<Any>>
@GET("coupon_listing")
suspend fun couponsListing(): Response<ApiResponse<CouponsResponse>>

View File

@@ -1,5 +1,7 @@
package com.woka.shop
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.woka.networking.ApiResult
import com.woka.networking.RetrofitHelper
import com.woka.networking.RetrofitHelper.handleApiCall
@@ -7,6 +9,7 @@ import com.woka.shop.models.addaddress.AddAddressRequestData
import com.woka.shop.models.addaddress.AddAddressResponseData
import com.woka.shop.models.addresslisting.ParentAddressData
import com.woka.shop.models.applycoupon.ApplyCouponResponse
import com.woka.shop.models.cartlisting.CartItem
import com.woka.shop.models.cartlisting.CartResponse
import com.woka.shop.models.categorylisting.CategoryResponse
import com.woka.shop.models.couponlisting.CouponsResponse
@@ -14,6 +17,7 @@ import com.woka.shop.models.createorder.CreateOrderRequestData
import com.woka.shop.models.createorder.CreateOrderResponse
import com.woka.shop.models.edd.EDDResponse
import com.woka.shop.models.productlisting.ProductListingResponse
import com.woka.shop.models.productlisting.ShopProduct
import com.woka.shop.models.subcategorylisting.SubCategoryResponse
import com.woka.shop.models.superlisting.SuperCategoryResponse
import okhttp3.FormBody
@@ -74,6 +78,10 @@ object ShopRepository {
// cart listing with loose caching
private var cartResponse: CartResponse? = null
private val _cartCountLiveData = MutableLiveData<Int>()
val cartCountLiveData: LiveData<Int>
get() = _cartCountLiveData
suspend fun cartListing(): ApiResult<CartResponse> {
if (cartResponse != null) {
return ApiResult.Success(cartResponse)
@@ -88,12 +96,50 @@ object ShopRepository {
is ApiResult.Loading -> {}
is ApiResult.Success -> {
cartResponse = response.data
response.data?.result?.let {
_cartCountLiveData.postValue(it.size)
}
}
}
return response
}
suspend fun addToCart(shopProduct: ShopProduct): ApiResult<Any>{
val response = handleApiCall {
apiService.addToCart(
FormBody.Builder()
.add("shop_master_id", "${shopProduct.id}")
.build()
)
}
when (response) {
is ApiResult.Error -> {}
is ApiResult.Loading -> {}
is ApiResult.Success -> {
if (cartResponse == null){
cartListing()
}else{
// removing from cache
cartResponse?.let { cartData ->
cartData.result?.let { cartItems ->
cartItems.add(CartItem(shopProduct))
shopProduct.product_final_price?.let {
cartData.total_amount = (cartData.total_amount?:0.0) + it
}
cartItems.let {
_cartCountLiveData.postValue(it.size)
}
}
}
}
}
}
return response
}
suspend fun removeCartItem(id: Int): ApiResult<Double> {
val response = handleApiCall {
apiService.removeCartItem(
@@ -120,6 +166,10 @@ object ShopRepository {
}
cartItems.removeIf { it?.id == id }
cartItems.let {
_cartCountLiveData.postValue(it.size)
}
}
}

View File

@@ -26,7 +26,7 @@ class CartAdapter: ListAdapter<CartItem, CartAdapter.CartViewHolder>(ASYNC_DIFF_
inner class CartViewHolder(val binding: CartItemViewHolderBinding): ViewHolder(binding.root)
var onCartItemDeleteListener: ((Int, Int) -> Unit)? = null
var onCartItemDeleteListener: ((CartItem, Int) -> Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartViewHolder {
return CartViewHolder(
@@ -55,7 +55,7 @@ class CartAdapter: ListAdapter<CartItem, CartAdapter.CartViewHolder>(ASYNC_DIFF_
delete.show()
delete.setOnClickListener {
cartItem.id?.let { id -> onCartItemDeleteListener?.invoke(id, holder.absoluteAdapterPosition) }
onCartItemDeleteListener?.invoke(cartItem, holder.absoluteAdapterPosition)
}
}
}

View File

@@ -43,7 +43,12 @@ class ShopProductAdapter: ListAdapter<ShopProduct, ShopProductAdapter.ProductVie
if (product.shop_image?.isNotEmpty() == true)
image.loadImage(product.shop_image.first())
title.text = product.product_name
title.text = if (product.sub_category_master_id == 12){
product.shop_master_detail?.product_name_english
}else{
product.shop_master_detail?.product_name_hindi
}
price.text = "${product.product_price}"
root.setOnClickListener {

View File

@@ -1,5 +1,8 @@
package com.woka.shop.models.cartlisting
import com.woka.shop.models.productlisting.ShopMasterDetail
import com.woka.shop.models.productlisting.ShopProduct
data class CartItem(
val category_master_id: Int?,
val id: Int?,
@@ -13,6 +16,21 @@ data class CartItem(
val sku_id: String?,
val stock_status: String?,
val sub_category_master_id: Int?,
val tax_category: Any?,
val tax_value: String?
)
){
constructor(shopProduct: ShopProduct): this(
shopProduct.category_master_id,
shopProduct.id,
shopProduct.product_name,
shopProduct.product_price,
shopProduct.product_final_price,
1,
shopProduct.remain_stock_quantity,
shopProduct.shop_image,
shopProduct.shop_master_detail,
shopProduct.sku_id,
shopProduct.stock_status,
shopProduct.sub_category_master_id,
shopProduct.tax_value
)
}

View File

@@ -1,10 +0,0 @@
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?
)

View File

@@ -1,5 +1,9 @@
package com.woka.shop.models.productlisting
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class ShopMasterDetail(
val description_english: String?,
val description_hindi: String?,
@@ -7,4 +11,4 @@ data class ShopMasterDetail(
val product_id: Int?,
val product_name_english: String?,
val product_name_hindi: String?
)
): Parcelable

View File

@@ -1,11 +1,17 @@
package com.woka.shop.models.productlisting
import android.os.Parcelable
import com.woka.shop.models.cartlisting.CartItem
import kotlinx.parcelize.Parcelize
@Parcelize
data class ShopProduct(
val added_to_cart: Boolean?,
var added_to_cart: Boolean?,
val category_master_id: Int?,
val id: Int?,
val product_name: String?,
val product_price: String?,
val product_final_price: Double?,
val product_thumbnail: String?,
val remain_stock_quantity: Int?,
val shop_image: List<String?>?,
@@ -13,6 +19,33 @@ data class ShopProduct(
val sku_id: String?,
val stock_status: String?,
val sub_category_master_id: Int?,
val tax_category: Any?,
val tax_value: String?
)
): Parcelable{
constructor(cartItem: CartItem, added_to_cart: Boolean?): this(
added_to_cart,
cartItem.category_master_id,
cartItem.id,
cartItem.product_name,
cartItem.product_price,
cartItem.product_final_price,
null,
cartItem.remain_stock_quantity,
cartItem.shop_image,
cartItem.shop_master_detail,
cartItem.sku_id,
cartItem.stock_status,
cartItem.sub_category_master_id,
cartItem.tax_value
)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ShopProduct) return false
return other.id == this.id
}
override fun hashCode(): Int {
return this.id?:0
}
}

View File

@@ -12,6 +12,7 @@ import com.woka.shop.models.cartlisting.CartResponse
import com.woka.shop.models.couponlisting.CouponsResponse
import com.woka.shop.models.createorder.CreateOrderRequestData
import com.woka.shop.models.createorder.CreateOrderResponse
import com.woka.shop.models.productlisting.ShopProduct
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
@@ -30,6 +31,8 @@ class CartViewModel: ViewModel() {
var selectedAddressId: Int? = null
val eddMap = HashMap<Int, String>()
// cart
val removedProducts = ArrayList<ShopProduct>()
// data callbacks
private val repository = ShopRepository

View File

@@ -1,5 +1,6 @@
package com.woka.shop.viewmodels
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@@ -11,7 +12,9 @@ import com.woka.shop.models.productlisting.ShopProduct
import com.woka.shop.models.subcategorylisting.SubCategoryData
import com.woka.shop.models.superlisting.SuperCategory
import com.woka.utils.PagingData
import com.woka.utils.TAG
import kotlinx.coroutines.launch
import kotlin.collections.HashSet
class ShopViewModel: ViewModel() {
@@ -105,14 +108,14 @@ class ShopViewModel: ViewModel() {
}
// product listing
private val _productListingLiveData = MutableLiveData<ApiResult<MutableList<ShopProduct>>>()
val productListingLiveData: LiveData<ApiResult<MutableList<ShopProduct>>>
private val _productListingLiveData = MutableLiveData<ApiResult<HashSet<ShopProduct>>>()
val productListingLiveData: LiveData<ApiResult<HashSet<ShopProduct>>>
get() = _productListingLiveData
var productPagingData = HashMap<String, PagingData>()
// product data for every super-category, category and sub-category
private var productDataMap = HashMap<String, MutableList<ShopProduct>>()
private var productDataMap = HashMap<String, HashSet<ShopProduct>>()
fun loadProducts(superCategoryId: String, categoryId: String, subCategoryId: String?) {
val key = "${superCategoryId}_${categoryId}_$subCategoryId"
@@ -159,7 +162,7 @@ class ShopViewModel: ViewModel() {
is ApiResult.Success -> {
response.data?.let { data ->
data.result?.filterNotNull()?.let { newList ->
val currentList = productDataMap.getOrDefault(key, ArrayList())
val currentList = productDataMap.getOrDefault(key, HashSet())
currentList.addAll(newList)
productDataMap[key] = currentList
@@ -173,7 +176,39 @@ class ShopViewModel: ViewModel() {
}
}
fun removeProductsFromCart(products: List<ShopProduct>){
val toBeReplaced = mutableSetOf<ShopProduct>()
for (productSet in productDataMap.values){
toBeReplaced.clear()
for (product in products){
if (productSet.contains(product)){
toBeReplaced.add(product)
}
}
productSet.removeAll(toBeReplaced)
productSet.addAll(toBeReplaced)
}
_productListingLiveData.postValue(ApiResult.Success(productDataMap[""]))
}
fun clearProductListingLiveData() {
_productListingLiveData.postValue(ApiResult.Loading())
}
// cart
val cartCountLivedata: LiveData<Int>
get() {
viewModelScope.launch {
repository.cartListing()
}
return repository.cartCountLiveData
}
suspend fun addToCart(shopProduct: ShopProduct): ApiResult<Any> {
return repository.addToCart(shopProduct)
}
}

View File

@@ -11,6 +11,10 @@ import com.woka.utils.WokaBaseActivity
class CartActivity : WokaBaseActivity() {
companion object{
const val EXTRA_REMOVED_CART_ITEMS = "extra_removed_cart_items"
}
private lateinit var binding: ActivityCartBinding
private lateinit var viewModel: CartViewModel

View File

@@ -5,14 +5,18 @@ 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.R
import com.woka.databinding.ActivityShopBinding
import com.woka.shop.viewmodels.ShopViewModel
import com.woka.shop.views.fragments.shop.ShopFragment1
import com.woka.utils.WokaBaseActivity
import com.woka.utils.setVisibility
class ShopActivity : WokaBaseActivity() {
private lateinit var binding: ActivityShopBinding
private lateinit var viewModel: ShopViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -25,6 +29,8 @@ class ShopActivity : WokaBaseActivity() {
insets
}
viewModel = ViewModelProvider(this)[ShopViewModel::class.java]
window.navigationBarColor = getColor(R.color.orders_bg)
supportFragmentManager.beginTransaction()
@@ -34,6 +40,8 @@ class ShopActivity : WokaBaseActivity() {
initViews()
clickEvents()
setObservers()
}
private fun initViews(){
@@ -55,4 +63,11 @@ class ShopActivity : WokaBaseActivity() {
}
}
}
private fun setObservers() {
viewModel.cartCountLivedata.observe(this){
binding.cartCount.text = "$it"
binding.cartCountView.setVisibility(it > 0)
}
}
}

View File

@@ -1,9 +1,11 @@
package com.woka.shop.views.fragments.cart
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@@ -12,7 +14,9 @@ import com.woka.R
import com.woka.databinding.FragmentCartBinding
import com.woka.networking.ApiResult
import com.woka.shop.adapters.CartAdapter
import com.woka.shop.models.productlisting.ShopProduct
import com.woka.shop.viewmodels.CartViewModel
import com.woka.shop.views.CartActivity
import com.woka.utils.ProgressView
import com.woka.utils.hide
import com.woka.utils.show
@@ -65,35 +69,42 @@ class CartFragment: Fragment() {
private fun clickEvents() {
binding.apply {
adapter.onCartItemDeleteListener = {id, position ->
adapter.onCartItemDeleteListener = {cartItem, 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)
cartItem.id?.let {
when (val response = viewModel.removeItem(it)){
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()
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()
noDataView.show()
}
}
}
viewModel.removedProducts.add(ShopProduct(cartItem, false))
activity?.setResult(AppCompatActivity.RESULT_OK, Intent().apply {
putParcelableArrayListExtra(CartActivity.EXTRA_REMOVED_CART_ITEMS, viewModel.removedProducts)
})
}
}
}

View File

@@ -1,16 +1,28 @@
package com.woka.shop.views.fragments.shop
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle
import android.text.Html
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.google.android.material.tabs.TabLayoutMediator
import com.woka.R
import com.woka.databinding.FragmentProductBinding
import com.woka.networking.ApiResult
import com.woka.shop.adapters.ProductImagesAdapter
import com.woka.shop.models.productlisting.ShopProduct
import com.woka.shop.viewmodels.ShopViewModel
import com.woka.shop.views.CartActivity
import com.woka.utils.ProgressView
import com.woka.utils.toast
import kotlinx.coroutines.launch
class ProductFragment private constructor(
private val shopProduct: ShopProduct,
@@ -23,18 +35,26 @@ class ProductFragment private constructor(
}
private lateinit var binding: FragmentProductBinding
private lateinit var viewModel: ShopViewModel
private lateinit var imageAdapter: ProductImagesAdapter
private lateinit var progressView: ProgressView
private lateinit var cartLauncher: ActivityResultLauncher<Intent>
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentProductBinding.inflate(inflater, container, false)
viewModel = ViewModelProvider(requireActivity())[ShopViewModel::class.java]
progressView = ProgressView(requireContext(), getString(R.string.please_wait))
val imageList = ArrayList<String>()
shopProduct.shop_image?.filterNotNull()?.let {
imageList.addAll(it)
}
imageAdapter = ProductImagesAdapter(imageList)
return binding.root
}
@@ -43,6 +63,10 @@ class ProductFragment private constructor(
initViews()
clickEvents()
registerLaunchers()
}
private fun initViews() {
@@ -52,15 +76,77 @@ class ProductFragment private constructor(
TabLayoutMediator(tabLayout, vpImages){_, _ -> }.attach()
title.text = shopProduct.product_name
categoryName.text = category
skuId.text = shopProduct.sku_id
price.text = shopProduct.product_price
description.text = Html.fromHtml(
shopProduct.shop_master_detail?.description_english,
Html.FROM_HTML_MODE_LEGACY
)
addToCart.text = if (shopProduct.added_to_cart == true){
getString(R.string.view_cart)
}else{
getString(R.string.add_to_cart)
}
if (shopProduct.sub_category_master_id == 12){
title.text = shopProduct.shop_master_detail?.product_name_english
description.text = Html.fromHtml(
shopProduct.shop_master_detail?.description_english,
Html.FROM_HTML_MODE_LEGACY
)
}else{
title.text = shopProduct.shop_master_detail?.product_name_hindi
description.text = Html.fromHtml(
shopProduct.shop_master_detail?.description_hindi,
Html.FROM_HTML_MODE_LEGACY
)
}
}
}
private fun clickEvents(){
binding.apply {
addToCart.setOnClickListener {
if (shopProduct.added_to_cart == false){
lifecycleScope.launch {
progressView.show()
when (val response = viewModel.addToCart(shopProduct)){
is ApiResult.Error -> {
progressView.hide()
toast(response.errorMessage)
}
is ApiResult.Loading -> {}
is ApiResult.Success -> {
progressView.hide()
toast(response.message)
shopProduct.added_to_cart = true
addToCart.text = getString(R.string.view_cart)
}
}
}
}else{
activity?.let {
cartLauncher.launch(Intent(it, CartActivity::class.java))
}
}
}
}
}
private fun registerLaunchers(){
cartLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ activityResult ->
if (activityResult.resultCode == RESULT_OK){
@Suppress("DEPRECATION")
activityResult.data?.getParcelableArrayListExtra<ShopProduct>(CartActivity.EXTRA_REMOVED_CART_ITEMS)?.let {
viewModel.removeProductsFromCart(it)
if (it.toSet().contains(shopProduct)){
binding.addToCart.text = if (shopProduct.added_to_cart == true){
getString(R.string.view_cart)
}else{
getString(R.string.add_to_cart)
}
}
}
}
}
}
}

View File

@@ -160,7 +160,7 @@ class ShopFragment3 private constructor(
}
is ApiResult.Success -> {
it.data?.let { productList ->
it.data?.toMutableList()?.let { productList ->
binding.rvProducts.show()
binding.productShimmer.hide()

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thickness="3dp"
android:useLevel="false">
<solid android:color="@color/white" />
<stroke
android:width="1dp"
android:color="@color/color_primary"/>
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/selected_shop_dot"
android:state_selected="true"/>
<item android:drawable="@drawable/default_shop_dot"/>
</selector>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thickness="3dp"
android:useLevel="false">
<solid android:color="@color/color_primary" />
<stroke
android:width="1dp"
android:color="@android:color/darker_gray"/>
</shape>
</item>
</layer-list>

View File

@@ -57,11 +57,11 @@
<com.woka.utils.PressableImageView
android:id="@+id/cart"
android:visibility="visible"
android:layout_width="@dimen/_30sdp"
android:layout_height="@dimen/_30sdp"
android:layout_width="@dimen/_34sdp"
android:layout_height="@dimen/_34sdp"
android:contentDescription="@string/image"
android:src="@drawable/img_notification"
android:src="@drawable/img_cart"
android:scaleType="fitXY"
android:layout_centerVertical="true"

View File

@@ -52,7 +52,7 @@
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/vp_images"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_height="@dimen/_120sdp"
android:layout_marginTop="15dp"
/>
@@ -61,11 +61,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:tabBackground="@drawable/onboard_indicator_selector"
app:tabBackground="@drawable/product_indicator_selector"
app:tabGravity="center"
app:tabIndicatorHeight="0dp"
app:tabPaddingEnd="10dp"
app:tabPaddingStart="10dp"
app:tabRippleColor="@android:color/transparent"
android:background="@android:color/transparent"

View File

@@ -281,4 +281,5 @@
<string name="category_name">Category Name :</string>
<string name="sku_id">SKU Id :</string>
<string name="add_to_cart">ADD TO CART</string>
<string name="view_cart">view cart</string>
</resources>