NTP-41124 | Selective refresh revamp. (#15073)
Signed-off-by: Naman Khurmi <naman.khurmi@navi.com>
This commit is contained in:
@@ -57,7 +57,8 @@ constructor(
|
||||
is HpEffects.OnActionsFromJson -> handleActionsFromJson(effects, homeVM)
|
||||
is HpEffects.TrackBottomBarEvents -> trackBottomBarEvents()
|
||||
is HpEffects.OnNotificationUpdatedCount -> handleNotificationCount(effects, homeVM)
|
||||
is HpEffects.OnRenderActions -> handleRenderActions(homeVM)
|
||||
is HpEffects.OnPostRenderActions -> handlePostRenderActions(homeVM)
|
||||
is HpEffects.OnApiSuccessRenderActions -> handleApiSuccessRenderActions(homeVM)
|
||||
is HpEffects.OnApiFailure -> handleApiFailure(effects, homeVM)
|
||||
is HpEffects.LogAppLaunchTime -> logAppLaunchTime(effects)
|
||||
is HpEffects.InitiatePayment -> onPaymentInitiated(effects.data)
|
||||
@@ -148,11 +149,12 @@ constructor(
|
||||
?.let { homeVM.handleAction(it) }
|
||||
}
|
||||
|
||||
private fun handleRenderActions(homeVM: HomeViewModel) {
|
||||
homeVM.state.value.renderActions?.let { renderActions ->
|
||||
renderActions.postRenderAction?.let { homeVM.handleActions(it) }
|
||||
renderActions.apiSuccessRenderAction?.let { homeVM.handleActions(it) }
|
||||
}
|
||||
private fun handlePostRenderActions(homeVM: HomeViewModel) {
|
||||
homeVM.state.value.renderActions?.postRenderAction?.let { homeVM.handleActions(it) }
|
||||
}
|
||||
|
||||
private fun handleApiSuccessRenderActions(homeVM: HomeViewModel) {
|
||||
homeVM.state.value.renderActions?.apiSuccessRenderAction.let { homeVM.handleActions(it) }
|
||||
}
|
||||
|
||||
private fun handleApiFailure(effects: HpEffects.OnApiFailure, homeVM: HomeViewModel) {
|
||||
|
||||
@@ -48,7 +48,6 @@ fun InitLifecycleListener(homeVM: () -> HomeViewModel, activity: HomePageActivit
|
||||
}
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
if (homeVM().shouldRefreshHomeApi) {
|
||||
homeVM().setEffect { HpEffects.OnRenderActions }
|
||||
homeVM().setEffect { HpEffects.FetchHomeApi }
|
||||
}
|
||||
resetImpressionStates(homeVM())
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024-2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.naviapp.home.model
|
||||
|
||||
import com.navi.base.cache.model.NaviCacheEntity
|
||||
|
||||
sealed interface HomeScreenContentResult {
|
||||
data class Success(
|
||||
val data: NaviCacheEntity? = null,
|
||||
val isDataSame: Boolean,
|
||||
val isScreenHashSame: Boolean = false,
|
||||
) : HomeScreenContentResult
|
||||
|
||||
data class Error(val isCacheAvailable: Boolean) : HomeScreenContentResult
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.naviapp.home.model
|
||||
|
||||
enum class SelectiveRefreshState {
|
||||
LOADING,
|
||||
SUCCESS,
|
||||
ERROR,
|
||||
}
|
||||
@@ -207,7 +207,9 @@ sealed interface HpEffects : UiEffect {
|
||||
|
||||
data class OnNotificationUpdatedCount(val count: Int) : HpEffects
|
||||
|
||||
data object OnRenderActions : HpEffects
|
||||
data object OnPostRenderActions : HpEffects
|
||||
|
||||
data object OnApiSuccessRenderActions : HpEffects
|
||||
|
||||
data class OnApiFailure(
|
||||
val errorMessage: ErrorMessage?,
|
||||
|
||||
@@ -17,7 +17,7 @@ import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
|
||||
import com.navi.common.network.models.ErrorMessage
|
||||
import com.navi.common.network.models.GenericErrorResponse
|
||||
import com.navi.common.network.models.RepoResult
|
||||
import com.navi.common.network.models.isSuccess
|
||||
import com.navi.common.network.models.isSuccessWithData
|
||||
import com.naviapp.BuildConfig
|
||||
import com.naviapp.app.NaviApplication
|
||||
import com.naviapp.home.respository.HomeRepository
|
||||
@@ -79,18 +79,55 @@ constructor(
|
||||
onFailure:
|
||||
suspend (apiErrorMessage: ErrorMessage?, errors: List<GenericErrorResponse>?) -> Unit,
|
||||
): NaviCacheAltSourceEntity {
|
||||
return if (connectivityObserver.isInternetConnected()) {
|
||||
response.data?.screenStructure?.let {
|
||||
|
||||
return when {
|
||||
// No internet case
|
||||
!connectivityObserver.isInternetConnected() -> {
|
||||
return handleNoInternet(response, noInternetCallback, onFailure)
|
||||
}
|
||||
// When response is error or has no data
|
||||
!response.isSuccessWithData() -> {
|
||||
onFailure(response.error, response.errors)
|
||||
return NaviCacheAltSourceEntity(isSuccess = false)
|
||||
}
|
||||
// When the response is success but screenStructure is null
|
||||
response.data?.screenStructure == null -> {
|
||||
// Check for the case when screenStructure is present in the response
|
||||
// [Same Screen Hash Case]
|
||||
if (response.data?.screenMetaData != null) {
|
||||
NaviCacheAltSourceEntity(isSuccess = true)
|
||||
} else {
|
||||
onFailure(
|
||||
response.error?.copy(message = SCREEN_STRUCTURE_AND_META_DATA_NULL),
|
||||
response.errors,
|
||||
)
|
||||
return NaviCacheAltSourceEntity(isSuccess = false)
|
||||
}
|
||||
}
|
||||
// When the response is success with Data
|
||||
else -> {
|
||||
NaviCacheAltSourceEntity(
|
||||
value = dataSerializers.toJson(response.data),
|
||||
value = response.data?.let { dataSerializers.toJson(it) },
|
||||
version = BuildConfig.VERSION_CODE,
|
||||
isSuccess = response.isSuccess(),
|
||||
isSuccess = true,
|
||||
)
|
||||
} ?: NaviCacheAltSourceEntity(isSuccess = false)
|
||||
} else {
|
||||
noInternetCallback()
|
||||
onFailure(response.error, response.errors)
|
||||
NaviCacheAltSourceEntity(isSuccess = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleNoInternet(
|
||||
response: RepoResult<AlchemistScreenDefinition>,
|
||||
noInternetCallback: () -> Unit,
|
||||
onFailure:
|
||||
suspend (apiErrorMessage: ErrorMessage?, errors: List<GenericErrorResponse>?) -> Unit,
|
||||
): NaviCacheAltSourceEntity {
|
||||
noInternetCallback()
|
||||
onFailure(response.error, response.errors)
|
||||
return NaviCacheAltSourceEntity(isSuccess = false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SCREEN_STRUCTURE_AND_META_DATA_NULL =
|
||||
"Both screen structure and meta data is null"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@ 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.model.HomeScreenContentResult
|
||||
import com.naviapp.home.model.SelectiveRefreshState
|
||||
import com.naviapp.home.reducer.HpEvents
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -31,60 +31,181 @@ constructor(
|
||||
) {
|
||||
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"
|
||||
// Event Types
|
||||
private const val EVENT_TYPE = "event_type"
|
||||
private const val TYPE_SYNC_START = "sync_start"
|
||||
private const val TYPE_FIRST_TIME_SYNC = "first_time_sync"
|
||||
private const val TYPE_INCREMENTAL_SYNC = "incremental_sync"
|
||||
private const val TYPE_CACHE_LOAD = "cache_load"
|
||||
private const val TYPE_CACHE_PROCESS = "cache_process"
|
||||
private const val TYPE_NETWORK_SYNC = "network_sync"
|
||||
private const val TYPE_CONTENT_COMPARE = "content_compare"
|
||||
private const val TYPE_SCREEN_UPDATE = "screen_update"
|
||||
private const val TYPE_NETWORK_ERROR = "network_error"
|
||||
|
||||
// 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"
|
||||
// Event Parameters
|
||||
private const val PARAM_IS_FIRST_TIME = "is_first_time"
|
||||
private const val PARAM_CACHE_STATUS = "cache_status"
|
||||
private const val PARAM_IS_DATA_SAME = "is_data_same"
|
||||
private const val PARAM_SYNC_STATUS = "sync_status"
|
||||
private const val PARAM_ERROR_TYPE = "error_type"
|
||||
private const val PARAM_CONTENT_STATUS = "content_status"
|
||||
|
||||
// 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"
|
||||
// Event Values
|
||||
private const val VALUE_NOT_FOUND = "not_found"
|
||||
private const val VALUE_LOADED = "loaded"
|
||||
private const val VALUE_HASH_UNCHANGED = "hash_unchanged"
|
||||
private const val VALUE_EMPTY_CONTENT = "empty_content"
|
||||
private const val VALUE_CONTENT_UPDATED = "content_updated"
|
||||
private const val VALUE_PRIORITY_SECTION_UPDATED = "priority_section_updated"
|
||||
private const val VALUE_SCREEN_LAYOUT_UPDATED = "screen_layout_updated"
|
||||
private const val VALUE_REMAINING_CONTENT_UPDATED = "remaining_content_updated"
|
||||
private const val VALUE_INCREMENTAL_SYNC = "incremental_sync"
|
||||
private const val VALUE_NETWORK_SYNC = "network_sync"
|
||||
private const val VALUE_ALT_SOURCE_FETCH = "alt_source_fetch"
|
||||
}
|
||||
|
||||
private fun statusTrackEvent(event: String) {
|
||||
NaviTrackEvent.trackEvent(
|
||||
EVENT_NAME,
|
||||
eventValues = mapOf(EVENT_KEY_CURRENT_STATUS to event),
|
||||
)
|
||||
private fun trackEvent(type: String, params: Map<String, String> = emptyMap()) {
|
||||
val eventParams = mutableMapOf<String, String>()
|
||||
eventParams[EVENT_TYPE] = type
|
||||
eventParams.putAll(params)
|
||||
NaviTrackEvent.trackEvent(EVENT_NAME, eventParams)
|
||||
}
|
||||
|
||||
private fun statusTrackEventWithContext(eventTemplate: String, vararg args: Any) {
|
||||
statusTrackEvent(eventTemplate.format(*args))
|
||||
}
|
||||
|
||||
private fun refreshHomeScreenContent(
|
||||
screenDefinition: AlchemistScreenDefinition,
|
||||
context(CoroutineScope)
|
||||
fun syncHomeContentData(
|
||||
isFirstTimeRender: Boolean,
|
||||
fetchHomeContent: suspend () -> NaviCacheAltSourceEntity,
|
||||
onContentSynchronized: () -> Unit,
|
||||
onHomeFetchSuccessFromNetwork: () -> Unit,
|
||||
onSelectiveRefreshStateChange: (SelectiveRefreshState) -> Unit,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
isRenderingFirstTime: Boolean,
|
||||
) {
|
||||
eventHandler(HpEvents.UpdateScreenWithoutContent(screenDefinition))
|
||||
trackEvent(TYPE_SYNC_START, mapOf(PARAM_IS_FIRST_TIME to isFirstTimeRender.toString()))
|
||||
|
||||
screenDefinition.screenStructure?.content?.widgets?.let { homeWidgets ->
|
||||
eventHandler(HpEvents.UpdateFrontLayerContent(homeWidgets, isRenderingFirstTime))
|
||||
if (isFirstTimeRender) {
|
||||
initializeFirstTimeContentSync(
|
||||
fetchHomeContent,
|
||||
onContentSynchronized,
|
||||
eventHandler,
|
||||
onSelectiveRefreshStateChange,
|
||||
onHomeFetchSuccessFromNetwork,
|
||||
)
|
||||
} else {
|
||||
performIncrementalContentSync(
|
||||
fetchHomeContent,
|
||||
onContentSynchronized,
|
||||
eventHandler,
|
||||
onSelectiveRefreshStateChange,
|
||||
onHomeFetchSuccessFromNetwork,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun retrieveHomeCacheData(
|
||||
private fun CoroutineScope.initializeFirstTimeContentSync(
|
||||
fetchHomeContent: suspend () -> NaviCacheAltSourceEntity,
|
||||
onContentSynchronized: () -> Unit,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
onSelectiveRefreshStateChange: (SelectiveRefreshState) -> Unit,
|
||||
onHomeFetchSuccessFromNetwork: () -> Unit,
|
||||
) {
|
||||
var cacheDataLoaded = false
|
||||
trackEvent(TYPE_FIRST_TIME_SYNC)
|
||||
|
||||
val cacheLoadOperation =
|
||||
initializeCacheDataLoad(
|
||||
eventHandler = eventHandler,
|
||||
onContentSynchronized = onContentSynchronized,
|
||||
onCacheLoaded = { cacheDataLoaded = it },
|
||||
)
|
||||
|
||||
initiateNetworkSyncAndProcess(
|
||||
fetchHomeContent = fetchHomeContent,
|
||||
cacheLoadOperation = cacheLoadOperation,
|
||||
cacheDataLoaded = cacheDataLoaded,
|
||||
onContentSynchronized = onContentSynchronized,
|
||||
eventHandler = eventHandler,
|
||||
onSelectiveRefreshStateChange = onSelectiveRefreshStateChange,
|
||||
onHomeFetchSuccessFromNetwork = onHomeFetchSuccessFromNetwork,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.performIncrementalContentSync(
|
||||
fetchHomeContent: suspend () -> NaviCacheAltSourceEntity,
|
||||
onContentSynchronized: () -> Unit,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
onSelectiveRefreshStateChange: (SelectiveRefreshState) -> Unit,
|
||||
onHomeFetchSuccessFromNetwork: () -> Unit,
|
||||
) {
|
||||
trackEvent(TYPE_INCREMENTAL_SYNC)
|
||||
|
||||
safeLaunch(Dispatchers.IO) {
|
||||
val networkResponse =
|
||||
fetchAndCompareAltSourceContent(
|
||||
key = NaviSharedDbKeys.HOME_TAB.name,
|
||||
getDataFromAltSource = fetchHomeContent,
|
||||
)
|
||||
|
||||
handleNetworkSyncResponse(
|
||||
networkResponse = networkResponse,
|
||||
eventHandler = eventHandler,
|
||||
onNetworkError = {
|
||||
trackEvent(
|
||||
TYPE_NETWORK_ERROR,
|
||||
mapOf(PARAM_ERROR_TYPE to VALUE_INCREMENTAL_SYNC),
|
||||
)
|
||||
onSelectiveRefreshStateChange(SelectiveRefreshState.ERROR)
|
||||
},
|
||||
onScreenHashUnchanged = {
|
||||
trackEvent(
|
||||
TYPE_CONTENT_COMPARE,
|
||||
mapOf(PARAM_CONTENT_STATUS to VALUE_HASH_UNCHANGED),
|
||||
)
|
||||
onHomeFetchSuccessFromNetwork()
|
||||
onSelectiveRefreshStateChange(SelectiveRefreshState.SUCCESS)
|
||||
},
|
||||
onScreenContentUpdated = { screenContent, isDataUnchanged ->
|
||||
trackEvent(
|
||||
TYPE_SCREEN_UPDATE,
|
||||
mapOf(PARAM_IS_DATA_SAME to isDataUnchanged.toString()),
|
||||
)
|
||||
updateScreenContent(
|
||||
screenContent = screenContent,
|
||||
eventHandler = eventHandler,
|
||||
isFirstRender = false,
|
||||
)
|
||||
onContentSynchronized()
|
||||
if (!isDataUnchanged) {
|
||||
onHomeFetchSuccessFromNetwork()
|
||||
}
|
||||
onSelectiveRefreshStateChange(SelectiveRefreshState.SUCCESS)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.initializeCacheDataLoad(
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
onContentSynchronized: () -> Unit,
|
||||
onCacheLoaded: (Boolean) -> Unit,
|
||||
): Deferred<Unit?> {
|
||||
trackEvent(TYPE_CACHE_LOAD)
|
||||
return safeAsync(Dispatchers.IO) {
|
||||
loadAndProcessCacheData(coroutineScope = this, eventHandler = eventHandler) {
|
||||
cachedEntity ->
|
||||
val cacheStatus = if (cachedEntity == null) VALUE_NOT_FOUND else VALUE_LOADED
|
||||
trackEvent(TYPE_CACHE_PROCESS, mapOf(PARAM_CACHE_STATUS to cacheStatus))
|
||||
onCacheLoaded(cachedEntity != null)
|
||||
if (cachedEntity != null) {
|
||||
onContentSynchronized()
|
||||
eventHandler(HpEvents.RenderedFirstTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadAndProcessCacheData(
|
||||
coroutineScope: CoroutineScope,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
onCompletion: (NaviCacheEntity?) -> Unit,
|
||||
@@ -92,31 +213,34 @@ constructor(
|
||||
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)
|
||||
trackEvent(
|
||||
TYPE_CACHE_PROCESS,
|
||||
mapOf(PARAM_CONTENT_STATUS to VALUE_PRIORITY_SECTION_UPDATED),
|
||||
)
|
||||
|
||||
val screenLayoutDeferred =
|
||||
coroutineScope.safeAsync {
|
||||
val screenLayout = deserializer.getScreenDefinitionWithoutContent()
|
||||
statusTrackEvent(CACHE_SCREEN_LAYOUT_PROCESSED)
|
||||
screenLayout?.let {
|
||||
eventHandler(HpEvents.UpdateScreenWithoutContent(screenLayout))
|
||||
trackEvent(
|
||||
TYPE_CACHE_PROCESS,
|
||||
mapOf(PARAM_CONTENT_STATUS to VALUE_SCREEN_LAYOUT_UPDATED),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope
|
||||
.safeAsync {
|
||||
val remainingContent = deserializer.getRemainingContent()
|
||||
statusTrackEvent(CACHE_REMAINING_CONTENT_PROCESSED)
|
||||
remainingContent?.let { content ->
|
||||
screenLayoutDeferred.await()
|
||||
eventHandler(
|
||||
@@ -125,189 +249,165 @@ constructor(
|
||||
isRenderingFirstTime = true,
|
||||
)
|
||||
)
|
||||
trackEvent(
|
||||
TYPE_CACHE_PROCESS,
|
||||
mapOf(PARAM_CONTENT_STATUS to VALUE_REMAINING_CONTENT_UPDATED),
|
||||
)
|
||||
}
|
||||
}
|
||||
.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(
|
||||
private fun CoroutineScope.initiateNetworkSyncAndProcess(
|
||||
fetchHomeContent: suspend () -> NaviCacheAltSourceEntity,
|
||||
cacheLoadOperation: Deferred<Unit?>,
|
||||
cacheDataLoaded: Boolean,
|
||||
onContentSynchronized: () -> Unit,
|
||||
onSelectiveRefreshStateChange: (SelectiveRefreshState) -> Unit,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
onHomeFetchSuccessFromNetwork: () -> Unit,
|
||||
) {
|
||||
trackEvent(TYPE_NETWORK_SYNC)
|
||||
safeLaunch(Dispatchers.IO) {
|
||||
statusTrackEvent(NETWORK_FETCH_START)
|
||||
val networkResponse = fetchNetworkData(fetchHomeContent)
|
||||
val networkResponse =
|
||||
fetchAndCompareAltSourceContent(
|
||||
key = NaviSharedDbKeys.HOME_TAB.name,
|
||||
getDataFromAltSource = fetchHomeContent,
|
||||
)
|
||||
|
||||
processNetworkResponse(
|
||||
handleNetworkSyncResponse(
|
||||
networkResponse = networkResponse,
|
||||
contextType = ResponseContext.FIRST_LAUNCH,
|
||||
eventHandler = eventHandler,
|
||||
onSuccess = { parsedScreenContent ->
|
||||
onNetworkError = {
|
||||
trackEvent(TYPE_NETWORK_ERROR, mapOf(PARAM_ERROR_TYPE to VALUE_NETWORK_SYNC))
|
||||
onSelectiveRefreshStateChange(SelectiveRefreshState.ERROR)
|
||||
},
|
||||
onScreenHashUnchanged = {
|
||||
trackEvent(TYPE_NETWORK_SYNC, mapOf(PARAM_SYNC_STATUS to VALUE_HASH_UNCHANGED))
|
||||
onHomeFetchSuccessFromNetwork()
|
||||
onSelectiveRefreshStateChange(SelectiveRefreshState.SUCCESS)
|
||||
},
|
||||
onScreenContentUpdated = { screenContent, isDataUnchanged ->
|
||||
trackEvent(
|
||||
TYPE_NETWORK_SYNC,
|
||||
mapOf(
|
||||
PARAM_SYNC_STATUS to VALUE_CONTENT_UPDATED,
|
||||
PARAM_IS_DATA_SAME to isDataUnchanged.toString(),
|
||||
),
|
||||
)
|
||||
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)
|
||||
}
|
||||
updateScreenContent(
|
||||
screenContent = screenContent,
|
||||
eventHandler = eventHandler,
|
||||
isFirstRender = !cacheDataLoaded,
|
||||
)
|
||||
if (!cacheDataLoaded && screenContent != null) {
|
||||
eventHandler(HpEvents.RenderedFirstTime)
|
||||
}
|
||||
onContentSynchronized()
|
||||
if (!isDataUnchanged) {
|
||||
onHomeFetchSuccessFromNetwork()
|
||||
}
|
||||
onSelectiveRefreshStateChange(SelectiveRefreshState.SUCCESS)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworkData(fetchHomeContent: suspend () -> NaviCacheAltSourceEntity) =
|
||||
naviCacheRepository.fetchFromAltSourceWithSimilarityCheck(
|
||||
key = NaviSharedDbKeys.HOME_TAB.name,
|
||||
version = BuildConfig.VERSION_CODE.toLong(),
|
||||
getDataFromAltSource = fetchHomeContent,
|
||||
)
|
||||
private suspend fun fetchAndCompareAltSourceContent(
|
||||
key: String,
|
||||
getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity,
|
||||
): HomeScreenContentResult {
|
||||
trackEvent(TYPE_CONTENT_COMPARE, mapOf("key" to key))
|
||||
|
||||
private suspend fun processNetworkResponse(
|
||||
networkResponse: NaviCacheEntityInfo?,
|
||||
contextType: ResponseContext,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
onSuccess: suspend (AlchemistScreenDefinition?) -> Unit,
|
||||
) {
|
||||
when {
|
||||
networkResponse == null -> {
|
||||
statusTrackEventWithContext(NETWORK_RESPONSE_NULL, contextType)
|
||||
val entityFromAltSource = getDataFromAltSource.invoke()
|
||||
val currentValueInDB = naviCacheRepository.get(key = key)
|
||||
|
||||
return when {
|
||||
!entityFromAltSource.isSuccess -> {
|
||||
trackEvent(TYPE_NETWORK_ERROR, mapOf(PARAM_ERROR_TYPE to VALUE_ALT_SOURCE_FETCH))
|
||||
HomeScreenContentResult.Error(isCacheAvailable = currentValueInDB != null)
|
||||
}
|
||||
networkResponse.isError -> {
|
||||
statusTrackEventWithContext(NETWORK_ERROR, contextType)
|
||||
if (contextType == ResponseContext.FIRST_LAUNCH) {
|
||||
eventHandler(HpEvents.TriggerErrorState)
|
||||
}
|
||||
|
||||
entityFromAltSource.value.isNullOrEmpty() -> {
|
||||
trackEvent(TYPE_CONTENT_COMPARE, mapOf(PARAM_CONTENT_STATUS to VALUE_EMPTY_CONTENT))
|
||||
HomeScreenContentResult.Success(isDataSame = true, isScreenHashSame = true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
statusTrackEventWithContext(NETWORK_SUCCESS, contextType)
|
||||
val parsedContent = networkResponse.data?.let { deserializer.getScreen(it.value) }
|
||||
onSuccess(parsedContent)
|
||||
val naviCacheEntity = entityFromAltSource.toNaviCacheEntity(key)
|
||||
val isDataSame = compareContentData(currentValueInDB, entityFromAltSource)
|
||||
|
||||
trackEvent(TYPE_CONTENT_COMPARE, mapOf(PARAM_IS_DATA_SAME to isDataSame.toString()))
|
||||
|
||||
HomeScreenContentResult.Success(
|
||||
data = if (isDataSame) null else naviCacheEntity,
|
||||
isDataSame = isDataSame,
|
||||
isScreenHashSame = false,
|
||||
)
|
||||
.also {
|
||||
if (!isDataSame) {
|
||||
trackEvent(
|
||||
TYPE_CONTENT_COMPARE,
|
||||
mapOf(PARAM_CONTENT_STATUS to VALUE_CONTENT_UPDATED),
|
||||
)
|
||||
naviCacheRepository.save(naviCacheEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 fun compareContentData(
|
||||
currentData: NaviCacheEntity?,
|
||||
altData: NaviCacheAltSourceEntity?,
|
||||
): Boolean {
|
||||
val currentDataValue = currentData?.value ?: return false
|
||||
val altDataValue = altData?.value ?: return false
|
||||
return currentData.version == altData.version && currentDataValue == altDataValue
|
||||
}
|
||||
|
||||
private enum class ResponseContext {
|
||||
FIRST_LAUNCH,
|
||||
SUBSEQUENT_FETCH,
|
||||
private suspend fun handleNetworkSyncResponse(
|
||||
networkResponse: HomeScreenContentResult,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
onNetworkError: () -> Unit,
|
||||
onScreenHashUnchanged: () -> Unit,
|
||||
onScreenContentUpdated:
|
||||
suspend (updatedContent: AlchemistScreenDefinition?, isDataUnchanged: Boolean) -> Unit,
|
||||
) {
|
||||
when (networkResponse) {
|
||||
is HomeScreenContentResult.Success -> {
|
||||
if (networkResponse.isScreenHashSame) {
|
||||
onScreenHashUnchanged()
|
||||
} else {
|
||||
val parsedContent =
|
||||
networkResponse.data?.let { deserializer.getScreen(it.value) }
|
||||
onScreenContentUpdated(parsedContent, networkResponse.isDataSame)
|
||||
}
|
||||
}
|
||||
is HomeScreenContentResult.Error -> {
|
||||
if (networkResponse.isCacheAvailable) {
|
||||
onNetworkError()
|
||||
} else {
|
||||
eventHandler(HpEvents.TriggerErrorState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateScreenContent(
|
||||
screenContent: AlchemistScreenDefinition?,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
isFirstRender: Boolean,
|
||||
) {
|
||||
if (screenContent == null) return
|
||||
|
||||
eventHandler(HpEvents.UpdateScreenWithoutContent(screenContent))
|
||||
|
||||
screenContent.screenStructure?.content?.widgets?.let { homeWidgets ->
|
||||
eventHandler(HpEvents.UpdateFrontLayerContent(homeWidgets, isFirstRender))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,16 +194,20 @@ constructor(
|
||||
handleDataModification()
|
||||
val screenHash = state.value.screenMetaData?.get(SCREEN_HASH)
|
||||
with(viewModelScope) {
|
||||
homeContentProcessingUseCase.synchronizeHomeContent(
|
||||
homeContentProcessingUseCase.syncHomeContentData(
|
||||
isFirstTimeRender = state.value.isRenderingFirstTime,
|
||||
eventHandler = { sendEvent(it) },
|
||||
onContentSynchronized = { finishProcessing() },
|
||||
eventHandler = ::sendEvent,
|
||||
onContentSynchronized = ::onContentSynchronized,
|
||||
fetchHomeContent = {
|
||||
fetchHomeDataFromApi(
|
||||
naeScreenName = naeScreenName,
|
||||
screenHash = screenHash,
|
||||
)
|
||||
},
|
||||
onHomeFetchSuccessFromNetwork = ::onHomeFetchSuccessFromNetwork,
|
||||
onSelectiveRefreshStateChange = {
|
||||
selectiveRefreshHandler.updateRefreshState(this@HomeViewModel, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -215,14 +219,13 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishProcessing() {
|
||||
setEffect { HpEffects.OnRenderActions }
|
||||
updateHomeApiTimestamp()
|
||||
selectiveRefreshHandler.handleSuccessState(this@HomeViewModel)
|
||||
private fun onContentSynchronized() {
|
||||
setEffect { HpEffects.OnPostRenderActions }
|
||||
TemporaryStorageHelper.updateApiTs(TemporaryStorageHelper.HOME)
|
||||
}
|
||||
|
||||
private fun updateHomeApiTimestamp() {
|
||||
TemporaryStorageHelper.updateApiTs(TemporaryStorageHelper.HOME)
|
||||
private fun onHomeFetchSuccessFromNetwork() {
|
||||
setEffect { HpEffects.OnApiSuccessRenderActions }
|
||||
naviAnalyticsEventTracker.onHomePageApiResponse(
|
||||
System.currentTimeMillis() - analyticsStartTs
|
||||
)
|
||||
|
||||
@@ -10,10 +10,31 @@ package com.naviapp.utils
|
||||
import com.navi.uitron.model.action.PublishEventAction
|
||||
import com.navi.uitron.model.action.PublishableEventData
|
||||
import com.navi.uitron.viewmodel.UiTronViewModel
|
||||
import com.naviapp.home.model.SelectiveRefreshState
|
||||
import com.naviapp.home.viewmodel.HomeViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
class SelectiveRefreshHandler @Inject constructor() {
|
||||
|
||||
fun updateRefreshState(
|
||||
homeViewModel: HomeViewModel,
|
||||
selectiveRefreshState: SelectiveRefreshState,
|
||||
) {
|
||||
when (selectiveRefreshState) {
|
||||
SelectiveRefreshState.LOADING -> {
|
||||
handleLoadingState(homeViewModel)
|
||||
}
|
||||
|
||||
SelectiveRefreshState.SUCCESS -> {
|
||||
handleSuccessState(homeViewModel)
|
||||
}
|
||||
|
||||
SelectiveRefreshState.ERROR -> {
|
||||
handleErrorState(homeViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSuccessState(viewModel: UiTronViewModel, stateKey: String? = null) {
|
||||
viewModel.handleAction(
|
||||
PublishEventAction(
|
||||
|
||||
Reference in New Issue
Block a user