Add generic cart models and CartUpdateManager

This commit is contained in:
Grigorii 2023-01-09 13:00:37 +04:00
parent ec1d6e5e61
commit 28b362ce00
5 changed files with 234 additions and 0 deletions

View File

@ -0,0 +1,19 @@
package ru.touchin.roboswag.cart_utils.models
abstract class CartModel<TProductModel : ProductModel> {
abstract val products: List<TProductModel>
val availableProducts: List<TProductModel>
get() = products.filter { it.isAvailable && !it.isDeleted }
val totalPrice: Int
get() = availableProducts.sumOf { it.price }
abstract fun <TCart> copyWith(
products: List<TProductModel> = this.products,
): TCart
@Suppress("UNCHECKED_CAST")
fun <TCart> asCart() = this as TCart
}

View File

@ -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<ProductModel> = emptyList()
abstract fun <TProduct> copyWith(
countInCart: Int = this.countInCart,
isDeleted: Boolean = this.isDeleted,
): TProduct
@Suppress("UNCHECKED_CAST")
fun <TProduct> asProduct(): TProduct = this as TProduct
}

View File

@ -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<TCart : CartModel<TProduct>, 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
}

View File

@ -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<TCart : CartModel<TProduct>, 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<TProduct>.() -> Unit) {
_currentCart.update { cart ->
cart.copyWith(products = cart.products.toMutableList().apply(updateAction))
}
}
private fun MutableList<TProduct>.updateProduct(id: Int, updateAction: TProduct.() -> TProduct) {
val index = indexOfFirst { it.id == id }
if (index >= 0) this[index] = updateAction.invoke(this[index])
}
}

View File

@ -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<TCart : CartModel<TProduct>, TProduct : ProductModel>(
private val localCartRepository: LocalCartRepository<TCart, TProduct>,
private val remoteCartRepository: IRemoteCartRepository<TCart, TProduct>,
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<Request<TCart>>()
@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<TCart>(products = mergedProducts + newProductsFromRemoteCart)
localCartRepository.updateCart(mergedCart)
}
}