NTP-40266 | Revamp for home content deserialization (#15038)
Signed-off-by: Naman Khurmi <naman.khurmi@navi.com>
This commit is contained in:
@@ -129,7 +129,7 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += ["-Xstring-concat=inline"]
|
||||
freeCompilerArgs += ["-Xstring-concat=inline","-Xcontext-receivers"]
|
||||
jvmTarget = '17'
|
||||
}
|
||||
lint {
|
||||
|
||||
@@ -87,7 +87,7 @@ class HomeReducer : BaseReducer<HpStates, HpEvents> {
|
||||
is HpEvents.UpdateFrontLayerContent -> {
|
||||
val updatedList =
|
||||
updateScreenContent(
|
||||
renderingFirstTime = previousState.isRenderingFirstTime,
|
||||
renderingFirstTime = event.isRenderingFirstTime,
|
||||
newWidgets = event.content,
|
||||
oldWidgets = previousState.frontLayerContent,
|
||||
)
|
||||
@@ -100,7 +100,7 @@ class HomeReducer : BaseReducer<HpStates, HpEvents> {
|
||||
is HpEvents.UpdatePrioritySectionData -> {
|
||||
val updatedList =
|
||||
updateScreenContent(
|
||||
renderingFirstTime = previousState.isRenderingFirstTime,
|
||||
renderingFirstTime = true,
|
||||
newWidgets =
|
||||
event.prioritySectionData.content ?: previousState.frontLayerContent,
|
||||
oldWidgets = previousState.frontLayerContent,
|
||||
@@ -157,7 +157,8 @@ sealed interface HpEvents : UiEvent {
|
||||
data object RenderedFirstTime : HpEvents
|
||||
|
||||
data class UpdateFrontLayerContent(
|
||||
val content: List<AlchemistWidgetModelDefinition<UiTronResponse>>
|
||||
val content: List<AlchemistWidgetModelDefinition<UiTronResponse>>,
|
||||
val isRenderingFirstTime: Boolean,
|
||||
) : HpEvents
|
||||
|
||||
data class UpdatePrioritySectionData(val prioritySectionData: HomePrioritySectionData) :
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.naviapp.home.usecase
|
||||
|
||||
import com.navi.analytics.utils.NaviTrackEvent
|
||||
import com.navi.base.cache.model.NaviCacheAltSourceEntity
|
||||
import com.navi.base.cache.model.NaviCacheEntity
|
||||
import com.navi.base.cache.model.NaviCacheEntityInfo
|
||||
import com.navi.base.cache.repository.NaviCacheRepositoryImpl
|
||||
import com.navi.base.cache.util.NaviSharedDbKeys
|
||||
import com.navi.common.alchemist.model.AlchemistScreenDefinition
|
||||
import com.navi.common.utils.safeAsync
|
||||
import com.navi.common.utils.safeLaunch
|
||||
import com.naviapp.BuildConfig
|
||||
import com.naviapp.home.reducer.HpEvents
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
class HomeContentProcessingUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val naviCacheRepository: NaviCacheRepositoryImpl,
|
||||
private val deserializer: AsyncDeserialization,
|
||||
) {
|
||||
companion object {
|
||||
private const val EVENT_NAME = "naviapp_home_content_processing_event"
|
||||
private const val EVENT_KEY_CURRENT_STATUS = "currentStatus"
|
||||
|
||||
// Cache related events
|
||||
private const val CACHE_RETRIEVAL_NO_CONTENT = "Cache retrieval - No cached content found"
|
||||
private const val CACHE_CONTENT_PROCESSING_START = "Starting cache content processing"
|
||||
private const val CACHE_PRIORITY_SECTION_EXTRACTED = "Priority section extracted"
|
||||
private const val CACHE_SCREEN_LAYOUT_PROCESSED = "Screen layout processed"
|
||||
private const val CACHE_REMAINING_CONTENT_PROCESSED = "Remaining content processed"
|
||||
private const val CACHE_CONTENT_PROCESSING_COMPLETED = "Cache content processing completed"
|
||||
private const val CACHE_DATA_NOT_FOUND = "Home cache data not found during first launch"
|
||||
private const val CACHE_DATA_LOADED =
|
||||
"Home cache data loaded successfully during first launch"
|
||||
private const val CACHE_DATA_RETRIEVAL_START = "Starting cache data retrieval"
|
||||
|
||||
// Network related events
|
||||
private const val NETWORK_FETCH_START = "Starting network data fetch"
|
||||
private const val NETWORK_SUBSEQUENT_FETCH_START = "Starting subsequent network fetch"
|
||||
private const val NETWORK_RESPONSE_NULL = "Network response is null - Context: %s"
|
||||
private const val NETWORK_ERROR = "Network error - Context: %s"
|
||||
private const val NETWORK_SUCCESS = "Network success - Context: %s"
|
||||
|
||||
// Sync related events
|
||||
private const val SYNC_STARTED = "Home sync started - First time render: %s"
|
||||
private const val SYNC_CACHE_OPERATION_COMPLETED =
|
||||
"Cache operation completed - Data loaded: %s"
|
||||
private const val SYNC_FIRST_RENDER_NETWORK = "First render completed from network data"
|
||||
private const val SYNC_REFRESH_CONTENT =
|
||||
"Refreshing content - First render: %s, Widgets: %d"
|
||||
}
|
||||
|
||||
private fun statusTrackEvent(event: String) {
|
||||
NaviTrackEvent.trackEvent(
|
||||
EVENT_NAME,
|
||||
eventValues = mapOf(EVENT_KEY_CURRENT_STATUS to event),
|
||||
)
|
||||
}
|
||||
|
||||
private fun statusTrackEventWithContext(eventTemplate: String, vararg args: Any) {
|
||||
statusTrackEvent(eventTemplate.format(*args))
|
||||
}
|
||||
|
||||
private fun refreshHomeScreenContent(
|
||||
screenDefinition: AlchemistScreenDefinition,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
isRenderingFirstTime: Boolean,
|
||||
) {
|
||||
eventHandler(HpEvents.UpdateScreenWithoutContent(screenDefinition))
|
||||
|
||||
screenDefinition.screenStructure?.content?.widgets?.let { homeWidgets ->
|
||||
eventHandler(HpEvents.UpdateFrontLayerContent(homeWidgets, isRenderingFirstTime))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun retrieveHomeCacheData(
|
||||
coroutineScope: CoroutineScope,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
onCompletion: (NaviCacheEntity?) -> Unit,
|
||||
) {
|
||||
val cachedHomeContent = naviCacheRepository.get(NaviSharedDbKeys.HOME_TAB.name)
|
||||
|
||||
if (cachedHomeContent == null) {
|
||||
statusTrackEvent(CACHE_RETRIEVAL_NO_CONTENT)
|
||||
onCompletion(null)
|
||||
return
|
||||
}
|
||||
|
||||
statusTrackEvent(CACHE_CONTENT_PROCESSING_START)
|
||||
deserializer.setCacheEntity(cachedHomeContent.value)
|
||||
|
||||
val priorityContentSection = deserializer.getPrioritySection()
|
||||
eventHandler(HpEvents.UpdatePrioritySectionData(priorityContentSection))
|
||||
statusTrackEvent(CACHE_PRIORITY_SECTION_EXTRACTED)
|
||||
|
||||
val screenLayoutDeferred =
|
||||
coroutineScope.safeAsync {
|
||||
val screenLayout = deserializer.getScreenDefinitionWithoutContent()
|
||||
statusTrackEvent(CACHE_SCREEN_LAYOUT_PROCESSED)
|
||||
screenLayout?.let {
|
||||
eventHandler(HpEvents.UpdateScreenWithoutContent(screenLayout))
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope
|
||||
.safeAsync {
|
||||
val remainingContent = deserializer.getRemainingContent()
|
||||
statusTrackEvent(CACHE_REMAINING_CONTENT_PROCESSED)
|
||||
remainingContent?.let { content ->
|
||||
screenLayoutDeferred.await()
|
||||
eventHandler(
|
||||
HpEvents.UpdateFrontLayerContent(
|
||||
content = content,
|
||||
isRenderingFirstTime = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.await()
|
||||
|
||||
statusTrackEvent(CACHE_CONTENT_PROCESSING_COMPLETED)
|
||||
onCompletion(cachedHomeContent)
|
||||
}
|
||||
|
||||
context(CoroutineScope)
|
||||
fun synchronizeHomeContent(
|
||||
isFirstTimeRender: Boolean,
|
||||
fetchHomeContent: suspend () -> NaviCacheAltSourceEntity,
|
||||
onContentSynchronized: () -> Unit,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
) {
|
||||
statusTrackEventWithContext(SYNC_STARTED, isFirstTimeRender)
|
||||
|
||||
if (isFirstTimeRender) {
|
||||
handleFirstTimeRender(fetchHomeContent, onContentSynchronized, eventHandler)
|
||||
} else {
|
||||
handleSubsequentFetch(fetchHomeContent, onContentSynchronized, eventHandler)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.handleFirstTimeRender(
|
||||
fetchHomeContent: suspend () -> NaviCacheAltSourceEntity,
|
||||
onContentSynchronized: () -> Unit,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
) {
|
||||
var cacheDataLoaded = false
|
||||
|
||||
val cacheLoadOperation =
|
||||
launchCacheLoad(
|
||||
eventHandler = eventHandler,
|
||||
onContentSynchronized = onContentSynchronized,
|
||||
onCacheLoaded = { cacheDataLoaded = it },
|
||||
)
|
||||
|
||||
launchNetworkFetchAndProcess(
|
||||
fetchHomeContent = fetchHomeContent,
|
||||
cacheLoadOperation = cacheLoadOperation,
|
||||
cacheDataLoaded = cacheDataLoaded,
|
||||
onContentSynchronized = onContentSynchronized,
|
||||
eventHandler = eventHandler,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.handleSubsequentFetch(
|
||||
fetchHomeContent: suspend () -> NaviCacheAltSourceEntity,
|
||||
onContentSynchronized: () -> Unit,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
) {
|
||||
safeLaunch(Dispatchers.IO) {
|
||||
statusTrackEvent(NETWORK_SUBSEQUENT_FETCH_START)
|
||||
val networkResponse = fetchNetworkData(fetchHomeContent)
|
||||
|
||||
processNetworkResponse(
|
||||
networkResponse = networkResponse,
|
||||
contextType = ResponseContext.SUBSEQUENT_FETCH,
|
||||
eventHandler = eventHandler,
|
||||
onSuccess = { screenContent ->
|
||||
screenContent?.let {
|
||||
refreshScreenWithTracking(
|
||||
screenContent = it,
|
||||
eventHandler = eventHandler,
|
||||
isFirstRender = false,
|
||||
onContentSynchronized = onContentSynchronized,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.launchCacheLoad(
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
onContentSynchronized: () -> Unit,
|
||||
onCacheLoaded: (Boolean) -> Unit,
|
||||
): Deferred<Unit?> =
|
||||
safeAsync(Dispatchers.IO) {
|
||||
statusTrackEvent(CACHE_DATA_RETRIEVAL_START)
|
||||
retrieveHomeCacheData(coroutineScope = this, eventHandler = eventHandler) { cachedEntity
|
||||
->
|
||||
if (cachedEntity == null) {
|
||||
statusTrackEvent(CACHE_DATA_NOT_FOUND)
|
||||
onCacheLoaded(false)
|
||||
} else {
|
||||
statusTrackEvent(CACHE_DATA_LOADED)
|
||||
onCacheLoaded(true)
|
||||
onContentSynchronized()
|
||||
eventHandler(HpEvents.RenderedFirstTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.launchNetworkFetchAndProcess(
|
||||
fetchHomeContent: suspend () -> NaviCacheAltSourceEntity,
|
||||
cacheLoadOperation: Deferred<Unit?>,
|
||||
cacheDataLoaded: Boolean,
|
||||
onContentSynchronized: () -> Unit,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
) {
|
||||
safeLaunch(Dispatchers.IO) {
|
||||
statusTrackEvent(NETWORK_FETCH_START)
|
||||
val networkResponse = fetchNetworkData(fetchHomeContent)
|
||||
|
||||
processNetworkResponse(
|
||||
networkResponse = networkResponse,
|
||||
contextType = ResponseContext.FIRST_LAUNCH,
|
||||
eventHandler = eventHandler,
|
||||
onSuccess = { parsedScreenContent ->
|
||||
cacheLoadOperation.await()
|
||||
statusTrackEventWithContext(SYNC_CACHE_OPERATION_COMPLETED, cacheDataLoaded)
|
||||
|
||||
parsedScreenContent?.let {
|
||||
refreshScreenWithTracking(
|
||||
screenContent = it,
|
||||
eventHandler = eventHandler,
|
||||
isFirstRender = !cacheDataLoaded,
|
||||
onContentSynchronized = onContentSynchronized,
|
||||
)
|
||||
if (!cacheDataLoaded) {
|
||||
statusTrackEvent(SYNC_FIRST_RENDER_NETWORK)
|
||||
eventHandler(HpEvents.RenderedFirstTime)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworkData(fetchHomeContent: suspend () -> NaviCacheAltSourceEntity) =
|
||||
naviCacheRepository.fetchFromAltSourceWithSimilarityCheck(
|
||||
key = NaviSharedDbKeys.HOME_TAB.name,
|
||||
version = BuildConfig.VERSION_CODE.toLong(),
|
||||
getDataFromAltSource = fetchHomeContent,
|
||||
)
|
||||
|
||||
private suspend fun processNetworkResponse(
|
||||
networkResponse: NaviCacheEntityInfo?,
|
||||
contextType: ResponseContext,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
onSuccess: suspend (AlchemistScreenDefinition?) -> Unit,
|
||||
) {
|
||||
when {
|
||||
networkResponse == null -> {
|
||||
statusTrackEventWithContext(NETWORK_RESPONSE_NULL, contextType)
|
||||
}
|
||||
networkResponse.isError -> {
|
||||
statusTrackEventWithContext(NETWORK_ERROR, contextType)
|
||||
if (contextType == ResponseContext.FIRST_LAUNCH) {
|
||||
eventHandler(HpEvents.TriggerErrorState)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
statusTrackEventWithContext(NETWORK_SUCCESS, contextType)
|
||||
val parsedContent = networkResponse.data?.let { deserializer.getScreen(it.value) }
|
||||
onSuccess(parsedContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshScreenWithTracking(
|
||||
screenContent: AlchemistScreenDefinition,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
isFirstRender: Boolean,
|
||||
onContentSynchronized: () -> Unit,
|
||||
) {
|
||||
statusTrackEventWithContext(
|
||||
SYNC_REFRESH_CONTENT,
|
||||
isFirstRender,
|
||||
screenContent.screenStructure?.content?.widgets?.size ?: 0,
|
||||
)
|
||||
refreshHomeScreenContent(
|
||||
screenDefinition = screenContent,
|
||||
eventHandler = eventHandler,
|
||||
isRenderingFirstTime = isFirstRender,
|
||||
)
|
||||
onContentSynchronized()
|
||||
}
|
||||
|
||||
private enum class ResponseContext {
|
||||
FIRST_LAUNCH,
|
||||
SUBSEQUENT_FETCH,
|
||||
}
|
||||
}
|
||||
@@ -39,10 +39,10 @@ import com.naviapp.home.reducer.HpEffects
|
||||
import com.naviapp.home.reducer.HpEvents
|
||||
import com.naviapp.home.reducer.HpStates
|
||||
import com.naviapp.home.respository.HomeRepository
|
||||
import com.naviapp.home.usecase.AsyncDeserialization
|
||||
import com.naviapp.home.usecase.FetchHomeItemsUseCase
|
||||
import com.naviapp.home.usecase.HandleCtaUseCase
|
||||
import com.naviapp.home.usecase.HandleUpiUseCase
|
||||
import com.naviapp.home.usecase.HomeContentProcessingUseCase
|
||||
import com.naviapp.models.response.NotificationSettings
|
||||
import com.naviapp.nux.handler.NewUserExperienceHandler
|
||||
import com.naviapp.utils.Constants.HomePageConstants.FETCH_HOME_ITEMS_TIMEOUT
|
||||
@@ -53,13 +53,10 @@ import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeout
|
||||
|
||||
@HiltViewModel
|
||||
@@ -68,7 +65,6 @@ class HomeViewModel
|
||||
constructor(
|
||||
private val homeRepository: HomeRepository,
|
||||
private val naviCacheRepository: NaviCacheRepositoryImpl,
|
||||
private val deserializer: AsyncDeserialization,
|
||||
private val fetchHomeItemsUseCase: FetchHomeItemsUseCase,
|
||||
private val selectiveRefreshHandler: SelectiveRefreshHandler,
|
||||
private val ctaHandler: HandleCtaUseCase,
|
||||
@@ -78,18 +74,17 @@ constructor(
|
||||
val videoViewHelper: Lazy<VideoViewHelper>,
|
||||
val sectionVisibilityTracker: HomePageSectionImpressionTracker,
|
||||
val postRenderTaskExecutor: PostRenderTaskExecutor,
|
||||
private val homeContentProcessingUseCase: HomeContentProcessingUseCase,
|
||||
) :
|
||||
BaseMviViewModel<HpStates, HpEvents, HpEffects>(
|
||||
initialState = HpStates(),
|
||||
reducer = HomeReducer(),
|
||||
) {
|
||||
var shouldRefreshHomeApi: Boolean = false
|
||||
private var homeTabLastUpdateTimestamp: Long = System.currentTimeMillis()
|
||||
private var homePageRefreshJob: Job? = null
|
||||
val naviAnalyticsEventTracker by lazy { NaviAnalytics.naviAnalytics.Home() }
|
||||
private var analyticsStartTs = System.currentTimeMillis()
|
||||
private var isHomeUIRenderedEventLogged = false
|
||||
private val mutex = Mutex()
|
||||
var isErrorNaeTriggered = false
|
||||
|
||||
// Internet Connectivity
|
||||
@@ -132,12 +127,12 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchHomeDataFromApi(naeScreenName: String) =
|
||||
private suspend fun fetchHomeDataFromApi(naeScreenName: String, screenHash: String? = null) =
|
||||
fetchHomeItemsUseCase.fetchHomeItemFromAPI(
|
||||
connectivityObserver = connectivityObserver,
|
||||
availableAppVersionCode =
|
||||
PreferenceManager.getIntPreferenceApp(CURRENT_VERSION_IN_STORE),
|
||||
screenHash = null,
|
||||
screenHash = screenHash,
|
||||
naeScreenName = naeScreenName,
|
||||
onFailure = { errorMessage, errors ->
|
||||
setEffect { HpEffects.OnApiFailure(errorMessage, errors) }
|
||||
@@ -180,11 +175,7 @@ constructor(
|
||||
}
|
||||
naviAnalyticsEventTracker.onHomePageApiCall()
|
||||
analyticsStartTs = System.currentTimeMillis()
|
||||
fetchHomeDataFromCache(
|
||||
availableAppVersionCode =
|
||||
PreferenceManager.getIntPreferenceApp(CURRENT_VERSION_IN_STORE),
|
||||
naeScreenName = naeScreenName,
|
||||
)
|
||||
fetchHomeDataFromCache(naeScreenName = naeScreenName)
|
||||
}
|
||||
|
||||
private fun handleLoggedOutUser(activity: HomePageActivity) {
|
||||
@@ -196,17 +187,25 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun fetchHomeDataFromCache(availableAppVersionCode: Int?, naeScreenName: String) {
|
||||
private fun fetchHomeDataFromCache(naeScreenName: String) {
|
||||
if (homePageRefreshJob?.isActive == true) return
|
||||
homePageRefreshJob =
|
||||
viewModelScope.safeLaunch(Dispatchers.IO) {
|
||||
handleDataModification()
|
||||
val screenHash = state.value.screenMetaData?.get(SCREEN_HASH)
|
||||
fetchAndHandleCacheData(
|
||||
availableAppVersionCode = availableAppVersionCode,
|
||||
screenHash = screenHash,
|
||||
naeScreenName = naeScreenName,
|
||||
)
|
||||
with(viewModelScope) {
|
||||
homeContentProcessingUseCase.synchronizeHomeContent(
|
||||
isFirstTimeRender = state.value.isRenderingFirstTime,
|
||||
eventHandler = { sendEvent(it) },
|
||||
onContentSynchronized = { finishProcessing() },
|
||||
fetchHomeContent = {
|
||||
fetchHomeDataFromApi(
|
||||
naeScreenName = naeScreenName,
|
||||
screenHash = screenHash,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,96 +215,9 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAndHandleCacheData(
|
||||
availableAppVersionCode: Int?,
|
||||
screenHash: String?,
|
||||
naeScreenName: String,
|
||||
) {
|
||||
naviCacheRepository
|
||||
.getDataAndFetchFromAltSourceWithSimilarityCheck(
|
||||
key = NaviSharedDbKeys.HOME_TAB.name,
|
||||
version = BuildConfig.VERSION_CODE.toLong(),
|
||||
getDataFromAltSource = {
|
||||
fetchHomeItemsUseCase.fetchHomeItemFromAPI(
|
||||
connectivityObserver = connectivityObserver,
|
||||
availableAppVersionCode = availableAppVersionCode,
|
||||
screenHash = screenHash,
|
||||
naeScreenName = naeScreenName,
|
||||
onFailure = { errorMessage, errors ->
|
||||
setEffect { HpEffects.OnApiFailure(errorMessage, errors) }
|
||||
},
|
||||
noInternetCallback = {
|
||||
viewModelScope.safeLaunch(Dispatchers.IO) {
|
||||
_internetConnectivity.emit(ConnectivityObserver.Status.Unavailable)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
.collect { response ->
|
||||
viewModelScope.safeLaunch(Dispatchers.IO) {
|
||||
mutex.withLock {
|
||||
if (response.isError) {
|
||||
sendEvent(HpEvents.TriggerErrorState)
|
||||
}
|
||||
response.data?.let { data ->
|
||||
if (state.value.isRenderingFirstTime) {
|
||||
handleFirstLoad(data)
|
||||
} else handleSubsequentLoad(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleFirstLoad(data: NaviCacheEntity) {
|
||||
deserializer.setCacheEntity(data.value)
|
||||
viewModelScope
|
||||
.async {
|
||||
sendEvent(HpEvents.UpdatePrioritySectionData(deserializer.getPrioritySection()))
|
||||
}
|
||||
.await()
|
||||
viewModelScope
|
||||
.async(Dispatchers.IO) {
|
||||
val screenDefinitionWithoutContentDeferred = launch {
|
||||
deserializer.getScreenDefinitionWithoutContent()?.let { screen ->
|
||||
sendEvent(HpEvents.UpdateScreenWithoutContent(screen))
|
||||
}
|
||||
}
|
||||
launch {
|
||||
deserializer.getRemainingContent()?.let { remainingItems ->
|
||||
screenDefinitionWithoutContentDeferred.invokeOnCompletion {
|
||||
sendEvent(HpEvents.UpdateFrontLayerContent(remainingItems))
|
||||
sendEvent(HpEvents.RenderedFirstTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.await()
|
||||
finishProcessing(data)
|
||||
}
|
||||
|
||||
private suspend fun handleSubsequentLoad(data: NaviCacheEntity) {
|
||||
viewModelScope
|
||||
.async {
|
||||
val screen = deserializer.getScreen(data.value)
|
||||
screen.let {
|
||||
sendEvent(HpEvents.UpdateScreenWithoutContent(it))
|
||||
it.screenStructure?.let { struct ->
|
||||
struct.content?.widgets?.let { widgets ->
|
||||
sendEvent(HpEvents.UpdateFrontLayerContent(widgets))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.await()
|
||||
finishProcessing(data)
|
||||
}
|
||||
|
||||
private fun finishProcessing(data: NaviCacheEntity) {
|
||||
private fun finishProcessing() {
|
||||
setEffect { HpEffects.OnRenderActions }
|
||||
updateHomeApiTimestamp()
|
||||
data.updatedAt.let { homeTabLastUpdateTimestamp = it }
|
||||
selectiveRefreshHandler.handleSuccessState(this@HomeViewModel)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.navi.base.cache.dao.NaviCacheDao
|
||||
import com.navi.base.cache.model.NaviCacheAltSourceEntity
|
||||
import com.navi.base.cache.model.NaviCacheEntity
|
||||
import com.navi.base.cache.model.NaviCacheEntityInfo
|
||||
import com.navi.base.utils.orTrue
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -55,7 +54,7 @@ interface NaviCacheRepository {
|
||||
emitOnFailure: Boolean = false,
|
||||
): Flow<NaviCacheEntity?>
|
||||
|
||||
fun getDataAndFetchFromAltSourceWithSimilarityCheck(
|
||||
suspend fun fetchFromAltSourceWithSimilarityCheck(
|
||||
key: String,
|
||||
version: Long? = null,
|
||||
getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity,
|
||||
@@ -67,7 +66,7 @@ interface NaviCacheRepository {
|
||||
|
||||
return currentData.version == altData.version && currentDataValue == altDataValue
|
||||
},
|
||||
): Flow<NaviCacheEntityInfo?>
|
||||
): NaviCacheEntityInfo?
|
||||
|
||||
suspend fun getLastUpdatedTime(key: String): Long
|
||||
}
|
||||
@@ -214,30 +213,22 @@ class NaviCacheRepositoryImpl @Inject constructor(private val naviCacheDao: Navi
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDataAndFetchFromAltSourceWithSimilarityCheck(
|
||||
override suspend fun fetchFromAltSourceWithSimilarityCheck(
|
||||
key: String,
|
||||
version: Long?,
|
||||
getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity,
|
||||
isCurrentAndAltDataSame:
|
||||
(currentData: NaviCacheEntity?, altData: NaviCacheAltSourceEntity) -> Boolean,
|
||||
) = flow {
|
||||
): NaviCacheEntityInfo? {
|
||||
val currentValueInDB = get(key = key)
|
||||
|
||||
if (currentValueInDB != null) { // Its a valid value
|
||||
emit(
|
||||
NaviCacheEntityInfo(
|
||||
data = currentValueInDB,
|
||||
isComingFromAltSource = false,
|
||||
isError = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val entityFromAltSource = getDataFromAltSource.invoke()
|
||||
|
||||
if (entityFromAltSource.isSuccess.not() || entityFromAltSource.value.isNullOrEmpty()) {
|
||||
if (currentValueInDB?.value.isNullOrEmpty().orTrue()) {
|
||||
emit(NaviCacheEntityInfo(isError = true, data = null))
|
||||
return if (currentValueInDB == null) {
|
||||
NaviCacheEntityInfo(isError = true)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
val naviCacheEntity =
|
||||
@@ -254,14 +245,9 @@ class NaviCacheRepositoryImpl @Inject constructor(private val naviCacheDao: Navi
|
||||
|
||||
if (isDataSame.not()) {
|
||||
save(naviCacheEntity = naviCacheEntity)
|
||||
emit(
|
||||
NaviCacheEntityInfo(
|
||||
data = naviCacheEntity,
|
||||
isError = false,
|
||||
isComingFromAltSource = true,
|
||||
)
|
||||
)
|
||||
return NaviCacheEntityInfo(data = naviCacheEntity, isComingFromAltSource = true)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,10 +87,14 @@ import java.util.Locale
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.time.Duration
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Request
|
||||
import org.json.JSONObject
|
||||
@@ -652,6 +656,26 @@ fun CoroutineScope.safeLaunch(
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> CoroutineScope.safeAsync(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> T,
|
||||
): Deferred<T?> {
|
||||
val exceptionHandler = CoroutineExceptionHandler { _, throwable -> throwable.log() }
|
||||
|
||||
return async(exceptionHandler + context, start) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: CancellationException) {
|
||||
e.log()
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
e.log()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.startEnterAnimation() {
|
||||
this.overridePendingTransition(R.anim.parallax_slide_in_right, R.anim.parallax_slide_out_left)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user