NTP-66546 | Introduce minimal delay in the rendering process on the HomePage to reduce ANRs and ensure smooth click registration. (#16253)
This commit is contained in:
@@ -79,7 +79,6 @@ import com.navi.common.utils.getDeviceSignature
|
||||
import com.navi.common.utils.getLocalStorageLocation
|
||||
import com.navi.common.utils.getNetworkType
|
||||
import com.navi.common.utils.getScreenRefreshRate
|
||||
import com.navi.common.utils.getTotalRamMemory
|
||||
import com.navi.common.utils.isInstalledInProfile
|
||||
import com.navi.common.utils.logNaviPayGlanceWidgetClick
|
||||
import com.navi.common.utils.observeNonNull
|
||||
@@ -197,6 +196,8 @@ import kotlin.time.Duration.Companion.seconds
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -325,6 +326,7 @@ class HomePageActivity :
|
||||
}
|
||||
}
|
||||
initObservers()
|
||||
homeVM.fetchDelayedRenderingConfig(this@HomePageActivity)
|
||||
handleInitialTouchEventState()
|
||||
initErrors()
|
||||
fetchHomeData()
|
||||
@@ -341,41 +343,32 @@ class HomePageActivity :
|
||||
TraceManager.endAllTraces()
|
||||
}
|
||||
|
||||
private fun shouldDisableInitialTouchEvent(): Boolean {
|
||||
val isTouchDisabled =
|
||||
FirebaseRemoteConfigHelper.getBoolean(
|
||||
FirebaseRemoteConfigHelper.DISABLE_INITIAL_TOUCH_EVENT,
|
||||
false,
|
||||
)
|
||||
val maxRamThresholdGb =
|
||||
FirebaseRemoteConfigHelper.getDouble(
|
||||
FirebaseRemoteConfigHelper.DISABLE_INITIAL_TOUCH_EVENT_MAX_RAM
|
||||
)
|
||||
val deviceRamGb = getTotalRamMemory(this)?.toDoubleOrNull() ?: 0.0
|
||||
|
||||
return isTouchDisabled && deviceRamGb < maxRamThresholdGb
|
||||
}
|
||||
|
||||
private fun handleInitialTouchEventState() {
|
||||
lifecycleScope.launch {
|
||||
if (shouldDisableInitialTouchEvent()) {
|
||||
homeVM.isClickEnabled.collect { isEnabled ->
|
||||
when {
|
||||
!isEnabled -> {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
||||
)
|
||||
naviAnalyticsEventTracker.homeClickDisabled()
|
||||
}
|
||||
combine(homeVM.delayedRenderingConfig, homeVM.isClickEnabled) { config, isEnabled ->
|
||||
val isInitialTouchDisabled = config?.isInitialTouchDisabled.orFalse()
|
||||
val hasLowerRamThanThreshold = config?.hasLowerRamThanThreshold.orFalse()
|
||||
Pair(isInitialTouchDisabled && hasLowerRamThanThreshold, isEnabled)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.collect { (isTouchDisabled, isEnabled) ->
|
||||
if (isTouchDisabled) {
|
||||
when {
|
||||
!isEnabled -> {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
||||
)
|
||||
naviAnalyticsEventTracker.homeClickDisabled()
|
||||
}
|
||||
|
||||
else -> {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
|
||||
naviAnalyticsEventTracker.homeClickEnabled()
|
||||
else -> {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
|
||||
naviAnalyticsEventTracker.homeClickEnabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,13 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -52,8 +55,10 @@ import com.airbnb.lottie.compose.LottieAnimation
|
||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||
import com.airbnb.lottie.compose.LottieConstants
|
||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||
import com.navi.base.utils.orFalse
|
||||
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
|
||||
import com.navi.common.alchemist.model.WidgetRenderState
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
|
||||
import com.navi.common.model.common.MastheadWidgetData
|
||||
import com.navi.naviwidgets.R
|
||||
import com.navi.pay.utils.conditional
|
||||
@@ -63,6 +68,9 @@ import com.naviapp.home.reducer.HpEvents
|
||||
import com.naviapp.home.utils.getHomeWidgetAnimationSpec
|
||||
import com.naviapp.home.viewmodel.HomeViewModel
|
||||
import com.naviapp.utils.Constants.HOME_SCREEN_IN_CAPS
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
|
||||
@@ -97,12 +105,21 @@ fun FrontLayerContent(
|
||||
TopNotchUI()
|
||||
}
|
||||
staticNudgeContainer()
|
||||
RenderUiTronContent(
|
||||
elementList = widgets,
|
||||
homeWidgetRenderer = homeWidgetRenderer,
|
||||
homeVM = homeVM,
|
||||
onEvent = onEvent,
|
||||
)
|
||||
if (isDelayedRenderingEnabled(homeVM)) {
|
||||
ChunkedWidgetRenderer(
|
||||
elementList = widgets,
|
||||
homeWidgetRenderer = homeWidgetRenderer,
|
||||
homeVM = homeVM,
|
||||
onEvent = onEvent,
|
||||
)
|
||||
} else {
|
||||
RenderUiTronContent(
|
||||
elementList = widgets,
|
||||
homeWidgetRenderer = homeWidgetRenderer,
|
||||
homeVM = homeVM,
|
||||
onEvent = onEvent,
|
||||
)
|
||||
}
|
||||
ContentLoaderLottie(isRenderingFirstTime)
|
||||
SideEffect {
|
||||
if (isRenderingFirstTime.not()) {
|
||||
@@ -118,6 +135,14 @@ fun FrontLayerContent(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun isDelayedRenderingEnabled(homeVM: () -> HomeViewModel): Boolean {
|
||||
val renderingConfig by homeVM().delayedRenderingConfig.collectAsState()
|
||||
val featureEnabled = renderingConfig?.isDelayedRenderingEnabled.orFalse()
|
||||
val isRamBelowThreshold = renderingConfig?.hasLowerRamThanThreshold.orFalse()
|
||||
return featureEnabled && isRamBelowThreshold
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MastheadWidget(
|
||||
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
@@ -218,7 +243,43 @@ private fun AnimatedContainerForWidgets(
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun RenderUiTronContent(
|
||||
private fun ChunkedWidgetRenderer(
|
||||
elementList: List<AlchemistWidgetModelDefinition<UiTronResponse>>,
|
||||
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
homeVM: () -> HomeViewModel,
|
||||
onEvent: (event: HpEvents) -> Unit,
|
||||
firstChunkSize: Int = 2,
|
||||
) {
|
||||
var displayedCount by remember { mutableIntStateOf(0) }
|
||||
val delayBetweenChunks = remember {
|
||||
FirebaseRemoteConfigHelper.getLong(
|
||||
FirebaseRemoteConfigHelper.HOMEPAGE_WIDGET_RENDER_DELAY_MS,
|
||||
140L,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(elementList) {
|
||||
withContext(Dispatchers.Default) {
|
||||
displayedCount = displayedCount.coerceAtMost(elementList.size)
|
||||
var nextChunk = if (displayedCount == 0) firstChunkSize else 1
|
||||
while (displayedCount < elementList.size) {
|
||||
displayedCount = (displayedCount + nextChunk).coerceAtMost(elementList.size)
|
||||
nextChunk += 1
|
||||
delay(delayBetweenChunks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderUiTronContent(
|
||||
elementList = elementList.take(displayedCount),
|
||||
homeWidgetRenderer = homeWidgetRenderer,
|
||||
homeVM = homeVM,
|
||||
onEvent = onEvent,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderUiTronContent(
|
||||
elementList: List<AlchemistWidgetModelDefinition<UiTronResponse>>,
|
||||
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
homeVM: () -> HomeViewModel,
|
||||
@@ -242,9 +303,11 @@ internal fun RenderUiTronContent(
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
WidgetRenderState.NOT_VISIBLE -> {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
WidgetRenderState.VISIBLE -> {
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.naviapp.home.model
|
||||
|
||||
data class RenderingConfig(
|
||||
val isInitialTouchDisabled: Boolean = false,
|
||||
val isDelayedRenderingEnabled: Boolean = false,
|
||||
val hasLowerRamThanThreshold: Boolean = false,
|
||||
)
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package com.naviapp.home.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.navi.analytics.utils.NaviTrackEvent
|
||||
import com.navi.base.cache.model.NaviCacheAltSourceEntity
|
||||
@@ -21,6 +22,7 @@ import com.navi.base.utils.ConnectivityObserver
|
||||
import com.navi.base.utils.isNotNullAndNotEmpty
|
||||
import com.navi.base.utils.orFalse
|
||||
import com.navi.common.basemvi.BaseMviViewModel
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
|
||||
import com.navi.common.model.ModuleNameV2
|
||||
import com.navi.common.model.NotificationSetting
|
||||
import com.navi.common.model.SettingsMedium
|
||||
@@ -29,6 +31,7 @@ import com.navi.common.uitron.helper.VideoViewHelper
|
||||
import com.navi.common.utils.Constants.SCREEN_HASH
|
||||
import com.navi.common.utils.Constants.UPI_NUX_SCREEN
|
||||
import com.navi.common.utils.TemporaryStorageHelper
|
||||
import com.navi.common.utils.getTotalRamMemory
|
||||
import com.navi.naviwidgets.utils.CURRENT_VERSION_IN_STORE
|
||||
import com.navi.pay.common.model.view.NaviPayScreenType
|
||||
import com.navi.pay.utils.NAVI_PAY_CTA_URL_PREFIX
|
||||
@@ -42,6 +45,7 @@ import com.naviapp.home.common.handler.PostRenderTaskExecutor
|
||||
import com.naviapp.home.compose.activity.HomePageActivity
|
||||
import com.naviapp.home.compose.listener.HomeScreenCallbackListener
|
||||
import com.naviapp.home.model.BottomBarTabType
|
||||
import com.naviapp.home.model.RenderingConfig
|
||||
import com.naviapp.home.reducer.HomeReducer
|
||||
import com.naviapp.home.reducer.HpEffects
|
||||
import com.naviapp.home.reducer.HpEvents
|
||||
@@ -62,11 +66,11 @@ import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
@@ -103,6 +107,35 @@ constructor(
|
||||
private val _internetConnectivity = MutableSharedFlow<ConnectivityObserver.Status>()
|
||||
val internetConnectivity: SharedFlow<ConnectivityObserver.Status> = _internetConnectivity
|
||||
|
||||
private val _delayedRenderingConfig: MutableStateFlow<RenderingConfig?> = MutableStateFlow(null)
|
||||
val delayedRenderingConfig = _delayedRenderingConfig.asStateFlow()
|
||||
|
||||
fun fetchDelayedRenderingConfig(context: Context) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val isTouchDisabled =
|
||||
FirebaseRemoteConfigHelper.getBoolean(
|
||||
FirebaseRemoteConfigHelper.DISABLE_INITIAL_TOUCH_EVENT,
|
||||
false,
|
||||
)
|
||||
val isDelayedRenderingEnabled =
|
||||
FirebaseRemoteConfigHelper.getBoolean(
|
||||
FirebaseRemoteConfigHelper.ENABLE_DELAYED_HP_WIDGETS_RENDERING,
|
||||
false,
|
||||
)
|
||||
val maxRamThreshold =
|
||||
FirebaseRemoteConfigHelper.getDouble(
|
||||
FirebaseRemoteConfigHelper.DISABLE_INITIAL_TOUCH_EVENT_MAX_RAM
|
||||
)
|
||||
val deviceRamGb = getTotalRamMemory(context)?.toDoubleOrNull() ?: 0.0
|
||||
_delayedRenderingConfig.value =
|
||||
RenderingConfig(
|
||||
isInitialTouchDisabled = isTouchDisabled,
|
||||
isDelayedRenderingEnabled = isDelayedRenderingEnabled,
|
||||
hasLowerRamThanThreshold = deviceRamGb < maxRamThreshold,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.safeLaunch((Dispatchers.IO)) { observeActionCallback() }
|
||||
viewModelScope.safeLaunch((Dispatchers.IO)) { observeInternetConnectivity() }
|
||||
@@ -112,13 +145,7 @@ constructor(
|
||||
val isClickEnabled: StateFlow<Boolean> = _isClickEnabled
|
||||
|
||||
fun updateClickEnabledState(isClickEnabled: Boolean) {
|
||||
viewModelScope.safeLaunch(Dispatchers.IO) {
|
||||
_isClickEnabled.value = isClickEnabled
|
||||
delay(10_000)
|
||||
if (_isClickEnabled.value.not()) {
|
||||
updateClickEnabledState(true)
|
||||
}
|
||||
}
|
||||
viewModelScope.safeLaunch(Dispatchers.IO) { _isClickEnabled.value = isClickEnabled }
|
||||
}
|
||||
|
||||
private suspend fun observeActionCallback() {
|
||||
|
||||
@@ -317,6 +317,8 @@ object FirebaseRemoteConfigHelper {
|
||||
|
||||
// HOME
|
||||
const val IS_GPS_POPUP_ENABLED = "GPS_POPUP_VISIBILITY"
|
||||
const val ENABLE_DELAYED_HP_WIDGETS_RENDERING = "ENABLE_DELAYED_HP_WIDGETS_RENDERING"
|
||||
const val HOMEPAGE_WIDGET_RENDER_DELAY_MS = "HOMEPAGE_WIDGET_RENDER_DELAY_MS"
|
||||
|
||||
private fun getFirebaseRemoteConfig(): FirebaseRemoteConfig {
|
||||
val remoteConfig = Firebase.remoteConfig
|
||||
|
||||
Reference in New Issue
Block a user