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:
Naman Khurmi
2025-05-21 21:54:30 +05:30
committed by GitHub
parent 517d25a096
commit f6f898a7ac
5 changed files with 144 additions and 45 deletions

View File

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

View File

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

View File

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

View File

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

View File

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