NTP-41124 | Selective refresh revamp. (#15073)

Signed-off-by: Naman Khurmi <naman.khurmi@navi.com>
This commit is contained in:
Naman Khurmi
2025-02-25 12:59:45 +05:30
committed by GitHub
parent 712712b7ee
commit d4e922c656
11 changed files with 423 additions and 292 deletions

View File

@@ -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) {

View File

@@ -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())

View File

@@ -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
}

View File

@@ -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,
}

View File

@@ -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?,

View File

@@ -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"
}
}

View File

@@ -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))
}
}
}

View File

@@ -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
)

View File

@@ -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(