NTP-8865 | AS | Feature/scratch history api revamp (#13316)
This commit is contained in:
@@ -111,4 +111,13 @@ interface RetrofitService {
|
||||
suspend fun getReferralData(
|
||||
@Header("X-Target") target: String
|
||||
): Response<GenericResponse<ReferralContactList>>
|
||||
|
||||
@GET("/rudolph/scratch-card/history")
|
||||
@RetryPolicy
|
||||
suspend fun getScratchCards(
|
||||
@Header("X-Target") target: String,
|
||||
@Query("pageSection") pageSection: String,
|
||||
@Query("pageNumber") pageNumber: String,
|
||||
@Query("pageSize") pageSize: String,
|
||||
): Response<GenericResponse<ScratchCardHistoryResponse>>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.coin.repo.pagingsource
|
||||
|
||||
import com.navi.base.utils.isNull
|
||||
import com.navi.coin.models.model.ScratchCard
|
||||
import com.navi.coin.models.model.ScratchCardHistoryResponse
|
||||
import com.navi.coin.models.states.ScratchCardPaginatedHistoryScreenState
|
||||
import com.navi.coin.repo.repository.ScratchCardHistoryScreenRepo
|
||||
import com.navi.common.checkmate.model.MetricInfo
|
||||
import com.navi.common.network.models.RepoResult
|
||||
import com.navi.common.network.models.isSuccessWithData
|
||||
import com.navi.rr.common.constants.SCRATCH_CARD_HISTORY_SCREEN
|
||||
import com.navi.rr.utils.custompager.BaseLazyGridPager
|
||||
import com.navi.rr.utils.custompager.CustomPagerConfig
|
||||
import com.navi.rr.utils.custompager.PagerItemState
|
||||
import com.navi.rr.utils.ext.isOdd
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class ScratchCardHistoryCustomPager(
|
||||
private val repository: ScratchCardHistoryScreenRepo,
|
||||
private val scope: CoroutineScope,
|
||||
pagerConfig: CustomPagerConfig = CustomPagerConfig(),
|
||||
) : BaseLazyGridPager<ScratchCard, ScratchCardHistoryResponse>(scope, pagerConfig) {
|
||||
|
||||
private var fetchType = FetchType.ACTIVE
|
||||
|
||||
private var buffer: ScratchCard? = null
|
||||
|
||||
private var previousAPICallIsUnScratched: Boolean = false
|
||||
|
||||
private val _scratchCardHistoryListData =
|
||||
MutableStateFlow<ScratchCardPaginatedHistoryScreenState>(
|
||||
ScratchCardPaginatedHistoryScreenState.Loading
|
||||
)
|
||||
val scratchCardHistoryListData = _scratchCardHistoryListData.asStateFlow()
|
||||
|
||||
private val DEFAULT_START_SIZE = 26
|
||||
|
||||
private val DEFAULT_ACTIVE_PAGE_SIZE = 50
|
||||
|
||||
init {
|
||||
pageSize = DEFAULT_ACTIVE_PAGE_SIZE
|
||||
pageNumber = 0
|
||||
}
|
||||
|
||||
override suspend fun fetchData(
|
||||
pageNumber: Int,
|
||||
pageSize: Int,
|
||||
metricInfo: MetricInfo<RepoResult<ScratchCardHistoryResponse>>
|
||||
): RepoResult<ScratchCardHistoryResponse> {
|
||||
return repository.getScratchCards(
|
||||
metricInfo = metricInfo,
|
||||
pageSection = getPageSectionByType(fetchType),
|
||||
pageSize = pageSize,
|
||||
pageNumber = pageNumber
|
||||
)
|
||||
}
|
||||
|
||||
private fun getPageSectionByType(fetchType: FetchType): String {
|
||||
return when (fetchType) {
|
||||
FetchType.ACTIVE -> FetchType.ACTIVE.name
|
||||
FetchType.PROCESSED -> FetchType.PROCESSED.name
|
||||
FetchType.EXPIRED -> FetchType.EXPIRED.name
|
||||
else -> FetchType.EXPIRED.name
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleIfEndOfListState(data: ScratchCardHistoryResponse?) {
|
||||
if (data?.isNextPageAvailable == false) {
|
||||
if (isEndOfTotalList().not()) {
|
||||
handleEndCase()
|
||||
} else {
|
||||
super.handleIfEndOfListState(data)
|
||||
if (pagerStates.value.pagingItems.size == 0)
|
||||
_scratchCardHistoryListData.update {
|
||||
ScratchCardPaginatedHistoryScreenState.Empty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function is used to set the next api to be called if we reach the end of previous api
|
||||
private fun handleEndCase() {
|
||||
when (fetchType) {
|
||||
FetchType.ACTIVE -> {
|
||||
fetchType = FetchType.PROCESSED
|
||||
previousAPICallIsUnScratched = true
|
||||
|
||||
// set page size and number to default
|
||||
pageNumber = DEFAULT_START_KEY
|
||||
pageSize = DEFAULT_PAGE_SIZE
|
||||
}
|
||||
FetchType.PROCESSED -> {
|
||||
fetchType = FetchType.EXPIRED
|
||||
pageNumber = DEFAULT_START_KEY
|
||||
}
|
||||
FetchType.EXPIRED -> {
|
||||
fetchType = FetchType.END_OF_LIST
|
||||
if (buffer.isNull()) pagerItemState.update { PagerItemState.EndOfList }
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun performPostLoadActions(data: ScratchCardHistoryResponse?) {
|
||||
handleMinimumData(data)
|
||||
}
|
||||
|
||||
// this handles the case when the data is less than 10 and the next api call is to be made
|
||||
private fun handleMinimumData(data: ScratchCardHistoryResponse?) {
|
||||
if (fetchType == FetchType.END_OF_LIST) return
|
||||
|
||||
val uiListSize = pagerList.value.size
|
||||
val dataSize = data?.scratchCardHistoryList?.size ?: 0
|
||||
|
||||
val isListSizeLessThanDefaultSize = uiListSize < DEFAULT_START_SIZE
|
||||
val isDataSizeLessThanDefaultPageSize = dataSize < pageSize
|
||||
val isUnscratchedDataOdd = previousAPICallIsUnScratched && uiListSize.isOdd()
|
||||
|
||||
if (
|
||||
isListSizeLessThanDefaultSize ||
|
||||
isDataSizeLessThanDefaultPageSize ||
|
||||
isUnscratchedDataOdd
|
||||
) {
|
||||
previousAPICallIsUnScratched = false
|
||||
loadNextPage()
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSuccessData(data: ScratchCardHistoryResponse?) {
|
||||
if (
|
||||
pagerList.value.isEmpty() &&
|
||||
data?.scratchCardHistoryList.isNullOrEmpty() &&
|
||||
fetchType == FetchType.EXPIRED
|
||||
) {
|
||||
_scratchCardHistoryListData.update { ScratchCardPaginatedHistoryScreenState.Empty }
|
||||
} else {
|
||||
val scratchCardList = alterDataIfRequired(data)
|
||||
pagerList.update { currentList ->
|
||||
currentList.apply { currentList.addAll(scratchCardList) }
|
||||
}
|
||||
_scratchCardHistoryListData.update { ScratchCardPaginatedHistoryScreenState.Success }
|
||||
}
|
||||
super.handleSuccessData(data)
|
||||
}
|
||||
|
||||
// This function is used to make the history ui as symmetric as possible by keeping even size
|
||||
// list
|
||||
private fun alterDataIfRequired(data: ScratchCardHistoryResponse?): List<ScratchCard> {
|
||||
val list = data?.scratchCardHistoryList.orEmpty().toMutableList()
|
||||
|
||||
// Will return if the list is of unscratched cards
|
||||
if (fetchType == FetchType.ACTIVE) {
|
||||
return list
|
||||
}
|
||||
val bufferSize = if (buffer != null) 1 else 0
|
||||
|
||||
val totalSizeIsOdd = (pagerList.value.size + list.size + bufferSize).isOdd()
|
||||
val alterData = totalSizeIsOdd && isEndOfTotalList().not()
|
||||
|
||||
// add stored scratch card if present using buffer
|
||||
buffer?.let {
|
||||
list.add(0, it)
|
||||
buffer = null
|
||||
}
|
||||
|
||||
// if it is the end of all api calls then no need to alter data
|
||||
if (fetchType == FetchType.EXPIRED && data?.isNextPageAvailable == false) return list
|
||||
|
||||
// alter data if total size is odd
|
||||
if (alterData && list.isNotEmpty()) {
|
||||
buffer = list.removeLast()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
override fun isEndOfTotalList() = fetchType == FetchType.END_OF_LIST
|
||||
|
||||
override fun refresh() {
|
||||
fetchType = FetchType.ACTIVE
|
||||
buffer = null
|
||||
pageSize = DEFAULT_ACTIVE_PAGE_SIZE
|
||||
super.refresh()
|
||||
}
|
||||
|
||||
override fun handleErrorState(data: ScratchCardHistoryResponse?) {
|
||||
_scratchCardHistoryListData.update { ScratchCardPaginatedHistoryScreenState.Error }
|
||||
}
|
||||
|
||||
override val metricInfo: MetricInfo<RepoResult<ScratchCardHistoryResponse>>
|
||||
get() =
|
||||
MetricInfo.CoinMetric(
|
||||
screen = SCRATCH_CARD_HISTORY_SCREEN,
|
||||
isNae = { !it.isSuccessWithData() }
|
||||
)
|
||||
|
||||
private enum class FetchType {
|
||||
ACTIVE,
|
||||
PROCESSED,
|
||||
EXPIRED,
|
||||
END_OF_LIST
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import com.navi.common.forge.model.ScreenDefinition
|
||||
import com.navi.common.model.ModuleNameV2
|
||||
import com.navi.common.network.models.RepoResult
|
||||
import com.navi.common.utils.Constants.GZIP
|
||||
import com.navi.rr.common.models.XTarget
|
||||
import com.navi.rr.common.network.retrofit.ResponseHandler
|
||||
import com.navi.rr.utils.cachemanager.CacheHandlerProxy
|
||||
import javax.inject.Inject
|
||||
@@ -47,6 +48,7 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
// Deprecated api for scratch card list
|
||||
suspend fun fetchScratchCardListData(
|
||||
pageNo: Int,
|
||||
pageSize: Int,
|
||||
@@ -64,4 +66,22 @@ constructor(
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getScratchCards(
|
||||
pageNumber: Int,
|
||||
pageSize: Int,
|
||||
pageSection: String,
|
||||
metricInfo: MetricInfo<RepoResult<ScratchCardHistoryResponse>>
|
||||
): RepoResult<ScratchCardHistoryResponse> {
|
||||
return responseHandler.handleResponse(
|
||||
metricInfo = metricInfo,
|
||||
response =
|
||||
retrofitService.getScratchCards(
|
||||
target = XTarget.RUDOLPH.name,
|
||||
pageSection = pageSection,
|
||||
pageNumber = pageNumber.toString(),
|
||||
pageSize = pageSize.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
package com.navi.coin.ui.compose.common
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.LocalOverscrollConfiguration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.animateScrollBy
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
@@ -30,9 +32,10 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
@@ -42,10 +45,12 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -58,8 +63,6 @@ import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
@@ -88,6 +91,7 @@ import com.navi.coin.utils.constant.Constants.SCRATCHED_SCRATCH_CARD_URL_KEY
|
||||
import com.navi.coin.utils.constant.Constants.SCRATCH_CARD_EXPIRED_BOTTOM_SHEET
|
||||
import com.navi.coin.utils.constant.Constants.SCRATCH_CARD_IN_PROGRESS_BOTTOM_SHEET
|
||||
import com.navi.coin.utils.constant.Constants.SCRATCH_CARD_RECEIVED_BOTTOM_SHEET
|
||||
import com.navi.coin.utils.constant.Constants.ScratchCardHistoryScreen.VIEW_MORE_SCROLL_DOWN_IN_DP
|
||||
import com.navi.coin.utils.ext.scaledSp
|
||||
import com.navi.coin.utils.formatDuration
|
||||
import com.navi.coin.vm.ScratchCardScreenVM
|
||||
@@ -101,8 +105,13 @@ import com.navi.rr.common.widgetFactory.WidgetRenderer
|
||||
import com.navi.rr.utils.NaviRRAnalytics
|
||||
import com.navi.rr.utils.constants.EventConstants.BOTTOM_SHEET_TYPE
|
||||
import com.navi.rr.utils.constants.EventConstants.SCRATCH_CARD_HISTORY_BOTTOM_SHEET_VISIBLE_EVENT
|
||||
import com.navi.rr.utils.custompager.PagerItemState
|
||||
import com.navi.rr.utils.custompager.PagerStateHolder
|
||||
import com.navi.uitron.model.UiTronResponse
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@SuppressLint("MutableCollectionMutableState")
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -111,17 +120,23 @@ fun ScratchCardListRenderer(
|
||||
modifier: Modifier,
|
||||
content: List<WidgetModelDefinition<UiTronResponse>>,
|
||||
viewModel: ScratchCardScreenVM,
|
||||
scratchHistoryCardList: LazyPagingItems<ScratchCard>,
|
||||
metadata: Map<String?, String?>?,
|
||||
renderBottomSheet: ((String, ScratchCard) -> Unit)?,
|
||||
showScratchCard: (ScratchCard) -> Unit,
|
||||
isScratchCardDisplayed: Boolean,
|
||||
setScratchCardDisplayed: (Boolean) -> Unit,
|
||||
pagerStates: PagerStateHolder<ScratchCard>,
|
||||
) {
|
||||
val analyticsHandler by remember { mutableStateOf(viewModel.analyticsHandler.Rewards()) }
|
||||
val paginatedHistoryScreenState =
|
||||
viewModel.scratchCardHistoryListSource.scratchCardHistoryListData
|
||||
.collectAsStateWithLifecycle()
|
||||
viewModel.scratchCardPager.scratchCardHistoryListData.collectAsStateWithLifecycle()
|
||||
|
||||
var hasMoreItemsToLoad by remember { mutableStateOf(true) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
val localDensity = LocalDensity.current
|
||||
val scrollToInPixel = remember { with(localDensity) { VIEW_MORE_SCROLL_DOWN_IN_DP.dp.toPx() } }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var clickedViewMore by remember { mutableStateOf(false) }
|
||||
BoxWithConstraints {
|
||||
val screenHeight = maxHeight
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
@@ -136,24 +151,29 @@ fun ScratchCardListRenderer(
|
||||
) {
|
||||
when (paginatedHistoryScreenState.value) {
|
||||
is ScratchCardPaginatedHistoryScreenState.Success -> {
|
||||
LaunchedEffect(
|
||||
scratchHistoryCardList.itemCount,
|
||||
scratchHistoryCardList
|
||||
) {
|
||||
LaunchedEffect(pagerStates.pagingItems.size) {
|
||||
viewModel.onScratchCardListChanged(
|
||||
scratchHistoryCardList,
|
||||
pagerStates.pagingItems,
|
||||
isScratchCardDisplayed,
|
||||
setScratchCardDisplayed
|
||||
)
|
||||
|
||||
if (clickedViewMore) {
|
||||
animateScrollDown(
|
||||
coroutineScope = coroutineScope,
|
||||
lazyGridState = viewModel.scratchCardPager.state,
|
||||
scrollToInPixel = scrollToInPixel
|
||||
)
|
||||
clickedViewMore = false
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
val lazyGridState = rememberLazyGridState()
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
state = lazyGridState,
|
||||
state = viewModel.scratchCardPager.state,
|
||||
) {
|
||||
items(count = content.size, span = { GridItemSpan(2) }) {
|
||||
WidgetRenderer(widget = content[it], viewModel = viewModel)
|
||||
@@ -196,26 +216,48 @@ fun ScratchCardListRenderer(
|
||||
lazyGridScope = this@LazyVerticalGrid,
|
||||
viewModel = viewModel,
|
||||
paginatedHistoryScreenState = paginatedHistoryScreenState.value,
|
||||
scratchHistoryCardList = scratchHistoryCardList,
|
||||
metadata = metadata,
|
||||
renderBottomSheet = renderBottomSheet,
|
||||
showScratchCard = showScratchCard,
|
||||
analyticsHandler = analyticsHandler
|
||||
analyticsHandler = analyticsHandler,
|
||||
pagerStates = pagerStates
|
||||
)
|
||||
}
|
||||
when (pagerStates.pagerLoadingItemState) {
|
||||
is PagerItemState.Loading -> {
|
||||
hasMoreItemsToLoad = true
|
||||
isLoading = true
|
||||
}
|
||||
is PagerItemState.Error -> {
|
||||
hasMoreItemsToLoad = false
|
||||
clickedViewMore = false
|
||||
item(span = { GridItemSpan(2) }) {
|
||||
RewardPaginationErrorComponent() {
|
||||
viewModel.scratchCardPager.retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
is PagerItemState.Loaded -> {
|
||||
hasMoreItemsToLoad = true
|
||||
isLoading = false
|
||||
}
|
||||
is PagerItemState.EndOfList -> {
|
||||
hasMoreItemsToLoad = false
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
scratchHistoryCardList.apply {
|
||||
when {
|
||||
loadState.refresh is LoadState.Loading -> {
|
||||
RewardHistoryLoadingComponent()
|
||||
}
|
||||
loadState.refresh is LoadState.Error -> {
|
||||
val error =
|
||||
scratchHistoryCardList.loadState.refresh as LoadState.Error
|
||||
RewardPaginationErrorComponent(scratchHistoryCardList::retry)
|
||||
}
|
||||
loadState.append is LoadState.Error -> {
|
||||
RewardPaginationErrorComponent(scratchHistoryCardList::retry)
|
||||
item(span = { GridItemSpan(2) }) {
|
||||
if (hasMoreItemsToLoad) {
|
||||
if (pagerStates.pagingItems.size > 0) {
|
||||
ViewMoreButton(isLoading) {
|
||||
if (isLoading.not()) {
|
||||
viewModel.scratchCardPager.loadNextPage()
|
||||
clickedViewMore = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
RewardHistoryLoadingComponent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,15 +268,81 @@ fun ScratchCardListRenderer(
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateScrollDown(
|
||||
coroutineScope: CoroutineScope,
|
||||
lazyGridState: LazyGridState,
|
||||
scrollToInPixel: Float
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
delay(300)
|
||||
lazyGridState.animateScrollBy(
|
||||
value = scrollToInPixel,
|
||||
animationSpec = tween(durationMillis = 300)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ViewMoreButton(loading: Boolean, onClick: () -> Unit) {
|
||||
Box(modifier = Modifier, contentAlignment = Alignment.Center) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.padding(top = 24.dp, bottom = 32.dp)
|
||||
.shadow(
|
||||
elevation = 8.dp,
|
||||
spotColor = Color(0xFFB0C0D9).copy(0.3f),
|
||||
clip = true,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
.width(156.dp)
|
||||
.height(32.dp)
|
||||
.background(color = Color.White, shape = RoundedCornerShape(4.dp))
|
||||
.clickable(interactionSource = NoRippleIndicationSource(), indication = null) {
|
||||
onClick.invoke()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (loading) {
|
||||
NaviCoinLottieAnimation(
|
||||
modifier = Modifier.width(32.dp),
|
||||
lottie = R.raw.progress_loader_purple_dotted,
|
||||
isRemoteLottie = false
|
||||
)
|
||||
} else {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "View more",
|
||||
textAlign = TextAlign.Center,
|
||||
style =
|
||||
TextStyle(
|
||||
fontSize = 12.scaledSp(),
|
||||
lineHeight = 18.scaledSp(),
|
||||
fontFamily = ttComposeFontFamily,
|
||||
fontWeight = getFontWeight(FontWeightEnum.TT_SEMI_BOLD),
|
||||
color = ctaTertiary,
|
||||
),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(WidgetsR.drawable.ic_arrow_black_down),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun scratchCardList(
|
||||
lazyGridScope: LazyGridScope,
|
||||
viewModel: ScratchCardScreenVM,
|
||||
paginatedHistoryScreenState: ScratchCardPaginatedHistoryScreenState,
|
||||
scratchHistoryCardList: LazyPagingItems<ScratchCard>,
|
||||
metadata: Map<String?, String?>?,
|
||||
renderBottomSheet: ((String, ScratchCard) -> Unit)?,
|
||||
showScratchCard: (ScratchCard) -> Unit,
|
||||
analyticsHandler: NaviRRAnalytics.Rewards
|
||||
analyticsHandler: NaviRRAnalytics.Rewards,
|
||||
pagerStates: PagerStateHolder<ScratchCard>
|
||||
) {
|
||||
|
||||
if (paginatedHistoryScreenState is ScratchCardPaginatedHistoryScreenState.Empty) {
|
||||
@@ -259,11 +367,11 @@ fun scratchCardList(
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (paginatedHistoryScreenState is ScratchCardPaginatedHistoryScreenState.Success) {
|
||||
lazyGridScope.items(count = scratchHistoryCardList.itemCount) { index ->
|
||||
val item = scratchHistoryCardList[index]
|
||||
} else {
|
||||
lazyGridScope.items(count = pagerStates.pagingItems.size) { index ->
|
||||
val item = pagerStates.pagingItems[index]
|
||||
val firstElement = index % 2 == 0
|
||||
item?.let {
|
||||
item.let {
|
||||
if (
|
||||
item.status == ScratchCardStatus.LOCKED.name ||
|
||||
item.status == ScratchCardStatus.EXPIRED.name
|
||||
|
||||
@@ -35,6 +35,7 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -57,8 +58,6 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.navi.base.deeplink.DeepLinkManager
|
||||
import com.navi.base.deeplink.util.DeeplinkConstants.PRODUCT_HELP_PAGE
|
||||
@@ -106,6 +105,7 @@ import com.navi.rr.scratchcard.model.ScratchCardResponse
|
||||
import com.navi.rr.scratchcard.ui.compose.ScratchCardRenderer
|
||||
import com.navi.rr.utils.NaviRRAnalytics
|
||||
import com.navi.rr.utils.constants.EventConstants
|
||||
import com.navi.rr.utils.custompager.PagerStateHolder
|
||||
import com.navi.rr.utils.ext.clickable
|
||||
import com.navi.rr.utils.filterAndPrioritize
|
||||
import com.navi.uitron.utils.setShimmerEffect
|
||||
@@ -130,9 +130,7 @@ fun ScratchCardHistoryScreen(
|
||||
var isScratchCardDisplayed by remember { mutableStateOf(false) }
|
||||
val scratchCardHistoryScreenState =
|
||||
viewModel.scratchCardHistoryScreenState.collectAsStateWithLifecycle()
|
||||
|
||||
val scratchCardHistoryList = viewModel.scratchCardHistoryListPager.collectAsLazyPagingItems()
|
||||
|
||||
val pagerStates by viewModel.scratchCardPager.pagerStates.collectAsState()
|
||||
val currentScratchCard by
|
||||
viewModel.scratchCardUseCases.scratchCard.collectAsStateWithLifecycle()
|
||||
val nextScratchCard by
|
||||
@@ -191,13 +189,13 @@ fun ScratchCardHistoryScreen(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) { viewModel.refreshData(scratchCardHistoryList) }
|
||||
LaunchedEffect(Unit) { viewModel.refreshData(pagerStates.pagingItems) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.scratchCardUseCases.scratchingComplete.collect {
|
||||
if (it) {
|
||||
viewModel.clearScratchCardList()
|
||||
viewModel.refreshData(scratchCardHistoryList)
|
||||
viewModel.refreshData(pagerStates.pagingItems)
|
||||
viewModel.scratchCardUseCases.setScratchingComplete(false)
|
||||
}
|
||||
}
|
||||
@@ -251,14 +249,14 @@ fun ScratchCardHistoryScreen(
|
||||
state = state,
|
||||
viewModel = viewModel,
|
||||
navigator = navigator,
|
||||
scratchCardHistoryList = scratchCardHistoryList,
|
||||
pagerStates = pagerStates,
|
||||
renderBottomSheet = { bottomSheetId, scratchCard ->
|
||||
renderBottomSheet(bottomSheetId, scratchCard)
|
||||
},
|
||||
showScratchCard = { startingCard ->
|
||||
viewModel.setScratchCardList(
|
||||
filterAndPrioritize(
|
||||
list = scratchCardHistoryList.itemSnapshotList.items,
|
||||
list = pagerStates.pagingItems,
|
||||
condition = { it.status == ScratchCardStatus.LOCKED.name },
|
||||
valueToStartCondition = { startingCard.refId == it.refId }
|
||||
)
|
||||
@@ -364,7 +362,7 @@ fun ScratchCardHistoryScreen(
|
||||
onBackPress = {
|
||||
viewModel.clearScratchCardList()
|
||||
if (viewModel.userHasScratchedCard) {
|
||||
viewModel.refreshData(scratchCardHistoryList)
|
||||
viewModel.refreshData(pagerStates.pagingItems)
|
||||
}
|
||||
}
|
||||
) { response ->
|
||||
@@ -414,7 +412,7 @@ fun ScratchCardHistoryScreen(
|
||||
.orFalse(),
|
||||
onBackPress = {
|
||||
viewModel.clearScratchCardList()
|
||||
viewModel.refreshData(scratchCardHistoryList)
|
||||
viewModel.refreshData(pagerStates.pagingItems)
|
||||
}
|
||||
) { response ->
|
||||
scratchAnalyticsTrigger(response, analyticsHandler)
|
||||
@@ -446,7 +444,7 @@ fun ScratchCardHistoryScreen(
|
||||
Modifier.clickable(disableRipple = true) {
|
||||
viewModel.clearScratchCardList()
|
||||
if (viewModel.userHasScratchedCard)
|
||||
viewModel.refreshData(scratchCardHistoryList)
|
||||
viewModel.refreshData(pagerStates.pagingItems)
|
||||
},
|
||||
painter = painterResource(CommonR.drawable.ic_close_cross_white),
|
||||
contentDescription = null
|
||||
@@ -471,11 +469,11 @@ fun ScratchCardHistorySuccessScreen(
|
||||
state: ScratchCardHistoryScreenState.Success,
|
||||
viewModel: ScratchCardScreenVM,
|
||||
navigator: DestinationsNavigator,
|
||||
scratchCardHistoryList: LazyPagingItems<ScratchCard>,
|
||||
renderBottomSheet: ((String, ScratchCard) -> Unit)?,
|
||||
showScratchCard: (ScratchCard) -> Unit,
|
||||
isScratchCardDisplayed: Boolean,
|
||||
setScratchCardDisplayed: (Boolean) -> Unit,
|
||||
pagerStates: PagerStateHolder<ScratchCard>,
|
||||
) {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val uiTronTopConfigHeightInPx = dpToPx(396)
|
||||
@@ -506,12 +504,12 @@ fun ScratchCardHistorySuccessScreen(
|
||||
modifier = Modifier.padding(it),
|
||||
content = state.data.screenStructure?.content?.widgets ?: listOf(),
|
||||
viewModel = viewModel,
|
||||
scratchHistoryCardList = scratchCardHistoryList,
|
||||
renderBottomSheet = renderBottomSheet,
|
||||
showScratchCard = showScratchCard,
|
||||
metadata = state.data.metaData,
|
||||
isScratchCardDisplayed = isScratchCardDisplayed,
|
||||
setScratchCardDisplayed = setScratchCardDisplayed,
|
||||
pagerStates = pagerStates
|
||||
)
|
||||
},
|
||||
backgroundColor = Color.Transparent
|
||||
|
||||
@@ -111,6 +111,10 @@ object Constants {
|
||||
const val EARN_UPTO = "Earn Upto"
|
||||
const val FOR_ = "for "
|
||||
}
|
||||
|
||||
object ScratchCardHistoryScreen {
|
||||
const val VIEW_MORE_SCROLL_DOWN_IN_DP = 115
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Status(val value: String) {
|
||||
|
||||
@@ -8,16 +8,12 @@
|
||||
package com.navi.coin.vm
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import com.navi.base.utils.orZero
|
||||
import com.navi.coin.models.model.ScratchCard
|
||||
import com.navi.coin.models.states.ScratchCardHistoryScreenState
|
||||
import com.navi.coin.models.states.ScratchCardStatus
|
||||
import com.navi.coin.navigator.CoinNavigationActions
|
||||
import com.navi.coin.repo.pagingsource.ScratchCardHistoryListSource
|
||||
import com.navi.coin.repo.pagingsource.ScratchCardHistoryCustomPager
|
||||
import com.navi.coin.repo.repository.ScratchCardHistoryScreenRepo
|
||||
import com.navi.coin.usecase.ScratchCardUseCases
|
||||
import com.navi.coin.utils.constant.Constants
|
||||
@@ -32,6 +28,7 @@ import com.navi.rr.common.constants.SCRATCH_CARD_HISTORY_SCREEN
|
||||
import com.navi.rr.common.di.qualifiers.CountDownHelperQualifier
|
||||
import com.navi.rr.common.models.RRErrorData
|
||||
import com.navi.rr.utils.cachingPngs
|
||||
import com.navi.rr.utils.custompager.CustomPagerConfig
|
||||
import com.navi.rr.utils.facade.ICountDownHelper
|
||||
import com.navi.uitron.model.UiTronResponse
|
||||
import com.navi.uitron.model.action.SchedulerAction
|
||||
@@ -44,10 +41,12 @@ import com.navi.uitron.model.data.UiTronActionData
|
||||
import com.ramcosta.composedestinations.spec.Direction
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
@@ -56,10 +55,20 @@ class ScratchCardScreenVM
|
||||
@Inject
|
||||
constructor(
|
||||
private val scratchCardHistoryScreenRepo: ScratchCardHistoryScreenRepo,
|
||||
var scratchCardHistoryListSource: ScratchCardHistoryListSource,
|
||||
@CountDownHelperQualifier val countDownHelper: ICountDownHelper,
|
||||
scratchCardUseCasesFactory: ScratchCardUseCases.Factory, // Inject the factory
|
||||
) : CoinBaseVM() {
|
||||
val scratchCardPager =
|
||||
ScratchCardHistoryCustomPager(
|
||||
repository = scratchCardHistoryScreenRepo,
|
||||
scope = viewModelScope.plus(SupervisorJob()),
|
||||
pagerConfig =
|
||||
CustomPagerConfig(
|
||||
autoLoadEnabled = false,
|
||||
prefetchDistance = 0,
|
||||
triggerInitialLoad = false
|
||||
)
|
||||
)
|
||||
|
||||
val scratchCardUseCases: ScratchCardUseCases = scratchCardUseCasesFactory.create(viewModelScope)
|
||||
|
||||
@@ -88,27 +97,9 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val scratchCardHistoryListPager =
|
||||
Pager(
|
||||
config =
|
||||
PagingConfig(
|
||||
pageSize = scratchCardHistoryListSource.DEFAULT_PAGE_SIZE,
|
||||
prefetchDistance = scratchCardHistoryListSource.DEFAULT_PAGE_SIZE,
|
||||
initialLoadSize = scratchCardHistoryListSource.DEFAULT_PAGE_SIZE * 5
|
||||
),
|
||||
pagingSourceFactory = {
|
||||
ScratchCardHistoryListSource(
|
||||
scratchCardHistoryScreenRepo = scratchCardHistoryScreenRepo
|
||||
)
|
||||
.also { scratchCardHistoryListSource = it }
|
||||
}
|
||||
)
|
||||
.flow
|
||||
.cachedIn(scope = viewModelScope)
|
||||
|
||||
fun refreshData(scratchCardHistoryList: LazyPagingItems<ScratchCard>) {
|
||||
fun refreshData(scratchCardHistoryList: List<ScratchCard>) {
|
||||
launch {
|
||||
scratchCardHistoryListSource.refresh()
|
||||
scratchCardPager.refresh()
|
||||
fetchScratchCardTopUiTronConfigs()
|
||||
userHasScratchedCard = false
|
||||
cancelAllTimer(scratchCardHistoryList)
|
||||
@@ -265,8 +256,8 @@ constructor(
|
||||
handle[key] = value
|
||||
}
|
||||
|
||||
fun createAllTimers(scratchCardList: LazyPagingItems<ScratchCard>) {
|
||||
scratchCardList.itemSnapshotList
|
||||
fun createAllTimers(scratchCardList: List<ScratchCard>) {
|
||||
scratchCardList
|
||||
.filter {
|
||||
it?.status == ScratchCardStatus.LOCKED.name &&
|
||||
it.expiresAfter.orZero() < ONE_DAY_IN_MILLI_SECONDS
|
||||
@@ -285,10 +276,9 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelAllTimer(scratchCardList: LazyPagingItems<ScratchCard>) {
|
||||
scratchCardList.itemSnapshotList
|
||||
.filter { it?.status == ScratchCardStatus.LOCKED.name }
|
||||
.filterNotNull()
|
||||
private fun cancelAllTimer(scratchCardList: List<ScratchCard>) {
|
||||
scratchCardList
|
||||
.filter { it.status == ScratchCardStatus.LOCKED.name }
|
||||
.forEach { handle[it.refId.toString()] = null }
|
||||
launch { countDownHelper.cancelAllTimers() }
|
||||
}
|
||||
@@ -353,13 +343,13 @@ constructor(
|
||||
}
|
||||
|
||||
fun onScratchCardListChanged(
|
||||
scratchHistoryCardList: LazyPagingItems<ScratchCard>,
|
||||
scratchHistoryCardList: List<ScratchCard>,
|
||||
isScratchCardDisplayed: Boolean,
|
||||
setScratchCardDisplayed: (Boolean) -> Unit,
|
||||
) {
|
||||
createAllTimers(scratchHistoryCardList)
|
||||
if (!isScratchCardDisplayed) {
|
||||
scratchHistoryCardList.itemSnapshotList
|
||||
scratchHistoryCardList
|
||||
.filterNotNull()
|
||||
.filter { it.status == ScratchCardStatus.LOCKED.name }
|
||||
.let {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.rr.common.models
|
||||
|
||||
enum class XTarget {
|
||||
RUDOLPH,
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.rr.utils.custompager
|
||||
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* Base class for paginating a lazy grid in Jetpack Compose. This class extends [BaseLazyPager] to
|
||||
* handle paginated data loading with support for a Compose [LazyGridState].
|
||||
*
|
||||
* @param IndividualResponseType The type of individual items in the list.
|
||||
* @param NetworkResponseType The type of the network response received when loading data.
|
||||
* @param scope [CoroutineScope] used to observe scroll state and trigger data loading.
|
||||
* @param config Configuration object that determines the pager's behavior, such as prefetch
|
||||
* distance and auto-loading.
|
||||
*/
|
||||
abstract class BaseLazyGridPager<IndividualResponseType : Any, NetworkResponseType : Any>(
|
||||
private val scope: CoroutineScope,
|
||||
private val config: CustomPagerConfig
|
||||
) : BaseLazyPager<IndividualResponseType, NetworkResponseType>(scope = scope, config = config) {
|
||||
|
||||
/** The state object that keeps track of the scroll state in the lazy grid. */
|
||||
val state by lazy { LazyGridState() }
|
||||
|
||||
/**
|
||||
* Determines the index of the last visible item in the lazy grid. Adjusts the value based on
|
||||
* the offset.
|
||||
*
|
||||
* @return The index of the last visible item relative to the currently loaded pages.
|
||||
*/
|
||||
override fun getLastPageVisibleItemIndex(): Int {
|
||||
val lastVisibleItemIndex = state.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
||||
return (lastVisibleItemIndex - offset).coerceAtLeast(0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.rr.utils.custompager
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* Base class for paginating a lazy list in Jetpack Compose. This class extends [BaseLazyPager] to
|
||||
* handle paginated data loading with support for a Compose [LazyListState].
|
||||
*
|
||||
* @param IndividualResponseType The type of individual items in the list.
|
||||
* @param NetworkResponseType The type of the network response received when loading data.
|
||||
* @param scope [CoroutineScope] used to observe scroll state and trigger data loading.
|
||||
* @param config Configuration object that determines the pager's behavior, such as prefetch
|
||||
* distance and auto-loading.
|
||||
*/
|
||||
abstract class BaseLazyListPager<IndividualResponseType : Any, NetworkResponseType : Any>(
|
||||
private val scope: CoroutineScope,
|
||||
private val config: CustomPagerConfig
|
||||
) : BaseLazyPager<IndividualResponseType, NetworkResponseType>(scope = scope, config = config) {
|
||||
|
||||
/** The state object that keeps track of the scroll state in the lazy list. */
|
||||
val state by lazy { LazyListState() }
|
||||
|
||||
/**
|
||||
* Determines the index of the last visible item in the lazy list. Adjusts the value based on
|
||||
* the offset.
|
||||
*
|
||||
* @return The index of the last visible item relative to the currently loaded pages.
|
||||
*/
|
||||
override fun getLastPageVisibleItemIndex(): Int {
|
||||
val lastVisibleItemIndex = state.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
||||
return (lastVisibleItemIndex - offset).coerceAtLeast(0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.rr.utils.custompager
|
||||
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Base class for lazy pagination in Jetpack Compose. This class extends [BasePager] to handle
|
||||
* paginated data loading.
|
||||
*
|
||||
* @param IndividualResponseType The type of individual items in the list.
|
||||
* @param NetworkResponseType The type of the network response received when loading data.
|
||||
* @param scope [CoroutineScope] used to observe scroll state and trigger data loading.
|
||||
* @param config Configuration object that determines the pager's behavior, such as prefetch
|
||||
* distance and auto-loading.
|
||||
*/
|
||||
abstract class BaseLazyPager<IndividualResponseType : Any, NetworkResponseType : Any>(
|
||||
private val scope: CoroutineScope,
|
||||
config: CustomPagerConfig
|
||||
) : BasePager<IndividualResponseType, NetworkResponseType>(scope = scope, config = config) {
|
||||
/**
|
||||
* Initializes the pager. If auto-load is enabled in the [config], it will start observing the
|
||||
* scroll state to load pages automatically.
|
||||
*/
|
||||
init {
|
||||
if (config.autoLoadEnabled) observeScrollState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes the scroll state of the lazy grid/list using [snapshotFlow]. When the last visible
|
||||
* item is near the end, it triggers loading the next page.
|
||||
*/
|
||||
private fun observeScrollState() {
|
||||
scope.launch {
|
||||
snapshotFlow { getLastPageVisibleItemIndex() }
|
||||
.collect { lastPageVisibleItem ->
|
||||
if (lastPageVisibleItem != 0) {
|
||||
handleScrollState(lastPageVisibleItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getLastPageVisibleItemIndex(): Int
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.rr.utils.custompager
|
||||
|
||||
import com.navi.common.checkmate.model.MetricInfo
|
||||
import com.navi.common.network.models.RepoResult
|
||||
import com.navi.common.utils.isValidResponse
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Base class for a custom pager implementation that supports paginated data loading.
|
||||
*
|
||||
* @param IndividualResponseType The type of each item in the list.
|
||||
* @param NetworkResponseType The type of the response received from the network.
|
||||
* @param scope CoroutineScope used to launch coroutines for data loading operations.
|
||||
* @param config Configuration parameters for the pager such as prefetch distance.
|
||||
*/
|
||||
abstract class BasePager<IndividualResponseType : Any, NetworkResponseType : Any>(
|
||||
private val scope: CoroutineScope,
|
||||
private val config: CustomPagerConfig
|
||||
) {
|
||||
protected val DEFAULT_PAGE_SIZE: Int = 10
|
||||
protected val DEFAULT_START_KEY: Int = 0
|
||||
|
||||
protected var pageSize: Int = DEFAULT_PAGE_SIZE
|
||||
protected var pageNumber: Int = DEFAULT_START_KEY
|
||||
|
||||
/** Offset for calculating the last visible item index of the last fetched page */
|
||||
protected var offset = 0
|
||||
|
||||
private val isReadyToLoad: Boolean
|
||||
get() = pagerItemState.value == PagerItemState.Loaded
|
||||
|
||||
/**
|
||||
* MutableStateFlow representing the current state of the pager item (Idle, Loading, Loaded,
|
||||
* Error, EndOfList)
|
||||
*/
|
||||
protected val pagerItemState = MutableStateFlow<PagerItemState>(PagerItemState.Idle)
|
||||
|
||||
/** MutableStateFlow holding the list of items that have been loaded */
|
||||
protected val pagerList = MutableStateFlow<MutableList<IndividualResponseType>>(mutableListOf())
|
||||
|
||||
/**
|
||||
* Combines the list of items and pager item state into a [PagerStateHolder]. Provides a stream
|
||||
* of pager states and the list of loaded items.
|
||||
*/
|
||||
val pagerStates: StateFlow<PagerStateHolder<IndividualResponseType>> =
|
||||
combine(
|
||||
pagerList,
|
||||
pagerItemState,
|
||||
) { pagingItems, pagerItemState ->
|
||||
PagerStateHolder(
|
||||
pagingItems = pagingItems,
|
||||
pagerLoadingItemState = pagerItemState,
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue =
|
||||
PagerStateHolder(
|
||||
pagingItems = pagerList.value,
|
||||
pagerLoadingItemState = pagerItemState.value,
|
||||
)
|
||||
)
|
||||
|
||||
init {
|
||||
if (config.triggerInitialLoad || config.autoLoadEnabled) startInitialLoading()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the scroll state and triggers loading of the next page if necessary.
|
||||
*
|
||||
* @param lastPageVisibleItemIndex The index of the last visible item in the list.
|
||||
*/
|
||||
protected fun handleScrollState(lastPageVisibleItemIndex: Int) {
|
||||
if (shouldLoadNextPage(lastPageVisibleItemIndex)) {
|
||||
loadNextPage()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the next page should be loaded based on the scroll state.
|
||||
*
|
||||
* @param lastVisibleItemIndex The index of the last visible item of the last page fetched.
|
||||
* @return true if the next page should be loaded, false otherwise.
|
||||
*/
|
||||
private fun shouldLoadNextPage(lastVisibleItemIndex: Int): Boolean {
|
||||
if (config.prefetchDistance == 0) return false
|
||||
val scrollConditionSatisfied = (lastVisibleItemIndex >= config.prefetchDistance)
|
||||
return scrollConditionSatisfied && isReadyToLoad
|
||||
}
|
||||
|
||||
/** Loads the next page of data. Can be overridden to provide custom load behavior. */
|
||||
open fun loadNextPage() {
|
||||
if (pagerStates.value.pagerLoadingItemState == PagerItemState.Loading) return
|
||||
scope.launch { load() }
|
||||
}
|
||||
|
||||
/** Retries loading the next page in case of an error. */
|
||||
open fun retry() {
|
||||
loadNextPage()
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads data for the next page. This function is responsible for managing the loading, success,
|
||||
* error, and end-of-list states.
|
||||
*/
|
||||
private suspend fun load() {
|
||||
handleLoadingState()
|
||||
updateState(PagerItemState.Loading)
|
||||
val response = fetchData(pageNumber, pageSize, metricInfo)
|
||||
if (response.isValidResponse()) {
|
||||
pageNumber++
|
||||
handleSuccessData(response.data)
|
||||
updateState(PagerItemState.Loaded)
|
||||
handleIfEndOfListState(response.data)
|
||||
performPostLoadActions(response.data)
|
||||
} else {
|
||||
handleErrorState(response.data)
|
||||
updateState(PagerItemState.Error("Something went wrong"))
|
||||
}
|
||||
}
|
||||
|
||||
protected fun updateState(pagerItem: PagerItemState) {
|
||||
pagerItemState.update { pagerItem }
|
||||
}
|
||||
|
||||
protected fun handleLoadingState() {
|
||||
pagerItemState.update { PagerItemState.Loading }
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract function to fetch the page data from the network. Must be implemented by subclasses.
|
||||
*
|
||||
* @param pageNumber The current page number.
|
||||
* @param pageSize The number of items per page.
|
||||
* @param metricInfo Metric information to track the pager's performance.
|
||||
* @return [RepoResult] containing the network response.
|
||||
*/
|
||||
abstract suspend fun fetchData(
|
||||
pageNumber: Int,
|
||||
pageSize: Int,
|
||||
metricInfo: MetricInfo<RepoResult<NetworkResponseType>>,
|
||||
): RepoResult<NetworkResponseType>
|
||||
|
||||
protected open fun handleSuccessData(data: NetworkResponseType?) {
|
||||
offset = pagerList.value.size // Update offset to reflect the size of the list
|
||||
}
|
||||
|
||||
protected open fun handleIfEndOfListState(data: NetworkResponseType?) {
|
||||
if (isEndOfTotalList()) {
|
||||
updateState(PagerItemState.EndOfList)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun handleErrorState(data: NetworkResponseType?)
|
||||
|
||||
open fun refresh() {
|
||||
scope.launch {
|
||||
offset = 0
|
||||
pageNumber = DEFAULT_START_KEY
|
||||
pagerItemState.update { PagerItemState.Idle } // Reset to idle before reloading
|
||||
pagerList.update { mutableListOf() }
|
||||
load()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startInitialLoading() {
|
||||
loadNextPage()
|
||||
}
|
||||
|
||||
/**
|
||||
* Metric information to track the pager's performance, such as the number of pages or items
|
||||
* loaded.
|
||||
*/
|
||||
abstract val metricInfo: MetricInfo<RepoResult<NetworkResponseType>>
|
||||
|
||||
/**
|
||||
* Performs any post-load actions after the data has been loaded. Can be overridden to provide
|
||||
*/
|
||||
abstract fun performPostLoadActions(data: NetworkResponseType?)
|
||||
|
||||
abstract fun isEndOfTotalList(): Boolean
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.rr.utils.custompager
|
||||
|
||||
data class CustomPagerConfig(
|
||||
val triggerInitialLoad: Boolean = true,
|
||||
var autoLoadEnabled: Boolean = false,
|
||||
val prefetchDistance: Int = 5,
|
||||
)
|
||||
|
||||
data class PagerStateHolder<T>(
|
||||
val pagingItems: MutableList<T>,
|
||||
val pagerLoadingItemState: PagerItemState,
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.navi.rr.utils.custompager
|
||||
|
||||
sealed interface PagerItemState {
|
||||
object Loaded : PagerItemState
|
||||
|
||||
object Loading : PagerItemState
|
||||
|
||||
object EndOfList : PagerItemState
|
||||
|
||||
class Error(message: String) : PagerItemState
|
||||
|
||||
object Idle : PagerItemState
|
||||
}
|
||||
@@ -19,3 +19,7 @@ fun <T> List<T>.thirdOrNull(): T? {
|
||||
}
|
||||
|
||||
fun <T> T?.toJson(): String = getGsonBuilders().toJson(this).orElse("{}")
|
||||
|
||||
fun Int.isEven(): Boolean = this % 2 == 0
|
||||
|
||||
fun Int.isOdd(): Boolean = this % 2 != 0
|
||||
|
||||
Reference in New Issue
Block a user