From 28b362ce002ef2d0e902037b0a72ace4f4ba123c Mon Sep 17 00:00:00 2001 From: Grigorii Date: Mon, 9 Jan 2023 13:00:37 +0400 Subject: [PATCH] Add generic cart models and CartUpdateManager --- .../roboswag/cart_utils/models/CartModel.kt | 19 +++ .../cart_utils/models/ProductModel.kt | 19 +++ .../repositories/IRemoteCartRepository.kt | 19 +++ .../repositories/LocalCartRepository.kt | 64 ++++++++++ .../update_manager/CartUpdateManager.kt | 113 ++++++++++++++++++ 5 files changed, 234 insertions(+) create mode 100644 cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/models/CartModel.kt create mode 100644 cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/models/ProductModel.kt create mode 100644 cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/repositories/IRemoteCartRepository.kt create mode 100644 cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/repositories/LocalCartRepository.kt create mode 100644 cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/update_manager/CartUpdateManager.kt diff --git a/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/models/CartModel.kt b/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/models/CartModel.kt new file mode 100644 index 0000000..e95b549 --- /dev/null +++ b/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/models/CartModel.kt @@ -0,0 +1,19 @@ +package ru.touchin.roboswag.cart_utils.models + +abstract class CartModel { + + abstract val products: List + + val availableProducts: List + get() = products.filter { it.isAvailable && !it.isDeleted } + + val totalPrice: Int + get() = availableProducts.sumOf { it.price } + + abstract fun copyWith( + products: List = this.products, + ): TCart + + @Suppress("UNCHECKED_CAST") + fun asCart() = this as TCart +} diff --git a/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/models/ProductModel.kt b/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/models/ProductModel.kt new file mode 100644 index 0000000..56e192b --- /dev/null +++ b/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/models/ProductModel.kt @@ -0,0 +1,19 @@ +package ru.touchin.roboswag.cart_utils.models + +abstract class ProductModel { + abstract val id: Int + abstract val countInCart: Int + abstract val price: Int + abstract val isAvailable: Boolean + abstract val isDeleted: Boolean + + open val variants: List = emptyList() + + abstract fun copyWith( + countInCart: Int = this.countInCart, + isDeleted: Boolean = this.isDeleted, + ): TProduct + + @Suppress("UNCHECKED_CAST") + fun asProduct(): TProduct = this as TProduct +} diff --git a/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/repositories/IRemoteCartRepository.kt b/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/repositories/IRemoteCartRepository.kt new file mode 100644 index 0000000..149a960 --- /dev/null +++ b/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/repositories/IRemoteCartRepository.kt @@ -0,0 +1,19 @@ +package ru.touchin.roboswag.cart_utils.repositories + +import ru.touchin.roboswag.cart_utils.models.CartModel +import ru.touchin.roboswag.cart_utils.models.ProductModel + +/** + * Interface for server-side cart repository where each request should return updated [CartModel] + */ +interface IRemoteCartRepository, TProduct : ProductModel> { + + suspend fun getCart(): TCart + + suspend fun addProduct(product: TProduct): TCart + + suspend fun removeProduct(id: Int): TCart + + suspend fun editProductCount(id: Int, count: Int): TCart + +} diff --git a/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/repositories/LocalCartRepository.kt b/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/repositories/LocalCartRepository.kt new file mode 100644 index 0000000..fb7b2ba --- /dev/null +++ b/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/repositories/LocalCartRepository.kt @@ -0,0 +1,64 @@ +package ru.touchin.roboswag.cart_utils.repositories + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import ru.touchin.roboswag.cart_utils.models.CartModel +import ru.touchin.roboswag.cart_utils.models.ProductModel + +/** + * Class that contains StateFlow of current [CartModel] which can be subscribed in ViewModels + */ +class LocalCartRepository, TProduct : ProductModel>( + initialCart: TCart +) { + + private val _currentCart = MutableStateFlow(initialCart) + val currentCart = _currentCart.asStateFlow() + + fun updateCart(cart: TCart) { + _currentCart.value = cart + } + + fun addProduct(product: TProduct) { + updateCartProducts { + add(product) + } + } + + fun removeProduct(id: Int) { + updateCartProducts { + val product = find { it.id == id } + remove(product) + } + } + + fun editProductCount(id: Int, count: Int) { + updateCartProducts { + updateProduct(id) { copyWith(countInCart = count) } + } + } + + fun markProductDeleted(id: Int) { + updateCartProducts { + updateProduct(id) { copyWith(isDeleted = true) } + } + } + + fun restoreDeletedProduct(id: Int) { + updateCartProducts { + updateProduct(id) { copyWith(isDeleted = false) } + } + } + + private fun updateCartProducts(updateAction: MutableList.() -> Unit) { + _currentCart.update { cart -> + cart.copyWith(products = cart.products.toMutableList().apply(updateAction)) + } + } + + private fun MutableList.updateProduct(id: Int, updateAction: TProduct.() -> TProduct) { + val index = indexOfFirst { it.id == id } + if (index >= 0) this[index] = updateAction.invoke(this[index]) + } +} diff --git a/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/update_manager/CartUpdateManager.kt b/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/update_manager/CartUpdateManager.kt new file mode 100644 index 0000000..5c2a143 --- /dev/null +++ b/cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/update_manager/CartUpdateManager.kt @@ -0,0 +1,113 @@ +package ru.touchin.roboswag.cart_utils.update_manager + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import ru.touchin.roboswag.cart_utils.models.CartModel +import ru.touchin.roboswag.cart_utils.models.ProductModel +import ru.touchin.roboswag.cart_utils.repositories.IRemoteCartRepository +import ru.touchin.roboswag.cart_utils.repositories.LocalCartRepository +import ru.touchin.roboswag.cart_utils.requests_qeue.Request +import ru.touchin.roboswag.cart_utils.requests_qeue.RequestsQueue + +/** + * Combines local and remote cart update actions + */ +open class CartUpdateManager, TProduct : ProductModel>( + private val localCartRepository: LocalCartRepository, + private val remoteCartRepository: IRemoteCartRepository, + private val maxRequestAttemptsCount: Int = MAX_REQUEST_CART_ATTEMPTS_COUNT, + private val errorHandler: (Throwable) -> Unit = {}, +) { + + companion object { + private const val MAX_REQUEST_CART_ATTEMPTS_COUNT = 3 + } + + private val requestsQueue = RequestsQueue>() + + @Volatile + var lastRemoteCart: TCart? = null + private set + + fun initCartRequestsQueue( + coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO), + ) { + requestsQueue.initRequestsExecution(coroutineScope) { request -> + runCatching { + lastRemoteCart = request.invoke() + if (!requestsQueue.hasPendingRequests()) updateLocalCartWithRemote() + }.onFailure { error -> + errorHandler.invoke(error) + requestsQueue.clearQueue() + tryToGetRemoteCartAgain() + } + } + } + + open fun addProduct(product: TProduct, restoreDeleted: Boolean = false) { + with(localCartRepository) { + if (restoreDeleted) restoreDeletedProduct(product.id) else addProduct(product) + } + + requestsQueue.addToQueue { + remoteCartRepository.addProduct(product) + } + } + + open fun removeProduct(id: Int, markDeleted: Boolean = false) { + with(localCartRepository) { + if (markDeleted) markProductDeleted(id) else removeProduct(id) + } + + requestsQueue.addToQueue { + remoteCartRepository.removeProduct(id) + } + } + + open fun editProductCount(id: Int, count: Int) { + localCartRepository.editProductCount(id, count) + + requestsQueue.addToQueue { + remoteCartRepository.editProductCount(id, count) + } + } + + open fun completelyDeleteProduct(id: Int) { + localCartRepository.removeProduct(id) + } + + private suspend fun tryToGetRemoteCartAgain() { + repeat(maxRequestAttemptsCount) { + runCatching { + lastRemoteCart = remoteCartRepository.getCart() + updateLocalCartWithRemote() + return + } + } + } + + private fun updateLocalCartWithRemote() { + val remoteCart = lastRemoteCart ?: return + val remoteProducts = remoteCart.products + val localProducts = localCartRepository.currentCart.value.products + + val newProductsFromRemoteCart = remoteProducts.filter { remoteProduct -> + localProducts.none { it.id == remoteProduct.id } + } + + val mergedProducts = localProducts.mapNotNull { localProduct -> + val sameRemoteProduct = remoteProducts.find { it.id == localProduct.id } + + when { + sameRemoteProduct != null -> sameRemoteProduct + localProduct.isDeleted -> localProduct + else -> null + } + } + + val mergedCart = remoteCart.copyWith(products = mergedProducts + newProductsFromRemoteCart) + localCartRepository.updateCart(mergedCart) + } + +}