NTP-71293 | Naman Khurmi | Improved chunked rendering with delay (#16553)
Co-authored-by: Venkat Praneeth Reddy <venkat.praneeth@navi.com>
This commit is contained in:
@@ -180,9 +180,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
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HomePageActivity :
|
||||
@@ -306,7 +305,6 @@ class HomePageActivity :
|
||||
}
|
||||
}
|
||||
initObservers()
|
||||
homeVM.fetchDelayedRenderingConfig(this@HomePageActivity)
|
||||
handleInitialTouchEventState()
|
||||
initErrors()
|
||||
fetchHomeData()
|
||||
@@ -323,15 +321,10 @@ class HomePageActivity :
|
||||
}
|
||||
|
||||
private fun handleInitialTouchEventState() {
|
||||
lifecycleScope.launch {
|
||||
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) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
homeVM.isClickEnabled.collect { isEnabled ->
|
||||
if (homeVM.isInitialTouchDisabled()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
when {
|
||||
!isEnabled -> {
|
||||
window.setFlags(
|
||||
@@ -348,6 +341,7 @@ class HomePageActivity :
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,43 +7,24 @@
|
||||
|
||||
package com.naviapp.home.compose.home.ui.content
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.LocalOverscrollConfiguration
|
||||
import androidx.compose.foundation.LocalOverscrollFactory
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
@@ -53,23 +34,17 @@ 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
|
||||
import com.navi.uitron.model.UiTronResponse
|
||||
import com.naviapp.home.compose.home.ui.renderer.ChunkedWidgetRenderer
|
||||
import com.naviapp.home.compose.home.ui.renderer.HomeWidgetRenderer
|
||||
import com.naviapp.home.compose.home.ui.renderer.MastheadWidgetRenderer
|
||||
import com.naviapp.home.reducer.HpEffects
|
||||
import com.naviapp.home.reducer.HpEvents
|
||||
import com.naviapp.home.utils.getHomeWidgetAnimationSpec
|
||||
import com.naviapp.home.utils.shouldRenderWidget
|
||||
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)
|
||||
@@ -77,7 +52,7 @@ fun FrontLayerContent(
|
||||
modifier: Modifier,
|
||||
frontLayerShape: Shape,
|
||||
widgets: List<AlchemistWidgetModelDefinition<UiTronResponse>>?,
|
||||
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
uitronRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
isRenderingFirstTime: Boolean,
|
||||
homeScrollState: () -> ScrollState,
|
||||
homeVM: () -> HomeViewModel,
|
||||
@@ -88,7 +63,7 @@ fun FrontLayerContent(
|
||||
isMastheadEnabled: Boolean,
|
||||
appBarHeight: Dp,
|
||||
) {
|
||||
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
|
||||
CompositionLocalProvider(LocalOverscrollFactory provides null) {
|
||||
Column(
|
||||
modifier
|
||||
.background(Color.White, frontLayerShape)
|
||||
@@ -98,35 +73,38 @@ fun FrontLayerContent(
|
||||
) {
|
||||
if (!widgets.isNullOrEmpty()) {
|
||||
Column {
|
||||
if (isMastheadEnabled) {
|
||||
MastheadWidget(homeWidgetRenderer, mastheadWidget, appBarHeight)
|
||||
} else {
|
||||
TopNotchUI()
|
||||
}
|
||||
MastheadWidgetRenderer(
|
||||
isMastheadEnabled = isMastheadEnabled,
|
||||
uitronRenderer = uitronRenderer,
|
||||
mastheadWidget = mastheadWidget,
|
||||
appBarHeight = appBarHeight,
|
||||
)
|
||||
staticNudgeContainer()
|
||||
if (isDelayedRenderingEnabled(homeVM)) {
|
||||
if (isRenderingFirstTime && homeVM().isDelayedRenderingEnabled()) {
|
||||
ChunkedWidgetRenderer(
|
||||
elementList = widgets,
|
||||
homeWidgetRenderer = homeWidgetRenderer,
|
||||
homeWidgetRenderer = uitronRenderer,
|
||||
homeVM = homeVM,
|
||||
onEvent = onEvent,
|
||||
)
|
||||
} else {
|
||||
RenderUiTronContent(
|
||||
elementList = widgets,
|
||||
homeWidgetRenderer = homeWidgetRenderer,
|
||||
homeVM = homeVM,
|
||||
onEvent = onEvent,
|
||||
)
|
||||
widgets.forEach { element ->
|
||||
HomeWidgetRenderer(
|
||||
element = element,
|
||||
uitronRenderer = uitronRenderer,
|
||||
homeVM = homeVM,
|
||||
onEvent = onEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
ContentLoaderLottie(isRenderingFirstTime)
|
||||
SideEffect {
|
||||
if (isRenderingFirstTime.not()) {
|
||||
onEffect(HpEffects.HomePageUiRenderedEvent)
|
||||
onEvent(HpEvents.UpdateIsHomePageRendered(true))
|
||||
} else {
|
||||
onEffect(HpEffects.LogAppLaunchTime(HOME_SCREEN_IN_CAPS))
|
||||
}
|
||||
}
|
||||
SideEffect {
|
||||
if (isRenderingFirstTime.not()) {
|
||||
onEffect(HpEffects.HomePageUiRenderedEvent)
|
||||
onEvent(HpEvents.UpdateIsHomePageRendered(true))
|
||||
} else {
|
||||
onEffect(HpEffects.LogAppLaunchTime(HOME_SCREEN_IN_CAPS))
|
||||
}
|
||||
}
|
||||
} else HomePageContentShimmer()
|
||||
@@ -134,205 +112,16 @@ 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,
|
||||
mastheadWidget: MastheadWidgetData?,
|
||||
appBarHeight: Dp,
|
||||
) {
|
||||
val shouldRender =
|
||||
mastheadWidget?.let { shouldRenderWidget(it.renderActions?.preRenderAction) } ?: false
|
||||
|
||||
if (!shouldRender) {
|
||||
Box(modifier = Modifier.padding(top = appBarHeight, bottom = 16.dp).fillMaxWidth())
|
||||
return
|
||||
}
|
||||
mastheadWidget?.let {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.height(IntrinsicSize.Max)
|
||||
.padding(bottom = it.widgetBottomPadding.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Box(modifier = Modifier.matchParentSize()) {
|
||||
homeWidgetRenderer(it.backgroundIllustration?.uiTronResponse)
|
||||
}
|
||||
Box(modifier = Modifier.padding(top = appBarHeight).fillMaxWidth()) {
|
||||
homeWidgetRenderer(it.widgetData?.uiTronResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TopNotchUI() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.padding(vertical = 8.dp)
|
||||
.width(40.dp)
|
||||
.height(4.dp)
|
||||
.background(color = Color(0xFFE3E5E5), shape = RoundedCornerShape(16.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContentLoaderLottie(isPrioritySectionRendered: Boolean) {
|
||||
if (isPrioritySectionRendered) {
|
||||
val composition by
|
||||
rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.cta_loader_purple))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 32.dp, bottom = 100.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
val composition by
|
||||
rememberLottieComposition(
|
||||
spec = LottieCompositionSpec.RawRes(R.raw.cta_loader_purple)
|
||||
)
|
||||
LottieAnimation(composition = composition, iterations = LottieConstants.IterateForever)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimatedContainerForWidgets(
|
||||
isVisible: MutableState<Boolean>,
|
||||
widget: AlchemistWidgetModelDefinition<UiTronResponse>,
|
||||
homeVM: HomeViewModel,
|
||||
homeWidget: @Composable () -> Unit,
|
||||
) {
|
||||
val setImpressionTracker =
|
||||
widget.widgetRenderActions?.onImpressionAction?.actions.isNullOrEmpty().not()
|
||||
AnimatedVisibility(
|
||||
visible = isVisible.value,
|
||||
modifier =
|
||||
Modifier.conditional(setImpressionTracker) {
|
||||
onGloballyPositioned { layoutCoordinates ->
|
||||
homeVM.sectionVisibilityTracker.updateSectionImpressionState(
|
||||
widget,
|
||||
layoutCoordinates,
|
||||
) { onImpressionAction ->
|
||||
homeVM.handleActions(onImpressionAction)
|
||||
}
|
||||
}
|
||||
},
|
||||
enter =
|
||||
expandVertically(
|
||||
expandFrom = Alignment.Top,
|
||||
clip = true,
|
||||
animationSpec = getHomeWidgetAnimationSpec(),
|
||||
) {
|
||||
0
|
||||
} + fadeIn(animationSpec = getHomeWidgetAnimationSpec()),
|
||||
exit =
|
||||
shrinkVertically(
|
||||
shrinkTowards = Alignment.Bottom,
|
||||
clip = true,
|
||||
animationSpec = getHomeWidgetAnimationSpec(),
|
||||
) {
|
||||
0
|
||||
} + fadeOut(animationSpec = getHomeWidgetAnimationSpec()),
|
||||
) {
|
||||
homeWidget()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChunkedWidgetRenderer(
|
||||
elementList: List<AlchemistWidgetModelDefinition<UiTronResponse>>,
|
||||
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
homeVM: () -> HomeViewModel,
|
||||
onEvent: (event: HpEvents) -> Unit,
|
||||
firstChunkSize: Int = 2,
|
||||
) {
|
||||
if (elementList.isEmpty()) return
|
||||
|
||||
val displayedCount = homeVM().widgetsDisplayed.coerceAtMost(elementList.size)
|
||||
|
||||
val delayBetweenChunks = remember {
|
||||
FirebaseRemoteConfigHelper.getLong(
|
||||
FirebaseRemoteConfigHelper.HOMEPAGE_WIDGET_RENDER_DELAY_MS,
|
||||
140L,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(elementList) {
|
||||
if (displayedCount < elementList.size) {
|
||||
withContext(Dispatchers.Default) {
|
||||
var nextChunk = if (displayedCount == 0) firstChunkSize else 1
|
||||
while (displayedCount < elementList.size) {
|
||||
homeVM()
|
||||
.updateDisplayedWidgetCount(
|
||||
(displayedCount + nextChunk).coerceAtMost(elementList.size)
|
||||
)
|
||||
nextChunk += 1
|
||||
delay(delayBetweenChunks)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
homeVM().updateDisplayedWidgetCount(elementList.size)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
onEvent: (event: HpEvents) -> Unit,
|
||||
) {
|
||||
elementList.forEach { element ->
|
||||
element.widgetId?.let { widgetId ->
|
||||
key(widgetId) {
|
||||
val visible = remember {
|
||||
mutableStateOf(element.widgetRenderState == WidgetRenderState.VISIBLE)
|
||||
}
|
||||
|
||||
LaunchedEffect(element.widgetRenderState) {
|
||||
when (element.widgetRenderState) {
|
||||
WidgetRenderState.NEWLY_ADDED -> {
|
||||
visible.value = true
|
||||
onEvent(
|
||||
HpEvents.UpdateScreenContentWidgetRenderState(
|
||||
widgetId,
|
||||
WidgetRenderState.VISIBLE,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
WidgetRenderState.NOT_VISIBLE -> {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
WidgetRenderState.VISIBLE -> {
|
||||
visible.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedContainerForWidgets(visible, element, homeVM()) {
|
||||
homeWidgetRenderer(element.widgetData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ fun HomeTopBar(
|
||||
appBarHeight: Dp,
|
||||
statusBarHeight: Dp,
|
||||
topBarContent: UiTronResponse?,
|
||||
homeWidgetRenderer: @Composable (UiTronResponse) -> Unit,
|
||||
uitronRenderer: @Composable (UiTronResponse) -> Unit,
|
||||
homeScrollState: () -> ScrollState,
|
||||
) {
|
||||
val isScrollingUp by remember { derivedStateOf { homeScrollState().value.dp < appBarHeight } }
|
||||
@@ -60,7 +60,7 @@ fun HomeTopBar(
|
||||
}
|
||||
|
||||
// Render the top bar
|
||||
ToolbarRender(topBarModifier, statusBarHeight, topBarContent, homeWidgetRenderer)
|
||||
ToolbarRender(topBarModifier, statusBarHeight, topBarContent, uitronRenderer)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -103,7 +103,7 @@ fun MastheadTopBar(
|
||||
modifier = toolbarModifier,
|
||||
statusBarHeight = statusBarHeight,
|
||||
toolbarContent = toolbarConfig?.toolBarNav?.uiTronResponse,
|
||||
homeWidgetRenderer = renderWidget,
|
||||
uitronRenderer = renderWidget,
|
||||
mastheadEnable = true,
|
||||
)
|
||||
}
|
||||
@@ -174,12 +174,12 @@ private fun ToolbarRender(
|
||||
modifier: Modifier,
|
||||
statusBarHeight: Dp,
|
||||
toolbarContent: UiTronResponse?,
|
||||
homeWidgetRenderer: @Composable (UiTronResponse) -> Unit,
|
||||
uitronRenderer: @Composable (UiTronResponse) -> Unit,
|
||||
mastheadEnable: Boolean = false,
|
||||
) {
|
||||
Row(modifier = modifier, verticalAlignment = Alignment.Top) {
|
||||
Row(Modifier.padding(top = statusBarHeight)) {
|
||||
toolbarContent?.let { homeWidgetRenderer(it) } ?: DefaultTopBar(mastheadEnable)
|
||||
toolbarContent?.let { uitronRenderer(it) } ?: DefaultTopBar(mastheadEnable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.naviapp.home.compose.home.ui.renderer
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
|
||||
import com.navi.uitron.model.UiTronResponse
|
||||
import com.naviapp.home.reducer.HpEvents
|
||||
import com.naviapp.home.viewmodel.HomeViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
@Composable
|
||||
fun ChunkedWidgetRenderer(
|
||||
elementList: List<AlchemistWidgetModelDefinition<UiTronResponse>>?,
|
||||
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
homeVM: () -> HomeViewModel,
|
||||
onEvent: (event: HpEvents) -> Unit,
|
||||
chunkSize: Int = 1,
|
||||
) {
|
||||
if (elementList.isNullOrEmpty()) return
|
||||
|
||||
val delayBetweenWidgets = remember {
|
||||
FirebaseRemoteConfigHelper.getLong(
|
||||
FirebaseRemoteConfigHelper.HOMEPAGE_WIDGET_RENDER_DELAY_MS,
|
||||
140L,
|
||||
)
|
||||
}
|
||||
|
||||
val widgetsToShow = remember(elementList) { mutableIntStateOf(0) }
|
||||
|
||||
Column {
|
||||
elementList.take(widgetsToShow.intValue).forEach { element ->
|
||||
key(element.widgetId) {
|
||||
HomeWidgetRenderer(
|
||||
element = element,
|
||||
uitronRenderer = homeWidgetRenderer,
|
||||
homeVM = homeVM,
|
||||
onEvent = onEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(elementList) {
|
||||
widgetsToShow.intValue = 0
|
||||
elementList.indices
|
||||
.chunked(chunkSize)
|
||||
.asFlow()
|
||||
.onEach { slice ->
|
||||
val nextCount = slice.last() + 1
|
||||
widgetsToShow.intValue = nextCount
|
||||
withFrameNanos {}
|
||||
if (slice.last() < elementList.lastIndex) {
|
||||
delay(delayBetweenWidgets)
|
||||
}
|
||||
}
|
||||
.collect()
|
||||
onEvent(HpEvents.RenderedFirstTime)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.naviapp.home.compose.home.ui.renderer
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
|
||||
import com.navi.common.alchemist.model.WidgetRenderState
|
||||
import com.navi.pay.utils.conditional
|
||||
import com.navi.uitron.model.UiTronResponse
|
||||
import com.naviapp.home.reducer.HpEvents
|
||||
import com.naviapp.home.utils.getHomeWidgetAnimationSpec
|
||||
import com.naviapp.home.viewmodel.HomeViewModel
|
||||
|
||||
@Composable
|
||||
fun HomeWidgetRenderer(
|
||||
element: AlchemistWidgetModelDefinition<UiTronResponse>,
|
||||
uitronRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
homeVM: () -> HomeViewModel,
|
||||
onEvent: (event: HpEvents) -> Unit,
|
||||
onRenderComplete: () -> Unit = {},
|
||||
) {
|
||||
val widgetId = element.widgetId
|
||||
if (widgetId == null) {
|
||||
onRenderComplete()
|
||||
return
|
||||
}
|
||||
val visible = remember {
|
||||
mutableStateOf(element.widgetRenderState == WidgetRenderState.VISIBLE)
|
||||
}
|
||||
key(widgetId) {
|
||||
HandleWidgetVisibility(
|
||||
widgetId = widgetId,
|
||||
elementState = element.widgetRenderState,
|
||||
onVisibilityChange = { visible.value = it },
|
||||
onEvent = onEvent,
|
||||
)
|
||||
AnimatedContainerForWidgets(
|
||||
isVisible = visible,
|
||||
widget = element,
|
||||
homeVM = homeVM,
|
||||
homeWidget = { uitronRenderer(element.widgetData) },
|
||||
)
|
||||
SideEffect { onRenderComplete() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HandleWidgetVisibility(
|
||||
widgetId: String,
|
||||
elementState: WidgetRenderState,
|
||||
onVisibilityChange: (Boolean) -> Unit,
|
||||
onEvent: (event: HpEvents) -> Unit,
|
||||
) {
|
||||
LaunchedEffect(elementState) {
|
||||
when (elementState) {
|
||||
WidgetRenderState.NEWLY_ADDED -> {
|
||||
onVisibilityChange(true)
|
||||
onEvent(
|
||||
HpEvents.UpdateScreenContentWidgetRenderState(
|
||||
widgetId,
|
||||
WidgetRenderState.VISIBLE,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
WidgetRenderState.NOT_VISIBLE -> onVisibilityChange(false)
|
||||
|
||||
WidgetRenderState.VISIBLE -> onVisibilityChange(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimatedContainerForWidgets(
|
||||
isVisible: MutableState<Boolean>,
|
||||
widget: AlchemistWidgetModelDefinition<UiTronResponse>,
|
||||
homeVM: () -> HomeViewModel,
|
||||
homeWidget: @Composable () -> Unit,
|
||||
) {
|
||||
val setImpressionTracker =
|
||||
widget.widgetRenderActions?.onImpressionAction?.actions.isNullOrEmpty().not()
|
||||
AnimatedVisibility(
|
||||
visible = isVisible.value,
|
||||
modifier =
|
||||
Modifier.conditional(setImpressionTracker) {
|
||||
onGloballyPositioned { layoutCoordinates ->
|
||||
homeVM().sectionVisibilityTracker.updateSectionImpressionState(
|
||||
widget,
|
||||
layoutCoordinates,
|
||||
) { onImpressionAction ->
|
||||
homeVM().handleActions(onImpressionAction)
|
||||
}
|
||||
}
|
||||
},
|
||||
enter = enterTransitionForAnimatedContainer(),
|
||||
exit = exitTransitionForAnimatedContainer(),
|
||||
content = { homeWidget() },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun exitTransitionForAnimatedContainer(): ExitTransition {
|
||||
return shrinkVertically(
|
||||
shrinkTowards = Alignment.Bottom,
|
||||
clip = true,
|
||||
animationSpec = getHomeWidgetAnimationSpec(),
|
||||
) {
|
||||
0
|
||||
} + fadeOut(animationSpec = getHomeWidgetAnimationSpec())
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun enterTransitionForAnimatedContainer(): EnterTransition {
|
||||
return expandVertically(
|
||||
expandFrom = Alignment.Top,
|
||||
clip = true,
|
||||
animationSpec = getHomeWidgetAnimationSpec(),
|
||||
) {
|
||||
0
|
||||
} + fadeIn(animationSpec = getHomeWidgetAnimationSpec())
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.naviapp.home.compose.home.ui.renderer
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.navi.common.model.common.MastheadWidgetData
|
||||
import com.navi.uitron.model.UiTronResponse
|
||||
import com.naviapp.home.utils.shouldRenderWidget
|
||||
|
||||
@Composable
|
||||
fun MastheadWidgetRenderer(
|
||||
isMastheadEnabled: Boolean,
|
||||
uitronRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
mastheadWidget: MastheadWidgetData?,
|
||||
appBarHeight: Dp,
|
||||
) {
|
||||
if (isMastheadEnabled) {
|
||||
MastheadWidget(uitronRenderer, mastheadWidget, appBarHeight)
|
||||
} else {
|
||||
TopNotchUI()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MastheadWidget(
|
||||
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
mastheadWidget: MastheadWidgetData?,
|
||||
appBarHeight: Dp,
|
||||
) {
|
||||
val shouldRender =
|
||||
mastheadWidget?.let { shouldRenderWidget(it.renderActions?.preRenderAction) } == true
|
||||
|
||||
if (!shouldRender) {
|
||||
Box(modifier = Modifier.padding(top = appBarHeight, bottom = 16.dp).fillMaxWidth())
|
||||
return
|
||||
}
|
||||
mastheadWidget.let {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.height(IntrinsicSize.Max)
|
||||
.padding(bottom = it.widgetBottomPadding.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Box(modifier = Modifier.matchParentSize()) {
|
||||
homeWidgetRenderer(it.backgroundIllustration?.uiTronResponse)
|
||||
}
|
||||
Box(modifier = Modifier.padding(top = appBarHeight).fillMaxWidth()) {
|
||||
homeWidgetRenderer(it.widgetData?.uiTronResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopNotchUI() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.padding(vertical = 8.dp)
|
||||
.width(40.dp)
|
||||
.height(4.dp)
|
||||
.background(color = Color(0xFFE3E5E5), shape = RoundedCornerShape(16.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import com.naviapp.home.compose.activity.HomePageActivity
|
||||
import com.naviapp.home.compose.home.ui.footer.utils.handleBottomBarData
|
||||
import com.naviapp.home.compose.home.utils.InitHomeScreenComponents
|
||||
import com.naviapp.home.compose.home.utils.retryHomePageApi
|
||||
import com.naviapp.home.compose.widgetfactory.HomeWidgetRenderer
|
||||
import com.naviapp.home.compose.widgetfactory.HomeUitronRenderer
|
||||
import com.naviapp.home.reducer.HpEvents
|
||||
import com.naviapp.home.reducer.HpStates
|
||||
import com.naviapp.home.utils.WidgetRenderer
|
||||
@@ -99,8 +99,8 @@ fun HomeScreen(
|
||||
true -> {
|
||||
if (hpStates().isLoading) {
|
||||
HomeScreenScaffoldRoot(
|
||||
homeWidgetRenderer = {
|
||||
HomeWidgetRenderer(
|
||||
uitronRenderer = {
|
||||
HomeUitronRenderer(
|
||||
widgetData = it?.data,
|
||||
viewModel = homeVM(),
|
||||
composeView = it?.parentComposeView,
|
||||
@@ -135,8 +135,8 @@ fun HomeScreen(
|
||||
)
|
||||
} else {
|
||||
HomeScreenScaffoldRoot(
|
||||
homeWidgetRenderer = { widget ->
|
||||
HomeWidgetRenderer(
|
||||
uitronRenderer = { widget ->
|
||||
HomeUitronRenderer(
|
||||
widgetData = widget?.data,
|
||||
viewModel = homeVM(),
|
||||
composeView = widget?.parentComposeView,
|
||||
|
||||
@@ -56,7 +56,7 @@ internal fun HomeScreenScaffoldRoot(
|
||||
hpStates: () -> HpStates,
|
||||
homeScrollState: () -> ScrollState,
|
||||
homeVM: () -> HomeViewModel,
|
||||
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
uitronRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
staticNudgeContainer: @Composable () -> Unit,
|
||||
uiController: SystemUiController,
|
||||
) {
|
||||
@@ -75,7 +75,7 @@ internal fun HomeScreenScaffoldRoot(
|
||||
frontLayerShape = frontLayerShape,
|
||||
homeScrollState = homeScrollState,
|
||||
homeVM = homeVM,
|
||||
homeWidgetRenderer = homeWidgetRenderer,
|
||||
uitronRenderer = uitronRenderer,
|
||||
onEffect = { homeVM().setEffect { it } },
|
||||
onEvent = { homeVM().sendEvent(it) },
|
||||
isRenderingFirstTime = hpStates().isRenderingFirstTime,
|
||||
@@ -88,7 +88,7 @@ internal fun HomeScreenScaffoldRoot(
|
||||
modifier = Modifier,
|
||||
statusBarHeight = statusBarHeight,
|
||||
toolbarConfig = (hpStates().collapsingToolbar),
|
||||
renderWidget = homeWidgetRenderer,
|
||||
renderWidget = uitronRenderer,
|
||||
scrollStateProvider = homeScrollState,
|
||||
onEffect = { homeVM().setEffect { it } },
|
||||
uiController = uiController,
|
||||
@@ -96,14 +96,14 @@ internal fun HomeScreenScaffoldRoot(
|
||||
} else {
|
||||
BackLayerContent(
|
||||
backLayerHeight = backLayerHeight,
|
||||
homeWidgetRenderer = homeWidgetRenderer,
|
||||
homeWidgetRenderer = uitronRenderer,
|
||||
backLayerData = (hpStates().collapsingToolbar?.collapsingTopNav)?.uiTronResponse,
|
||||
)
|
||||
FrontLayerWithBackDropScroll(
|
||||
hpStates = hpStates,
|
||||
homeScrollState = homeScrollState,
|
||||
homeVM = homeVM,
|
||||
homeWidgetRenderer = homeWidgetRenderer,
|
||||
uitronRenderer = uitronRenderer,
|
||||
frontLayerShape = frontLayerShape,
|
||||
appBarHeight = appBarHeight,
|
||||
backLayerHeight = backLayerHeight,
|
||||
@@ -114,7 +114,7 @@ internal fun HomeScreenScaffoldRoot(
|
||||
appBarHeight = appBarHeight,
|
||||
statusBarHeight = statusBarHeight,
|
||||
topBarContent = (hpStates().collapsingToolbar?.toolBarNav)?.uiTronResponse,
|
||||
homeWidgetRenderer = homeWidgetRenderer,
|
||||
uitronRenderer = uitronRenderer,
|
||||
homeScrollState = homeScrollState,
|
||||
)
|
||||
}
|
||||
@@ -139,7 +139,7 @@ private fun FrontLayerWithBackDropScroll(
|
||||
hpStates: () -> HpStates,
|
||||
homeScrollState: () -> ScrollState,
|
||||
homeVM: () -> HomeViewModel,
|
||||
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
uitronRenderer: @Composable (UiTronResponse?) -> Unit,
|
||||
frontLayerShape: RoundedCornerShape,
|
||||
appBarHeight: Dp,
|
||||
backLayerHeight: Dp,
|
||||
@@ -178,7 +178,7 @@ private fun FrontLayerWithBackDropScroll(
|
||||
.padding(bottom = appBarHeight),
|
||||
frontLayerShape = frontLayerShape,
|
||||
widgets = (hpStates().frontLayerContent),
|
||||
homeWidgetRenderer = homeWidgetRenderer,
|
||||
uitronRenderer = uitronRenderer,
|
||||
isRenderingFirstTime = hpStates().isRenderingFirstTime,
|
||||
homeScrollState = homeScrollState,
|
||||
homeVM = homeVM,
|
||||
|
||||
@@ -16,7 +16,7 @@ import com.naviapp.home.compose.uiTron.renderer.HomeCustomUiTronRenderer
|
||||
import com.naviapp.home.viewmodel.HomeViewModel
|
||||
|
||||
@Composable
|
||||
fun HomeWidgetRenderer(
|
||||
fun HomeUitronRenderer(
|
||||
widgetData: MutableMap<String, UiTronData?>?,
|
||||
composeView: List<UiTronView>?,
|
||||
viewModel: HomeViewModel,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* * 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,
|
||||
)
|
||||
@@ -85,9 +85,10 @@ class HomeReducer : BaseReducer<HpStates, HpEvents> {
|
||||
previousState.copy(frontLayerContent = newContent)
|
||||
}
|
||||
is HpEvents.UpdateFrontLayerContent -> {
|
||||
val newFirst = previousState.isRenderingFirstTime && event.isRenderingFirstTime
|
||||
val updatedList =
|
||||
updateScreenContent(
|
||||
renderingFirstTime = event.isRenderingFirstTime,
|
||||
renderingFirstTime = newFirst,
|
||||
newWidgets = event.content,
|
||||
oldWidgets = previousState.frontLayerContent,
|
||||
)
|
||||
@@ -95,6 +96,7 @@ class HomeReducer : BaseReducer<HpStates, HpEvents> {
|
||||
isLoading = false,
|
||||
isError = false,
|
||||
frontLayerContent = updatedList,
|
||||
isRenderingFirstTime = newFirst,
|
||||
)
|
||||
}
|
||||
is HpEvents.UpdatePrioritySectionData -> {
|
||||
|
||||
@@ -68,6 +68,9 @@ constructor(
|
||||
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"
|
||||
|
||||
// Lower ram device
|
||||
private const val VALUE_SCREEN_UPDATED = "screen_updated_on_low_end_device"
|
||||
}
|
||||
|
||||
private fun trackEvent(type: String, params: Map<String, String> = emptyMap()) {
|
||||
@@ -77,9 +80,10 @@ constructor(
|
||||
NaviTrackEvent.trackEvent(EVENT_NAME, eventParams)
|
||||
}
|
||||
|
||||
context(CoroutineScope)
|
||||
fun syncHomeContentData(
|
||||
scope: CoroutineScope,
|
||||
isFirstTimeRender: Boolean,
|
||||
isDelayedRenderingEnabled: Boolean,
|
||||
fetchHomeContent: suspend (shouldShowNae: Boolean) -> NaviCacheAltSourceEntity,
|
||||
onContentSynchronized: () -> Unit,
|
||||
onHomeFetchSuccessFromNetwork: () -> Unit,
|
||||
@@ -88,7 +92,8 @@ constructor(
|
||||
) {
|
||||
trackEvent(TYPE_SYNC_START, mapOf(PARAM_IS_FIRST_TIME to isFirstTimeRender.toString()))
|
||||
if (isFirstTimeRender) {
|
||||
initializeFirstTimeContentSync(
|
||||
scope.initializeFirstTimeContentSync(
|
||||
isDelayedRenderingEnabled,
|
||||
fetchHomeContent,
|
||||
onContentSynchronized,
|
||||
eventHandler,
|
||||
@@ -96,7 +101,7 @@ constructor(
|
||||
onHomeFetchSuccessFromNetwork,
|
||||
)
|
||||
} else {
|
||||
performIncrementalContentSync(
|
||||
scope.performIncrementalContentSync(
|
||||
{ fetchHomeContent(false) },
|
||||
onContentSynchronized,
|
||||
eventHandler,
|
||||
@@ -107,6 +112,7 @@ constructor(
|
||||
}
|
||||
|
||||
private fun CoroutineScope.initializeFirstTimeContentSync(
|
||||
isDelayedRenderingEnabled: Boolean,
|
||||
fetchHomeContent: suspend (shouldShowNae: Boolean) -> NaviCacheAltSourceEntity,
|
||||
onContentSynchronized: () -> Unit,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
@@ -118,6 +124,7 @@ constructor(
|
||||
|
||||
val cacheLoadOperation =
|
||||
initializeCacheDataLoad(
|
||||
isDelayedRenderingEnabled = isDelayedRenderingEnabled,
|
||||
eventHandler = eventHandler,
|
||||
onContentSynchronized = onContentSynchronized,
|
||||
onCacheLoaded = { cacheDataLoaded.value = it },
|
||||
@@ -190,29 +197,37 @@ constructor(
|
||||
}
|
||||
|
||||
private fun CoroutineScope.initializeCacheDataLoad(
|
||||
isDelayedRenderingEnabled: Boolean,
|
||||
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)
|
||||
}
|
||||
}
|
||||
loadAndProcessCacheData(
|
||||
isDelayedRenderingEnabled = isDelayedRenderingEnabled,
|
||||
eventHandler = eventHandler,
|
||||
onContentSynchronized = onContentSynchronized,
|
||||
onCompletion = { 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)
|
||||
}
|
||||
},
|
||||
onCacheLoaded = onCacheLoaded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadAndProcessCacheData(
|
||||
coroutineScope: CoroutineScope,
|
||||
private suspend fun CoroutineScope.loadAndProcessCacheData(
|
||||
isDelayedRenderingEnabled: Boolean,
|
||||
eventHandler: (HpEvents) -> Unit,
|
||||
onContentSynchronized: () -> Unit,
|
||||
onCompletion: (NaviCacheEntity?) -> Unit,
|
||||
onCacheLoaded: (Boolean) -> Unit,
|
||||
) {
|
||||
val cachedHomeContent = naviCacheRepository.get(NaviSharedDbKeys.HOME_TAB.name)
|
||||
|
||||
@@ -223,22 +238,30 @@ constructor(
|
||||
|
||||
deserializer.setCacheEntity(cachedHomeContent.value)
|
||||
|
||||
val prioritySection = deserializer.getPrioritySection()
|
||||
|
||||
val updatedPriorityContentSection =
|
||||
HomePrioritySectionData(
|
||||
content = getFilteredWidgets(prioritySection.content),
|
||||
topNav = prioritySection.topNav,
|
||||
if (isDelayedRenderingEnabled) {
|
||||
val content = deserializer.getScreen(cachedHomeContent.value)
|
||||
updateScreenContent(
|
||||
screenContent = content,
|
||||
eventHandler = eventHandler,
|
||||
isFirstRender = true,
|
||||
)
|
||||
trackEvent(TYPE_CACHE_PROCESS, mapOf(PARAM_CONTENT_STATUS to VALUE_SCREEN_UPDATED))
|
||||
onCacheLoaded(true)
|
||||
onContentSynchronized()
|
||||
} else {
|
||||
val prioritySection = deserializer.getPrioritySection()
|
||||
val updatedPriorityContentSection =
|
||||
HomePrioritySectionData(
|
||||
content = getFilteredWidgets(prioritySection.content),
|
||||
topNav = prioritySection.topNav,
|
||||
)
|
||||
eventHandler(HpEvents.UpdatePrioritySectionData(updatedPriorityContentSection))
|
||||
trackEvent(
|
||||
TYPE_CACHE_PROCESS,
|
||||
mapOf(PARAM_CONTENT_STATUS to VALUE_PRIORITY_SECTION_UPDATED),
|
||||
)
|
||||
|
||||
eventHandler(HpEvents.UpdatePrioritySectionData(updatedPriorityContentSection))
|
||||
trackEvent(
|
||||
TYPE_CACHE_PROCESS,
|
||||
mapOf(PARAM_CONTENT_STATUS to VALUE_PRIORITY_SECTION_UPDATED),
|
||||
)
|
||||
|
||||
val screenLayoutDeferred =
|
||||
coroutineScope.safeAsync {
|
||||
val screenLayoutDeferred = safeAsync {
|
||||
val screenLayout = deserializer.getScreenDefinitionWithoutContent()
|
||||
screenLayout?.let {
|
||||
eventHandler(HpEvents.UpdateScreenWithoutContent(screenLayout))
|
||||
@@ -249,26 +272,25 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope
|
||||
.safeAsync {
|
||||
val remainingContent = deserializer.getRemainingContent()
|
||||
remainingContent?.let { content ->
|
||||
screenLayoutDeferred.await()
|
||||
eventHandler(
|
||||
HpEvents.UpdateFrontLayerContent(
|
||||
content = getFilteredWidgets(content) ?: emptyList(),
|
||||
isRenderingFirstTime = true,
|
||||
safeAsync {
|
||||
val remainingContent = deserializer.getRemainingContent()
|
||||
remainingContent?.let { content ->
|
||||
screenLayoutDeferred.await()
|
||||
eventHandler(
|
||||
HpEvents.UpdateFrontLayerContent(
|
||||
content = getFilteredWidgets(content) ?: emptyList(),
|
||||
isRenderingFirstTime = true,
|
||||
)
|
||||
)
|
||||
)
|
||||
trackEvent(
|
||||
TYPE_CACHE_PROCESS,
|
||||
mapOf(PARAM_CONTENT_STATUS to VALUE_REMAINING_CONTENT_UPDATED),
|
||||
)
|
||||
trackEvent(
|
||||
TYPE_CACHE_PROCESS,
|
||||
mapOf(PARAM_CONTENT_STATUS to VALUE_REMAINING_CONTENT_UPDATED),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.await()
|
||||
|
||||
onCompletion(cachedHomeContent)
|
||||
.await()
|
||||
onCompletion(cachedHomeContent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.initiateNetworkSyncAndProcess(
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2024-2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.naviapp.home.usecase
|
||||
|
||||
import android.content.Context
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
|
||||
import com.navi.common.utils.getTotalRamMemory
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Use case responsible for managing rendering configuration decisions based on device capabilities
|
||||
* and remote configuration flags. This helps optimize the app's rendering behavior based on device
|
||||
* constraints.
|
||||
*/
|
||||
class RenderingConfigurationUseCase
|
||||
@Inject
|
||||
constructor(@ApplicationContext private val context: Context) {
|
||||
companion object {
|
||||
private const val DEFAULT_TOUCH_DISABLED = false
|
||||
private const val DEFAULT_DELAYED_RENDERING = false
|
||||
}
|
||||
|
||||
/**
|
||||
* The device RAM in GB, calculated once and cached for reuse. Returns 0.0 if RAM information
|
||||
* cannot be determined.
|
||||
*/
|
||||
val deviceRamGb: Double by lazy { getTotalRamMemory(context)?.toDoubleOrNull() ?: 0.0 }
|
||||
|
||||
/** Flag indicating if the device has lower RAM than the threshold defined in remote config. */
|
||||
private val isRamBelowThreshold by lazy {
|
||||
val maxRamThreshold =
|
||||
FirebaseRemoteConfigHelper.getDouble(
|
||||
FirebaseRemoteConfigHelper.DISABLE_INITIAL_TOUCH_EVENT_MAX_RAM
|
||||
)
|
||||
deviceRamGb < maxRamThreshold
|
||||
}
|
||||
|
||||
/** Flag indicating if delayed rendering is enabled in Firebase Remote Config. */
|
||||
private val isDelayedRenderingFeatureEnabled by lazy {
|
||||
FirebaseRemoteConfigHelper.getBoolean(
|
||||
FirebaseRemoteConfigHelper.ENABLE_DELAYED_HP_WIDGETS_RENDERING,
|
||||
DEFAULT_DELAYED_RENDERING,
|
||||
)
|
||||
}
|
||||
|
||||
/** Flag indicating if disabling initial touch events is enabled in Firebase Remote Config. */
|
||||
private val isTouchDisabledFeatureEnabled by lazy {
|
||||
FirebaseRemoteConfigHelper.getBoolean(
|
||||
FirebaseRemoteConfigHelper.DISABLE_INITIAL_TOUCH_EVENT,
|
||||
DEFAULT_TOUCH_DISABLED,
|
||||
)
|
||||
}
|
||||
|
||||
/** Cached result for delayed rendering decision. */
|
||||
private val _isDelayedRenderingEnabled by lazy {
|
||||
isDelayedRenderingFeatureEnabled && isRamBelowThreshold
|
||||
}
|
||||
|
||||
/** Cached result for initial touch disabled decision. */
|
||||
private val _isInitialTouchDisabled by lazy {
|
||||
isTouchDisabledFeatureEnabled && isRamBelowThreshold
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether delayed rendering should be enabled based on device capabilities and remote
|
||||
* configuration.
|
||||
*
|
||||
* @return true if delayed rendering should be enabled, false otherwise
|
||||
*/
|
||||
fun isDelayedRenderingEnabled() = _isDelayedRenderingEnabled
|
||||
|
||||
/**
|
||||
* Returns whether initial touch events should be disabled based on device capabilities and
|
||||
* remote configuration.
|
||||
*
|
||||
* @return true if initial touch events should be disabled, false otherwise
|
||||
*/
|
||||
fun isInitialTouchDisabled() = _isInitialTouchDisabled
|
||||
}
|
||||
@@ -9,7 +9,6 @@ package com.naviapp.home.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -26,7 +25,6 @@ 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
|
||||
@@ -35,7 +33,6 @@ 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
|
||||
@@ -49,7 +46,6 @@ 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
|
||||
@@ -59,6 +55,7 @@ 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.home.usecase.RenderingConfigurationUseCase
|
||||
import com.naviapp.nux.handler.NewUserExperienceHandler
|
||||
import com.naviapp.utils.Constants.EmailConstants.IS_USER_EMAIL_SUBMITTED
|
||||
import com.naviapp.utils.Constants.HOME_SCREEN_SCREEN_HASH
|
||||
@@ -75,7 +72,6 @@ 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
|
||||
@@ -96,6 +92,7 @@ constructor(
|
||||
val sectionVisibilityTracker: HomePageSectionImpressionTracker,
|
||||
val postRenderTaskExecutor: PostRenderTaskExecutor,
|
||||
private val homeContentProcessingUseCase: HomeContentProcessingUseCase,
|
||||
private val renderingConfigurationUseCase: RenderingConfigurationUseCase,
|
||||
@ApplicationContext val context: Context,
|
||||
) :
|
||||
BaseMviViewModel<HpStates, HpEvents, HpEffects>(
|
||||
@@ -108,53 +105,19 @@ constructor(
|
||||
private var analyticsStartTs = System.currentTimeMillis()
|
||||
private var isHomeUIRenderedEventLogged = false
|
||||
var isErrorNaeTriggered = false
|
||||
private val totalRamMemory = getTotalRamMemory(context = context)?.toDoubleOrNull() ?: 100.0
|
||||
private var postApiOneTimeActionTriggered by mutableStateOf(false)
|
||||
|
||||
// Internet Connectivity
|
||||
private val _internetConnectivity = MutableSharedFlow<ConnectivityObserver.Status>()
|
||||
val internetConnectivity: SharedFlow<ConnectivityObserver.Status> = _internetConnectivity
|
||||
|
||||
var widgetsDisplayed by mutableIntStateOf(0)
|
||||
private set
|
||||
fun isDelayedRenderingEnabled() = renderingConfigurationUseCase.isDelayedRenderingEnabled()
|
||||
|
||||
private var postApiOneTimeActionTriggered by mutableStateOf(false)
|
||||
|
||||
fun updateDisplayedWidgetCount(count: Int) {
|
||||
widgetsDisplayed = count
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
fun isInitialTouchDisabled() = renderingConfigurationUseCase.isInitialTouchDisabled()
|
||||
|
||||
init {
|
||||
viewModelScope.safeLaunch((Dispatchers.IO)) { observeActionCallback() }
|
||||
viewModelScope.safeLaunch((Dispatchers.IO)) { observeInternetConnectivity() }
|
||||
viewModelScope.safeLaunch(Dispatchers.IO) { observeActionCallback() }
|
||||
viewModelScope.safeLaunch(Dispatchers.IO) { observeInternetConnectivity() }
|
||||
}
|
||||
|
||||
private val _isClickEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
@@ -207,7 +170,7 @@ constructor(
|
||||
screenHash = screenHash,
|
||||
shouldShowNae = shouldShowNae.orFalse(),
|
||||
naeScreenName = naeScreenName,
|
||||
totalRam = totalRamMemory,
|
||||
totalRam = renderingConfigurationUseCase.deviceRamGb,
|
||||
onFailure = { errorMessage, errors ->
|
||||
setEffect { HpEffects.OnApiFailure(errorMessage, errors) }
|
||||
},
|
||||
@@ -273,24 +236,24 @@ constructor(
|
||||
screenHash.orEmpty(),
|
||||
)
|
||||
}
|
||||
with(viewModelScope) {
|
||||
homeContentProcessingUseCase.syncHomeContentData(
|
||||
isFirstTimeRender = state.value.isRenderingFirstTime,
|
||||
eventHandler = ::sendEvent,
|
||||
onContentSynchronized = ::onContentSynchronized,
|
||||
fetchHomeContent = {
|
||||
fetchHomeDataFromApi(
|
||||
naeScreenName = naeScreenName,
|
||||
screenHash = screenHash,
|
||||
shouldShowNae = it,
|
||||
)
|
||||
},
|
||||
onHomeFetchSuccessFromNetwork = ::onHomeFetchSuccessFromNetwork,
|
||||
onSelectiveRefreshStateChange = {
|
||||
selectiveRefreshHandler.updateRefreshState(this@HomeViewModel, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
homeContentProcessingUseCase.syncHomeContentData(
|
||||
scope = this.viewModelScope,
|
||||
isFirstTimeRender = state.value.isRenderingFirstTime,
|
||||
isDelayedRenderingEnabled = isDelayedRenderingEnabled(),
|
||||
eventHandler = ::sendEvent,
|
||||
onContentSynchronized = ::onContentSynchronized,
|
||||
fetchHomeContent = {
|
||||
fetchHomeDataFromApi(
|
||||
naeScreenName = naeScreenName,
|
||||
screenHash = screenHash,
|
||||
shouldShowNae = it,
|
||||
)
|
||||
},
|
||||
onHomeFetchSuccessFromNetwork = ::onHomeFetchSuccessFromNetwork,
|
||||
onSelectiveRefreshStateChange = {
|
||||
selectiveRefreshHandler.updateRefreshState(this@HomeViewModel, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ import com.navi.rr.scratchcard.ui.compose.ScratchCardRendererV2
|
||||
import com.navi.rr.utils.constants.ScratchCardAnimationConstants.SCRATCH_CARD_DEFAULT_POSITION
|
||||
import com.navi.rr.utils.constants.ScratchCardAnimationConstants.SCRATCH_CARD_INITIAL_POSITION
|
||||
import com.navi.rr.utils.ext.clickable
|
||||
import com.naviapp.home.compose.home.ui.content.isDelayedRenderingEnabled
|
||||
import com.naviapp.home.model.BottomBarTabType
|
||||
import com.naviapp.home.reducer.HpStates
|
||||
import com.naviapp.home.viewmodel.HomeViewModel
|
||||
@@ -75,7 +74,7 @@ fun ScratchCardOverlayRenderer(
|
||||
),
|
||||
label = EMPTY,
|
||||
)
|
||||
val delayedRenderingEnabled = isDelayedRenderingEnabled(homeVM)
|
||||
val delayedRenderingEnabled = homeVM().isDelayedRenderingEnabled()
|
||||
var isReadyToRender by remember { mutableStateOf(!delayedRenderingEnabled) }
|
||||
if (
|
||||
shouldShowScratchCard(
|
||||
|
||||
Reference in New Issue
Block a user