Skip to content

Commit

Permalink
Merge branch 'refs/heads/issue/12967-retry-pagination-error' into iss…
Browse files Browse the repository at this point in the history
…ue/13061-integrate-api-change
  • Loading branch information
AnirudhBhat committed Dec 5, 2024
2 parents a2525fa + 2664527 commit c763e5a
Show file tree
Hide file tree
Showing 5 changed files with 334 additions and 581 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -96,28 +96,23 @@ fun WooPosItemList(
}
}

item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(104.dp),
contentAlignment = Alignment.Center
) {
when (state.paginationState) {
PaginationState.Error -> {
onErrorWhilePaginating()
}
PaginationState.Loading -> {
ItemsLoadingItem()
}
PaginationState.None -> {
Spacer(modifier = Modifier.height(0.dp))
}
when (state.paginationState) {
PaginationState.Error -> {
item {
onErrorWhilePaginating()
}
}
PaginationState.Loading -> {
item {
ItemsLoadingItem()
}
}
PaginationState.None -> {
}
}

item {
Spacer(modifier = Modifier.height(54.dp))
Spacer(modifier = Modifier.height(104.dp))
}
}
InfiniteListHandler(listState, state) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.woocommerce.android.ui.woopos.home.items.variations

import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class VariationsLRUCache<K, V>(private val maxSize: Int) {

companion object {
private const val LOAD_FACTOR = 0.75f
}
private val cache = object : LinkedHashMap<K, V>(maxSize, LOAD_FACTOR, true) {
override fun removeEldestEntry(eldest: Map.Entry<K, V>): Boolean {
return size > maxSize
}
}

private val mutex = Mutex()

suspend fun get(key: K): V? {
return mutex.withLock {
cache[key]
}
}

suspend fun put(key: K, value: V) {
mutex.withLock {
cache[key] = value
}
}

suspend fun remove(key: K) {
mutex.withLock {
cache.remove(key)
}
}

suspend fun clear() {
mutex.withLock {
cache.clear()
}
}

suspend fun containsKey(key: K): Boolean {
return mutex.withLock {
cache.containsKey(key)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import com.woocommerce.android.ui.products.variations.selector.VariationListHand
import com.woocommerce.android.util.WooLog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
Expand All @@ -13,30 +18,57 @@ import javax.inject.Singleton
class WooPosVariationsDataSource @Inject constructor(
private val handler: VariationListHandler
) {
fun getVariationsFlow(productId: Long): Flow<List<ProductVariation>> {
return handler.getVariationsFlow(productId)
private val variationCache = VariationsLRUCache<Long, List<ProductVariation>>(maxSize = 50)
private val cacheMutex = kotlinx.coroutines.sync.Mutex()

private suspend fun getCachedVariations(productId: Long): List<ProductVariation> {
return cacheMutex.withLock { variationCache.get(productId) ?: emptyList() }
}

private suspend fun updateCache(productId: Long, variations: List<ProductVariation>) {
cacheMutex.withLock {
variationCache.put(productId, variations)
}
}

fun canLoadMore(): Boolean {
return handler.canLoadMore()
}

suspend fun fetchVariations(productId: Long, forceRefresh: Boolean = true): Result<Unit> {
val result = handler.fetchVariations(productId, forceRefresh = forceRefresh)
return if (result.isSuccess) {
Result.success(Unit)
fun fetchFirstPage(
productId: Long,
forceRefresh: Boolean = true
): Flow<FetchResult<List<ProductVariation>>> = flow {
if (forceRefresh) {
updateCache(productId, emptyList())
}

val cachedVariations = getCachedVariations(productId)
if (cachedVariations.isNotEmpty()) {
emit(FetchResult.Cached(cachedVariations))
}

val result = handler.fetchVariations(productId, forceRefresh = true)
if (result.isSuccess) {
val remoteVariations = handler.getVariationsFlow(productId).firstOrNull() ?: emptyList()
updateCache(productId, remoteVariations)
emit(FetchResult.Remote(Result.success(remoteVariations)))
} else {
result.logFailure()
Result.failure(
result.exceptionOrNull() ?: Exception("Unknown error while loading more variations")
emit(
FetchResult.Remote(
Result.failure(
result.exceptionOrNull() ?: Exception("Unknown error while fetching variations")
)
)
)
}
}
}.flowOn(Dispatchers.IO)

suspend fun loadMore(productId: Long): Result<Unit> = withContext(Dispatchers.IO) {
suspend fun loadMore(productId: Long): Result<List<ProductVariation>> = withContext(Dispatchers.IO) {
val result = handler.loadMore(productId)
if (result.isSuccess) {
Result.success(Unit)
val fetchedVariations = handler.getVariationsFlow(productId).first()
Result.success(fetchedVariations)
} else {
result.logFailure()
Result.failure(
Expand All @@ -55,3 +87,8 @@ private fun Result<Unit>.logFailure() {
val errorMessage = error?.message ?: "Unknown error"
WooLog.e(WooLog.T.POS, "Loading variations failed - $errorMessage", error)
}

sealed class FetchResult<out T> {
data class Cached<out T>(val data: T) : FetchResult<T>()
data class Remote<out T>(val result: Result<T>) : FetchResult<T>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.woocommerce.android.ui.woopos.home.items.variations

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.woocommerce.android.model.ProductVariation
import com.woocommerce.android.ui.woopos.common.data.WooPosGetProductById
import com.woocommerce.android.ui.woopos.home.ChildToParentEvent
import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender
Expand All @@ -15,9 +16,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -39,60 +38,88 @@ class WooPosVariationsViewModel @Inject constructor(
)

private var flowJob: Job? = null
private var fetchJob: Job? = null
private var loadMoreJob: Job? = null

fun init(productId: Long, withPullToRefresh: Boolean = false, withCart: Boolean = true) {
resetState()
startCollectingVariationsFlow(productId)
fetchVariations(productId = productId, withPullToRefresh = withPullToRefresh, withCart = withCart)
loadVariations(
productId = productId,
withPullToRefresh = withPullToRefresh,
withCart = withCart,
forceRefresh = false
)
}

private fun resetState() {
flowJob?.cancel()
variationsDataSource.resetLoadMoreState()
_viewState.value = WooPosVariationsViewState.Loading(withCart = true)
}

private fun startCollectingVariationsFlow(productId: Long) {
flowJob?.cancel()
flowJob = viewModelScope.launch {
variationsDataSource.getVariationsFlow(productId)
.map { variationList ->
if (variationList.isEmpty() && fetchJob?.isActive == true) {
WooPosVariationsViewState.Loading(withCart = true)
} else if (variationList.isEmpty()) {
WooPosVariationsViewState.Error()
} else {
WooPosVariationsViewState.Content(
items = variationList.filter { it.price != null }.map {
WooPosItem.Variation(
id = it.remoteVariationId,
name = it.getName(getProductById(productId)),
productId = it.remoteProductId,
price = priceFormat(it.price),
imageUrl = it.image?.source
)
private fun loadVariations(
productId: Long,
forceRefresh: Boolean,
withPullToRefresh: Boolean,
withCart: Boolean,
) {
viewModelScope.launch {
_viewState.value = if (withPullToRefresh) {
buildProductsReloadingState()
} else {
WooPosVariationsViewState.Loading(withCart = withCart)
}

variationsDataSource.fetchFirstPage(productId, forceRefresh = forceRefresh).collect { result ->
when (result) {
is FetchResult.Cached -> {
if (result.data.isNotEmpty()) {
updateViewStateWithVariations(result.data, productId)
}
}

is FetchResult.Remote -> {
_viewState.value = when {
result.result.isSuccess -> {
val variations = result.result.getOrThrow()
if (variations.isNotEmpty()) {
WooPosVariationsViewState.Content(
items = variations.filter { it.price != null }.map {
WooPosItem.Variation(
id = it.remoteVariationId,
name = it.getName(getProductById(productId)),
productId = it.remoteProductId,
price = priceFormat(it.price),
imageUrl = it.image?.source
)
}
)
} else {
WooPosVariationsViewState.Empty()
}
}
)

else -> WooPosVariationsViewState.Error()
}
}
}
.collect { state ->
_viewState.value = state
}
}
}
}

private fun fetchVariations(productId: Long, withPullToRefresh: Boolean, withCart: Boolean) {
_viewState.value = if (withPullToRefresh) {
buildProductsReloadingState()
private suspend fun updateViewStateWithVariations(variations: List<ProductVariation>, productId: Long) {
if (variations.isEmpty()) {
_viewState.value = WooPosVariationsViewState.Empty()
} else {
WooPosVariationsViewState.Loading(withCart = withCart)
}
fetchJob?.cancel()

fetchJob = viewModelScope.launch {
variationsDataSource.fetchVariations(productId, forceRefresh = true)
_viewState.value = WooPosVariationsViewState.Content(
items = variations.filter { it.price != null }.map {
WooPosItem.Variation(
id = it.remoteVariationId,
name = it.getName(getProductById(productId)),
productId = it.remoteProductId,
price = priceFormat(it.price),
imageUrl = it.image?.source
)
}
)
}
}

Expand All @@ -119,32 +146,36 @@ class WooPosVariationsViewModel @Inject constructor(
loadMoreJob?.cancel()
loadMoreJob = viewModelScope.launch {
val result = variationsDataSource.loadMore(productId)
_viewState.update { current ->
if (current is WooPosVariationsViewState.Content) {
current.copy(paginationState = determinePaginationState(result))
} else {
current
}
_viewState.value = if (result.isSuccess) {
WooPosVariationsViewState.Content(
items = result.getOrThrow().filter { it.price != null }.map {
WooPosItem.Variation(
id = it.remoteVariationId,
name = it.getName(getProductById(productId)),
productId = it.remoteProductId,
price = priceFormat(it.price),
imageUrl = it.image?.source
)
}
)
} else {
currentState.copy(paginationState = PaginationState.Error)
}
}
}

private fun determinePaginationState(result: Result<*>): PaginationState {
return if (result.isSuccess) PaginationState.None else PaginationState.Error
}

fun onUIEvent(event: WooPosVariationsUIEvents) {
when (event) {
is WooPosVariationsUIEvents.EndOfItemsListReached -> {
onEndOfVariationsListReached(event.productId)
}

is WooPosVariationsUIEvents.PullToRefreshTriggered -> {
fetchVariations(event.productId, withPullToRefresh = true, withCart = false)
loadVariations(event.productId, forceRefresh = true, withPullToRefresh = true, withCart = false)
}

is WooPosVariationsUIEvents.VariationsLoadingErrorRetryButtonClicked -> {
init(event.productId, withPullToRefresh = false, withCart = false)
loadVariations(event.productId, forceRefresh = true, withPullToRefresh = false, withCart = false)
}

is WooPosVariationsUIEvents.OnItemClicked -> {
Expand Down
Loading

0 comments on commit c763e5a

Please sign in to comment.