Add generic cart models and CartUpdateManager
This commit is contained in:
parent
ec1d6e5e61
commit
28b362ce00
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue