diff --git a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/RewardsBountyBoardScreen.kt b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/RewardsBountyBoardScreen.kt index 8b5945c92d..7c3f8123a5 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/RewardsBountyBoardScreen.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/ui/compose/screen/RewardsBountyBoardScreen.kt @@ -312,7 +312,9 @@ fun RewardsBountyBoardScreen( }, onScratched = { referenceId -> if (viewModel.scratchCardDataSource is ApiScratchCardDataSource) { - viewModel.scratchCardPagingDataSource.scratched(referenceId) + viewModel.scratchCardPagingDataSource.setScratchedItem( + referenceId + ) } }, sendBackResponse = { response -> diff --git a/android/navi-coin/src/main/java/com/navi/coin/utils/pagingsource/ScratchCardPagingSource.kt b/android/navi-coin/src/main/java/com/navi/coin/utils/pagingsource/ScratchCardPagingSource.kt index 1744da8d49..9a5043ee9f 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/utils/pagingsource/ScratchCardPagingSource.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/utils/pagingsource/ScratchCardPagingSource.kt @@ -15,7 +15,7 @@ import com.navi.common.utils.isValidResponse import com.navi.rr.common.constants.REWARDS_BOUNTY_BOARD_SCREEN import com.navi.rr.scratchcard.model.ScratchCardModel import com.navi.rr.utils.pagingsource.NaviScratchCardPagingSource -import com.navi.rr.utils.pagingsource.state.ItemState +import com.navi.rr.utils.pagingsource.state.State class ScratchCardPagingSource( private val repository: RewardsBountyBoardRepo, @@ -30,7 +30,7 @@ class ScratchCardPagingSource( return item.referenceId.orEmpty() } - override suspend fun fetchNextPage(): List> { + override suspend fun fetchNextPage(): List> { if (!isNextPageAvailable) return emptyList() @@ -47,6 +47,6 @@ class ScratchCardPagingSource( if (response.isValidResponse()) { isNextPageAvailable = response.data?.isNextPageAvailable.orTrue() } - return response.data?.scratchCardHistoryList.orEmpty().map { ItemState(element = it) } + return response.data?.scratchCardHistoryList.orEmpty().map { State(element = it) } } } diff --git a/android/navi-rr/src/main/java/com/navi/rr/utils/datasource/ApiScratchCardDataSource.kt b/android/navi-rr/src/main/java/com/navi/rr/utils/datasource/ApiScratchCardDataSource.kt index c2839ea470..a590a6cbf7 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/utils/datasource/ApiScratchCardDataSource.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/utils/datasource/ApiScratchCardDataSource.kt @@ -18,12 +18,10 @@ class ApiScratchCardDataSource( override suspend fun getCard(): ScratchCardModelDataEntity? { return naviScratchCardPagingSource.getNextItem()?.run { - item?.let { - ScratchCardModelDataEntity( - it.element.copy(templateName = templateName), - !isLastItem, - ) - } + ScratchCardModelDataEntity( + scratchCardModel = item.element.copy(templateName = templateName), + isNextElementAvailable = !isLastItem, + ) } } } diff --git a/android/navi-rr/src/main/java/com/navi/rr/utils/pagingsource/NaviScratchCardPagingSource.kt b/android/navi-rr/src/main/java/com/navi/rr/utils/pagingsource/NaviScratchCardPagingSource.kt index 4860c8cee4..a449231d36 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/utils/pagingsource/NaviScratchCardPagingSource.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/utils/pagingsource/NaviScratchCardPagingSource.kt @@ -8,8 +8,8 @@ package com.navi.rr.utils.pagingsource import com.navi.common.utils.log -import com.navi.rr.utils.pagingsource.state.ItemState import com.navi.rr.utils.pagingsource.state.PagingItem +import com.navi.rr.utils.pagingsource.state.State import java.util.concurrent.ConcurrentLinkedQueue import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope @@ -20,71 +20,78 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch abstract class NaviScratchCardPagingSource(private val thresholdPercentage: Float) { - private val coroutineScope = - CoroutineScope( - Dispatchers.IO + CoroutineExceptionHandler { _, throwable -> throwable.log() } - ) - private val itemQueue: ConcurrentLinkedQueue> = ConcurrentLinkedQueue() + private val itemQueue: ConcurrentLinkedQueue> = ConcurrentLinkedQueue() - private val _isQueueEmpty = MutableStateFlow(false) // StateFlow to track queue state + private val _isQueueEmpty = + MutableStateFlow(itemQueue.isEmpty()) // StateFlow to track queue state val isQueueEmpty = _isQueueEmpty.asStateFlow() - protected abstract fun getReferenceId(item: Item): String // Abstract method to get referenceID - - private fun updateQueueState() { - _isQueueEmpty.update { itemQueue.isEmpty() } - } - - var isNextPageAvailable: Boolean = true - - private var isLastPage: Boolean = false + protected var isNextPageAvailable: Boolean = true private var isPrefetching: Boolean = false private var lastPageSize: Int = 10 - protected abstract suspend fun fetchNextPage(): List>? + private val coroutineScope = + CoroutineScope( + Dispatchers.IO + + CoroutineExceptionHandler { _, throwable -> + clearData() + throwable.log() + } + ) - suspend fun getNextItem(): PagingItem?>? { + private fun updateQueueState() { + _isQueueEmpty.update { itemQueue.isEmpty() } + } + + protected abstract fun getReferenceId(item: Item): String // Abstract method to get referenceID + + protected abstract suspend fun fetchNextPage(): List>? + + suspend fun getNextItem(): PagingItem>? { return getNextItems() } - private suspend fun getNextItems(): PagingItem?>? { - var needPrefetch = false - - if (itemQueue.isEmpty() && !isLastPage) { - val pageItems = fetchNextPage() - if (pageItems != null && pageItems.isEmpty()) { - isLastPage = true - } else { - pageItems?.let { - addItemToQueueUniquely(pageItems) - lastPageSize = pageItems.size - updateQueueState() - } - } - } + private suspend fun getNextItems(): PagingItem>? { + fetchInitialPageIfNeeded() if (itemQueue.isEmpty()) return null - val nextItem = getNextNonPeekedItem() + val nextItem = getNextItemAndMarkPeeked() ?: return null - val thresholdCount = (lastPageSize * thresholdPercentage).toInt() + triggerPrefetchIfNeeded() - nextItem?.let { nextItem.isPeeked = true } - if (getNonPeekedItemsCount() <= thresholdCount && (!isLastPage && isNextPageAvailable)) { - needPrefetch = true - } + val isLastItem = isLastAvailableItem() - if (needPrefetch) { + return PagingItem(nextItem, isLastItem) + } + + private fun fetchInitialPageIfNeeded() { + if (itemQueue.isEmpty() && isNextPageAvailable) { + isPrefetching = false prefetchData() } - val isLastItem = getNonPeekedItemsCount() == 0 && (isLastPage || isNextPageAvailable.not()) + } - val pagingItem = PagingItem(nextItem, isLastItem) + private fun getNextItemAndMarkPeeked(): State? { + val item = getNextNonPeekedItem() + item?.isPeeked = true + return item + } - return pagingItem + private fun triggerPrefetchIfNeeded() { + val thresholdCount = lastPageSize * thresholdPercentage + val remaining = getNonPeekedItemsCount() + + if (remaining <= thresholdCount && isNextPageAvailable) { + prefetchData() + } + } + + private fun isLastAvailableItem(): Boolean { + return getNonPeekedItemsCount() == 0 && !isNextPageAvailable } private fun prefetchData() { @@ -92,15 +99,11 @@ abstract class NaviScratchCardPagingSource(private val thresholdPercentage coroutineScope.launch { isPrefetching = true val newItems = fetchNextPage() - if (newItems != null && newItems.isEmpty()) { - isLastPage = true - } else { - newItems?.let { - addItemToQueueUniquely(newItems) - updateQueueState() - lastPageSize = newItems.size - } + newItems?.let { + addItemToQueueUniquely(newItems) + lastPageSize = newItems.size } + updateQueueState() isPrefetching = false } } @@ -110,27 +113,19 @@ abstract class NaviScratchCardPagingSource(private val thresholdPercentage } fun resetPeekedItems() { - removeScratched() - iterateOverQueue { item, _ -> if (item.isScratched.not()) item.isPeeked = false } + iterateOverQueue { item, _ -> item.isPeeked = item.isScratched } + updateQueueState() } fun clearData() { itemQueue.clear() - isLastPage = false isPrefetching = false isNextPageAvailable = true updateQueueState() } - private fun getNextNonPeekedItem(): ItemState? { - val iterator = itemQueue.iterator() - while (iterator.hasNext()) { - val item = iterator.next() - if (!item.isPeeked) { - return item - } - } - return null + private fun getNextNonPeekedItem(): State? { + return findFirstInQueue { item, _ -> !item.isPeeked } } private fun getNonPeekedItemsCount(): Int { @@ -145,7 +140,7 @@ abstract class NaviScratchCardPagingSource(private val thresholdPercentage return count } - fun notPeeked(referenceId: String) { + fun setNonPeekedItem(referenceId: String) { iterateOverQueue { item, _ -> if (getReferenceId(item.element) == referenceId) { item.isPeeked = false @@ -153,7 +148,7 @@ abstract class NaviScratchCardPagingSource(private val thresholdPercentage } } - fun scratched(referenceId: String) { + fun setScratchedItem(referenceId: String) { iterateOverQueue { item, _ -> if (getReferenceId(item.element) == referenceId) { item.isScratched = true @@ -163,15 +158,19 @@ abstract class NaviScratchCardPagingSource(private val thresholdPercentage } private fun removeScratched() { + var removed = false iterateOverQueue { item, itr -> if (item.isScratched) { + removed = true itr.remove() - updateQueueState() } } + if (removed) { + updateQueueState() + } } - private fun addItemToQueueUniquely(pageItems: List>) { + private fun addItemToQueueUniquely(pageItems: List>) { pageItems.forEach { networkItem -> val networkItemRefId = getReferenceId(networkItem.element) @@ -181,6 +180,7 @@ abstract class NaviScratchCardPagingSource(private val thresholdPercentage val queueItemRefId = getReferenceId(itemState.element) if (networkItemRefId == queueItemRefId) { itemFound = true + return@iterateOverQueue } } if (!itemFound) { @@ -190,12 +190,23 @@ abstract class NaviScratchCardPagingSource(private val thresholdPercentage } private fun iterateOverQueue( - action: (ItemState, iterator: MutableIterator>) -> Unit + predicate: (State, iterator: MutableIterator>) -> Unit ) { val iterator = itemQueue.iterator() while (iterator.hasNext()) { val item = iterator.next() - action(item, iterator) + predicate(item, iterator) } } + + private fun findFirstInQueue( + predicate: (State, MutableIterator>) -> Boolean + ): State? { + val iterator = itemQueue.iterator() + while (iterator.hasNext()) { + val item = iterator.next() + if (predicate(item, iterator)) return item + } + return null + } } diff --git a/android/navi-rr/src/main/java/com/navi/rr/utils/pagingsource/state/ItemState.kt b/android/navi-rr/src/main/java/com/navi/rr/utils/pagingsource/state/State.kt similarity index 93% rename from android/navi-rr/src/main/java/com/navi/rr/utils/pagingsource/state/ItemState.kt rename to android/navi-rr/src/main/java/com/navi/rr/utils/pagingsource/state/State.kt index d03a8d49c1..f190afd5be 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/utils/pagingsource/state/ItemState.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/utils/pagingsource/state/State.kt @@ -7,7 +7,7 @@ package com.navi.rr.utils.pagingsource.state -data class ItemState( +data class State( var isPeeked: Boolean = false, var isScratched: Boolean = false, val element: T,