TP-62634 | Selective-refresh (#10718)

Co-authored-by: Hitesh <hitesh.kumar@navi.com>
Co-authored-by: namankhurmi <naman.khurmi@navi.com>
This commit is contained in:
Abhinav Gupta
2024-05-20 23:02:55 +05:30
committed by GitHub
parent 5cea659273
commit c007533277
18 changed files with 646 additions and 192 deletions

View File

@@ -0,0 +1,34 @@
package com.naviapp.home.common.actions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.navi.base.utils.orTrue
import com.navi.common.uitron.model.action.V3HomeAction
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.viewmodel.HomeVM
@Composable
fun HandleApiAction(
viewModel: HomeVM,
activity: HomePageActivity,
naviAnalyticsEventTracker: NaviAnalytics.Home,
isPaymentLoaderShowing: Boolean
) {
LaunchedEffect(Unit) {
viewModel.getActionCallback().collect { action ->
when (action) {
is V3HomeAction -> {
viewModel.fetchCards(
showLoader = action.showLoader.orTrue(),
naviAnalyticsEventTracker = naviAnalyticsEventTracker,
activity = activity,
isPaymentLoaderShowing = isPaymentLoaderShowing
)
}
else -> {}
}
}
}
}

View File

@@ -0,0 +1,25 @@
package com.naviapp.home.common.handler
import androidx.compose.runtime.Composable
import com.navi.ap.common.handler.HandlePublishEventAction
import com.navi.common.managers.PermissionsManager
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.home.common.actions.HandleApiAction
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.viewmodel.HomeVM
@Composable
fun InitActionsHandler(
viewModel: HomeVM,
activity: HomePageActivity,
naviAnalyticsEventTracker: NaviAnalytics.Home,
isPaymentLoaderShowing: Boolean
) {
HandlePublishEventAction(viewModel = viewModel)
HandleApiAction(
viewModel = viewModel,
activity = activity,
naviAnalyticsEventTracker = naviAnalyticsEventTracker,
isPaymentLoaderShowing = isPaymentLoaderShowing
)
}

View File

@@ -0,0 +1,98 @@
package com.naviapp.home.common.handler
import androidx.compose.runtime.mutableStateMapOf
import com.navi.common.model.common.WidgetResponse
import com.navi.naviwidgets.models.NaviUiTronWidget
import com.navi.naviwidgets.models.NaviWidget
import com.naviapp.home.model.WidgetUiState
import com.naviapp.home.model.WidgetUiStateData
import javax.inject.Inject
typealias WidgetId = String
class HomePageDataUpdateHandler @Inject constructor() {
//List to observe existingHomePageData
private var existingHomePageWidgets: MutableList<NaviWidget> = mutableListOf()
private val widgetUiStateMap = mutableStateMapOf<WidgetId, WidgetUiStateData>()
private var homeContentList = mutableListOf<WidgetId>()
fun updateHomePageData(
widgetResponse: WidgetResponse,
updateHomePageSuccess: (WidgetResponse) -> Unit
) {
val content = widgetResponse.contentWidget
when (widgetUiStateMap.isEmpty()) {
true -> {
if (content != null) {
val homeContent = mutableListOf<WidgetId>()
content.forEach { naviWidget ->
val widgetId = naviWidget.widgetId.orEmpty()
homeContent.add(widgetId)
widgetUiStateMap[widgetId] = WidgetUiStateData(
widgetUiState = WidgetUiState.VISIBLE,
widgetData = (naviWidget as NaviUiTronWidget).uiTronWidget
)
}
homeContentList = homeContent
}
}
false -> {
if (content != null) {
homeContentList = constructHomePageList(
oldData = existingHomePageWidgets.toMutableList(),
newData = content.toMutableList()
)
}
}
}
//Update home data
updateHomePageSuccess.invoke(widgetResponse)
//Replace the existing widgets to new once
existingHomePageWidgets =
content?.toMutableList() ?: mutableListOf()
}
private fun constructHomePageList(
oldData: MutableList<NaviWidget>,
newData: MutableList<NaviWidget>
): MutableList<WidgetId> {
val updatedHomeList: MutableList<WidgetId> = mutableListOf()
val seenElements = mutableSetOf<WidgetId>()
//For addition of new widgets compared to previous data
newData.forEach { naviWidget ->
val widgetId = naviWidget.widgetId.orEmpty()
seenElements.add(widgetId)
val uiState = if (oldData.any { it.widgetId.orEmpty() == widgetId }) {
WidgetUiState.VISIBLE
} else {
WidgetUiState.NEWLY_ADDED
}
widgetUiStateMap[widgetId] = WidgetUiStateData(
uiState,
(naviWidget as NaviUiTronWidget).uiTronWidget
)
updatedHomeList.add(widgetId)
}
//For deletion of existing widgets compared to new data
oldData.forEachIndexed { index, naviWidget ->
val widgetId = naviWidget.widgetId.orEmpty()
if (widgetId !in seenElements) {
seenElements.add(widgetId)
widgetUiStateMap[widgetId]?.apply {
widgetUiState = WidgetUiState.NOT_VISIBLE
}
updatedHomeList.getOrNull(index)?.let {
updatedHomeList.add(index, widgetId)
} ?: updatedHomeList.add(widgetId)
}
}
return updatedHomeList
}
fun getWidgetUiStateMap() = widgetUiStateMap
fun getHomeContentList() = homeContentList
}

View File

@@ -0,0 +1,57 @@
package com.naviapp.home.common.handler
import com.navi.uitron.model.action.PublishEventAction
import com.navi.uitron.model.action.PublishableEventData
import com.navi.uitron.viewmodel.UiTronViewModel
import javax.inject.Inject
class SelectiveRefreshHandler @Inject constructor() {
fun handleSuccessState(homeVM: UiTronViewModel) {
homeVM.handleAction(
PublishEventAction(
events = listOf(
PublishableEventData(
eventName = SELECTIVE_REFRESH_SUCCESS,
stateKey = SUCCESS_STATE
)
)
)
)
}
fun handleErrorState(homeVM: UiTronViewModel) {
homeVM.handleAction(
PublishEventAction(
events = listOf(
PublishableEventData(
eventName = SELECTIVE_REFRESH_ERROR,
stateKey = ERROR_STATE
)
)
)
)
}
fun handleLoadingState(homeVM: UiTronViewModel) {
homeVM.handleAction(
PublishEventAction(
events = listOf(
PublishableEventData(
eventName = SELECTIVE_REFRESH_LOADING,
stateKey = LOADING_STATE
)
)
)
)
}
companion object {
const val SELECTIVE_REFRESH_SUCCESS = "selective_refresh_success"
const val SELECTIVE_REFRESH_ERROR = "selective_refresh_error"
const val SELECTIVE_REFRESH_LOADING = "selective_refresh_loading"
const val SUCCESS_STATE = "success_state"
const val ERROR_STATE = "error_state"
const val LOADING_STATE = "loading_state"
}
}

View File

@@ -30,6 +30,7 @@ import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.common.viewmodel.BottomNavBarVM
import com.naviapp.common.viewmodel.InAppUpdateVM
import com.naviapp.dashboard.viewmodels.DashboardSharedVM
import com.naviapp.home.common.handler.InitActionsHandler
import com.naviapp.home.compose.components.BottomBarWithFabButton
import com.naviapp.home.compose.components.HomePageNavHost
import com.naviapp.home.compose.homescreen.onTabClick
@@ -62,6 +63,12 @@ fun HomePageActivityMainScreen(
) {
val navController = rememberNavController()
val selectedTabId by sharedVM.selectedTabId.collectAsStateWithLifecycle()
InitActionsHandler(
viewModel = homeVM,
activity = homePageActivity,
naviAnalyticsEventTracker = naviHomeAnalytics,
isPaymentLoaderShowing = paymentVM.isPaymentLoaderShowing()
)
LaunchedEffect(key1 = selectedTabId) {
onTabSelected.invoke(selectedTabId)

View File

@@ -1,20 +1,23 @@
package com.naviapp.home.compose.homescreen
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.Image
import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -22,6 +25,8 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
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.Modifier
@@ -33,6 +38,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.sharedpref.PreferenceManager
import com.navi.common.model.common.CollapsingTopNavConfig
@@ -40,134 +46,119 @@ import com.navi.common.model.common.Header
import com.navi.common.uitron.model.action.LayoutStateIds
import com.navi.common.uitron.model.action.UpdateViewStateActionV2
import com.navi.common.utils.getSessionId
import com.navi.naviwidgets.models.NaviUiTronWidget
import com.navi.naviwidgets.models.NaviWidget
import com.navi.pay.utils.conditional
import com.navi.uitron.UiTronSdkManager
import com.navi.uitron.render.UiTronRenderer
import com.navi.uitron.utils.toPx
import com.naviapp.R
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.home.common.handler.WidgetId
import com.naviapp.home.compose.extension.bottomShadow
import com.naviapp.home.utils.shimmerEffect
import com.naviapp.home.model.WidgetUiState
import com.naviapp.home.utils.getHomeWidgetAnimationSpec
import com.naviapp.home.viewmodel.HomeVM
import com.naviapp.utils.Constants
import com.naviapp.utils.Constants.SECTION_CONTENT_SIZE
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/*
Order of composition: UpperContent and ContentLoader Lottie -> BackLayer and TopAppBar -> UpperMiddleContent -> LowerMiddleContent -> LowerContent -> ProfileScreen
Added some delay after rendering each layer to get some time to take clicks
/**
* Order of composition:
*
* 1. UpperContent and ContentLoader Lottie
* 2. BackLayer and TopAppBar
* 3. UpperMiddleContent
* 4. LowerMiddleContent
* 5. LowerContent
* 6. ProfileScreen
*/
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun frontLayerContent(modifier: Modifier, frontLayerData: List<NaviWidget>?, homeVM: HomeVM) =
@Composable {
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
Column(modifier) {
frontLayerData?.let {
UpperContent(homeVM, it.take(homeVM.upperContentSize))
fun FrontLayerContent(
modifier: Modifier,
isShimmerVisible: Boolean,
homeVM: HomeVM
) {
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
Column(modifier) {
if (isShimmerVisible.not()) {
val widgetOrderList = homeVM.getHomeContentList()
Column {
UpperContent(homeVM) {
RenderUiTronContent(widgetOrderList.take(homeVM.upperContentSize), homeVM)
val middleAndLowerContent = it.drop(homeVM.upperContentSize)
UpperMiddleContent(homeVM, middleAndLowerContent.take(SECTION_CONTENT_SIZE))
}
val middleAndLowerContent = widgetOrderList.drop(homeVM.upperContentSize)
UpperMiddleContent(homeVM) {
RenderUiTronContent(
middleAndLowerContent.take(Constants.SECTION_CONTENT_SIZE), homeVM
)
}
val lowerContent = middleAndLowerContent.drop(Constants.SECTION_CONTENT_SIZE)
LowerMiddleContent(homeVM) {
RenderUiTronContent(
lowerContent.take(Constants.SECTION_CONTENT_SIZE), homeVM
)
val lowerContent = middleAndLowerContent.drop(SECTION_CONTENT_SIZE)
LowerMiddleContent(homeVM, lowerContent.take(SECTION_CONTENT_SIZE))
LowerContent(homeVM, lowerContent.drop(SECTION_CONTENT_SIZE))
ContentLoaderLottie(homeVM)
} ?: run {
DefaultContentShimmer()
}
LowerContent(homeVM) {
RenderUiTronContent(
lowerContent.drop(Constants.SECTION_CONTENT_SIZE), homeVM
)
}
ContentLoaderLottie(homeVM = homeVM)
}
} else run {
HomePageContentShimmer()
}
}
}
}
@Composable
fun DefaultContentShimmer() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp)
) {
Spacer(modifier = Modifier.height(24.dp))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.height(34.dp)
.width(94.dp)
.shimmerEffect()
)
Box(
modifier = Modifier
.height(34.dp)
.width(148.dp)
.shimmerEffect()
)
private fun RenderUiTronContent(
elementList: List<WidgetId>,
homeVM: HomeVM
) {
elementList.forEach { element ->
key(element) {
val widgetUiMap = homeVM.getWidgetUiStateMap()
val visible = remember(widgetUiMap[element]?.widgetUiState) {
mutableStateOf(widgetUiMap[element]?.widgetUiState == WidgetUiState.VISIBLE)
}
LaunchedEffect(widgetUiMap[element]?.widgetUiState) {
if (widgetUiMap[element]?.widgetUiState == WidgetUiState.NEWLY_ADDED) {
visible.value = true
widgetUiMap[element].apply {
this?.widgetUiState = WidgetUiState.VISIBLE
}
}
}
AnimatedContainerForWidgets(isVisible = visible) {
val response = homeVM.getWidgetUiStateMap()[element]?.widgetData
UiTronRenderer(
dataMap = response?.data, uiTronViewModel = homeVM
).Render(composeViews = response?.parentComposeView.orEmpty())
}
}
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier
.height(108.dp)
.fillMaxWidth()
.shimmerEffect()
)
Spacer(modifier = Modifier.height(32.dp))
Box(
modifier = Modifier
.height(24.dp)
.width(176.dp)
.shimmerEffect()
)
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier
.height(132.dp)
.fillMaxWidth()
.shimmerEffect()
)
}
}
Spacer(modifier = Modifier.height(32.dp))
Box(
modifier = Modifier
.height(24.dp)
.width(120.dp)
.shimmerEffect()
)
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier
.height(104.dp)
.fillMaxWidth()
.shimmerEffect()
)
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier
.height(78.dp)
.fillMaxWidth()
.shimmerEffect()
)
Spacer(modifier = Modifier.height(32.dp))
Box(
modifier = Modifier
.height(24.dp)
.width(176.dp)
.shimmerEffect()
)
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier
.height(132.dp)
.fillMaxWidth()
.shimmerEffect()
)
@Composable
private fun AnimatedContainerForWidgets(
isVisible: MutableState<Boolean>, homeWidget: @Composable () -> Unit
) {
AnimatedVisibility(visible = isVisible.value, 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()
}
}
@@ -190,7 +181,7 @@ fun ContentLoaderLottie(
)
LottieAnimation(
composition = composition,
iterations = 5000
iterations = LottieConstants.IterateForever
)
}
} else {
@@ -201,27 +192,27 @@ fun ContentLoaderLottie(
@Composable
fun UpperContent(
homeVM: HomeVM,
content: List<NaviWidget>,
upperContent: @Composable () -> Unit,
) {
LaunchedEffect(Unit) {
homeVM.logAppLaunchTimeEvent(Constants.HOME_SCREEN_IN_CAPS)
// Show back layer and top app bar
homeVM.setIsReadyToRenderBackLayerAndTopAppBar(true)
}
RenderUiTronContent(content = content, homeVM = homeVM)
upperContent()
}
@Composable
fun UpperMiddleContent(
homeVM: HomeVM,
content: List<NaviWidget>,
upperMiddleContent: @Composable () -> Unit,
) {
val showUpperMiddleLayer by homeVM.isReadyToRenderUpperMiddleContent.collectAsStateWithLifecycle()
if (showUpperMiddleLayer) {
LaunchedEffect(Unit) {
homeVM.setIsReadyToRenderLowerMiddleContent(true)
}
RenderUiTronContent(content = content, homeVM = homeVM)
upperMiddleContent()
}
}
@@ -229,21 +220,21 @@ fun UpperMiddleContent(
@Composable
fun LowerMiddleContent(
homeVM: HomeVM,
content: List<NaviWidget>,
lowerMiddleContent: @Composable () -> Unit,
) {
val showLowerMiddleLayer by homeVM.isReadyToRenderLowerMiddleContent.collectAsStateWithLifecycle()
if (showLowerMiddleLayer) {
LaunchedEffect(Unit) {
homeVM.setIsReadyToRenderLowerContent(true)
}
RenderUiTronContent(content = content, homeVM = homeVM)
lowerMiddleContent()
}
}
@Composable
fun LowerContent(
homeVM: HomeVM,
content: List<NaviWidget>,
lowerContent: @Composable () -> Unit,
) {
val showLowerFrontLayer by homeVM.isReadyToRenderLowerContent.collectAsStateWithLifecycle()
if (showLowerFrontLayer) {
@@ -252,41 +243,28 @@ fun LowerContent(
homeVM.setIsShowContentLoader(false)
homeVM.setIsReadyToRenderProfileScreen(true)
}
RenderUiTronContent(content = content, homeVM = homeVM)
lowerContent()
homeVM.logDnDataDisplayedEvent()
}
}
@Composable
fun RenderUiTronContent(
content: List<NaviWidget>,
homeVM: HomeVM
) {
content.forEach { item ->
if (item is NaviUiTronWidget) {
UiTronRenderer(item.uiTronWidget?.data, homeVM)
.Render(composeViews = item.uiTronWidget?.parentComposeView.orEmpty())
}
}
}
fun backLayerComposable(modifier: Modifier, backLayerData: Header?, homeVM: HomeVM) =
@Composable {
Column(modifier.fillMaxWidth()) {
val isShowBackLayer by homeVM.isReadyToRenderBackLayerAndTopAppBar.collectAsStateWithLifecycle()
if (isShowBackLayer) {
LaunchedEffect(Unit) {
homeVM.setIsReadyToRenderUpperMiddleContent(true)
}
backLayerData?.collapsingTopNav?.uiTronResponse?.let {
UiTronRenderer(
it.data,
homeVM
).Render(composeViews = it.parentComposeView.orEmpty())
}
fun BackLayerComposable(modifier: Modifier, backLayerData: Header?, homeVM: HomeVM) {
Column(modifier.fillMaxWidth()) {
val isShowBackLayer by homeVM.isReadyToRenderBackLayerAndTopAppBar.collectAsStateWithLifecycle()
if (isShowBackLayer) {
LaunchedEffect(Unit) {
homeVM.setIsReadyToRenderUpperMiddleContent(true)
}
backLayerData?.collapsingTopNav?.uiTronResponse?.let {
UiTronRenderer(
it.data,
homeVM
).Render(composeViews = it.parentComposeView.orEmpty())
}
}
}
}
fun topBarLayout(
appBarHeight: Dp,

View File

@@ -0,0 +1,102 @@
package com.naviapp.home.compose.homescreen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.naviapp.home.utils.shimmerEffect
@Composable
fun HomePageContentShimmer() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp)
) {
Spacer(modifier = Modifier.height(24.dp))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.height(34.dp)
.width(94.dp)
.shimmerEffect()
)
Box(
modifier = Modifier
.height(34.dp)
.width(148.dp)
.shimmerEffect()
)
}
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier
.height(108.dp)
.fillMaxWidth()
.shimmerEffect()
)
Spacer(modifier = Modifier.height(32.dp))
Box(
modifier = Modifier
.height(24.dp)
.width(176.dp)
.shimmerEffect()
)
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier
.height(132.dp)
.fillMaxWidth()
.shimmerEffect()
)
Spacer(modifier = Modifier.height(32.dp))
Box(
modifier = Modifier
.height(24.dp)
.width(120.dp)
.shimmerEffect()
)
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier
.height(104.dp)
.fillMaxWidth()
.shimmerEffect()
)
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier
.height(78.dp)
.fillMaxWidth()
.shimmerEffect()
)
Spacer(modifier = Modifier.height(32.dp))
Box(
modifier = Modifier
.height(24.dp)
.width(176.dp)
.shimmerEffect()
)
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier
.height(132.dp)
.fillMaxWidth()
.shimmerEffect()
)
}
}

View File

@@ -167,28 +167,31 @@ fun HomeScreenScaffold(
NaviHomePageScaffold(
appBar =
topBarLayout(
appBarHeight = appBarHeight,
statusBarHeight = statusBarHeight,
topBarData = widgetResponse.header,
homeVM = homeVM,
listState = listState
),
backLayer =
backLayerComposable(
topBarLayout(
appBarHeight = appBarHeight,
statusBarHeight = statusBarHeight,
topBarData = widgetResponse.header,
homeVM = homeVM,
listState = listState
),
backLayer = {
BackLayerComposable(
modifier = Modifier.requiredHeight(backLayerHeight),
backLayerData = widgetResponse.header,
homeVM = homeVM
),
frontLayer =
frontLayerContent(
)
},
frontLayer = {
FrontLayerContent(
modifier =
Modifier.fillMaxHeight()
.background(Color.White, frontLayerShapeRadius)
.verticalScroll(listState()),
frontLayerData = widgetResponse.contentWidget,
Modifier
.fillMaxHeight()
.background(Color.White, frontLayerShapeRadius)
.verticalScroll(listState()),
isShimmerVisible = widgetResponse.contentWidget.isNullOrEmpty(),
homeVM = homeVM
),
)
},
concealedHeight = appBarHeight,
revealedHeight = backLayerHeight - curvature,
gesturesEnabled = gesturesEnabled.value,

View File

@@ -0,0 +1,14 @@
package com.naviapp.home.model
import com.navi.uitron.model.UiTronResponse
enum class WidgetUiState {
VISIBLE,
NOT_VISIBLE,
NEWLY_ADDED
}
data class WidgetUiStateData(
var widgetUiState: WidgetUiState,
var widgetData: UiTronResponse? = null
)

View File

@@ -1,5 +1,7 @@
package com.naviapp.home.utils
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
@@ -118,4 +120,8 @@ fun DrawIcon(
painter = painter,
contentDescription = content
)
}
}
fun <T> getHomeWidgetAnimationSpec(): FiniteAnimationSpec<T> {
return tween(500, easing = CubicBezierEasing(0.83f, 0.17f, 0.23f, 0.89f))
}

View File

@@ -76,6 +76,8 @@ import com.naviapp.common.transformer.AppLoadTimerMapper
import com.naviapp.common.viewmodel.BottomNavBarVM
import com.naviapp.common.viewmodel.InAppUpdateVM
import com.naviapp.home.activity.InAppNotificationActivity
import com.naviapp.home.common.handler.HomePageDataUpdateHandler
import com.naviapp.home.common.handler.SelectiveRefreshHandler
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.compose.listener.HomeScreenCallbackListener
import com.naviapp.home.compose.model.BottomStickyNudgeState
@@ -129,7 +131,9 @@ constructor(
private val gson: Gson,
private val naviCacheRepository: NaviCacheRepositoryImpl,
private val connectivityObserver: ConnectivityObserver,
private val naviPayCustomerStatusHandler: NaviPayCustomerStatusHandler
private val naviPayCustomerStatusHandler: NaviPayCustomerStatusHandler,
private val selectiveRefreshHandler: SelectiveRefreshHandler,
private val homePageDataUpdateHandler: HomePageDataUpdateHandler
) : BaseVM() {
private val _isReadyToRenderBackLayerAndTopAppBar = MutableStateFlow(false)
@@ -418,7 +422,9 @@ constructor(
) {
val needToShowLoader =
TemporaryStorageHelper.isDataModified(screen = TemporaryStorageHelper.HOME)
if (showLoader) {
selectiveRefreshHandler.handleLoadingState(this)
}
if (needToShowLoader) {
coroutineScope.launch(Dispatchers.IO) {
val naviCacheAltSourceEntity =
@@ -438,8 +444,12 @@ constructor(
)
)
deserializeWidgetResponseString(response = getHomeTabFromDatabase())?.let { responseData ->
_homeScreenState.update { HomeScreenState.Success(responseData) }
homePageDataUpdateHandler.updateHomePageData(
responseData,
::updateHomePageSuccess
)
_homeScreenExtraData.emit(HomePageExtraData(responseData.extraData))
selectiveRefreshHandler.handleSuccessState(this@HomeVM)
}
updateApiTsForHomePage()
@@ -463,7 +473,7 @@ constructor(
val homeTabFromDatabase = getHomeTabFromDatabase()
naviCacheRepository
.getDataAndFetchFromAltSource(
.getDataAndFetchFromAltSourceWithDetails(
key = NaviSharedDbKeys.HOME_TAB.name,
version = BuildConfig.VERSION_CODE.toLong(),
getDataFromAltSource = {
@@ -482,28 +492,36 @@ constructor(
emitMultipleValues = true,
)
.collect { response ->
deserializeWidgetResponseString(response)?.let { widgetResponse ->
deserializeWidgetResponseString(response.data)?.let { widgetResponse ->
try {
if (
showLoader.not() &&
getHomeScreenData().isNotNull() &&
response.value == homeTabFromDatabase?.value
response.data?.value == homeTabFromDatabase?.value
) {
updateCachedResponse(true)
} else {
_homeScreenState.update {
HomeScreenState.Success(
widgetResponse
if (response.isCurrentAndAltDataSame.orFalse().not()) {
homePageDataUpdateHandler.updateHomePageData(
widgetResponse,
::updateHomePageSuccess
)
_homeScreenExtraData.emit(
HomePageExtraData(
widgetResponse.extraData
)
)
updateApiTsForHomePage()
response.data?.updatedAt?.let { timestamp ->
homeTabLastUpdateTimestamp = timestamp
}
preCacheLottieUrls(widgetResponse)
}
_homeScreenExtraData.emit(HomePageExtraData(widgetResponse.extraData))
updateApiTsForHomePage()
response.updatedAt.let { timestamp ->
homeTabLastUpdateTimestamp = timestamp
if (response.isComingFromAltSource.orFalse()) {
selectiveRefreshHandler.handleSuccessState(this@HomeVM)
}
}
preCacheLottieUrls(widgetResponse)
}
} catch (e: Exception) {
e.log()
}
@@ -514,6 +532,18 @@ constructor(
}
}
private fun updateHomePageSuccess(widgetResponse: WidgetResponse) {
_homeScreenState.update {
HomeScreenState.Success(
widgetResponse
)
}
}
fun getWidgetUiStateMap() = homePageDataUpdateHandler.getWidgetUiStateMap()
fun getHomeContentList() = homePageDataUpdateHandler.getHomeContentList()
private fun deserializeWidgetResponseString(response: NaviCacheEntity?): WidgetResponse? =
getGsonBuilderForWidgetizedResponse().fromJson(response?.value, WidgetResponse::class.java)
@@ -585,27 +615,30 @@ constructor(
naviUpiDeviceFingerprint = deviceInfoDetails.deviceFingerPrint,
ssid = deviceInfoDetails.provider.ssid
)
if (!response.isValidResponse()) {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
!connectivityObserver.isInternetConnected()
) {
_connectivityObserverHolder.emit(ConnectivityObserver.Status.Unavailable)
} else if (response.error?.statusCode == API_CODE_SOCKET_TIMEOUT) {
selectiveRefreshHandler.handleErrorState(this)
} else if (
response.error?.statusCode != API_CODE_UNKNOWN_HOST &&
response.error?.statusCode != API_CODE_CONNECT_EXCEPTION &&
response.error?.statusCode != NO_INTERNET &&
response.error?.statusCode != API_CODE_SOCKET_TIMEOUT
response.error?.statusCode != NO_INTERNET
) {
val errorUnifiedResponse =
getErrorUnifiedResponse(errors = response.errors, error = response.error)
sendFailureEvent(NaviAnalytics.NEW_HOME_ACTIVITY, errorUnifiedResponse)
_homeScreenState.emit(HomeScreenState.Error(errorUnifiedResponse.errorResponse))
if ((homeScreenState.value is HomeScreenState.Success).not()) {
_homeScreenState.emit(HomeScreenState.Error(errorUnifiedResponse.errorResponse))
} else {
selectiveRefreshHandler.handleErrorState(this)
}
}
return NaviCacheAltSourceEntity(isSuccess = false)
}
return NaviCacheAltSourceEntity(
value = naviAppSerializerGsonBuilder().toJson(response.data),
version = BuildConfig.VERSION_CODE,
@@ -893,9 +926,7 @@ constructor(
isPaymentLoaderShowing: Boolean,
) {
if (BaseUtils.isUserLoggedIn()) {
if (showLoader && !isPaymentLoaderShowing && isProfileDrawerOpen.not()) {
activity.showLoader()
} else {
if (showLoader.not() || isPaymentLoaderShowing || isProfileDrawerOpen) {
naviAnalyticsEventTracker.onHomePageInit(
System.currentTimeMillis() - analyticsStartTs
)
@@ -1067,6 +1098,7 @@ constructor(
_topNavOffsetValue.update { value }
}
}
private fun getHomeScreenData(): WidgetResponse? {
return if (homeScreenState.value is HomeScreenState.Success) {
(homeScreenState.value as HomeScreenState.Success).data

View File

@@ -11,20 +11,20 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.rememberCoroutineScope
import com.google.gson.GsonBuilder
import com.navi.ap.common.viewmodel.ApplicationPlatformVM
import com.navi.ap.utils.registerApUiTronDeSerializers
import com.navi.base.utils.isNotNullAndNotEmpty
import com.navi.uitron.model.action.PublishEventAction
import com.navi.uitron.model.data.SubscriberEventData
import com.navi.uitron.model.event.UiTronDataProviderFactory
import com.navi.uitron.model.ui.BaseProperty
import com.navi.uitron.viewmodel.UiTronViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@Composable
fun HandlePublishEventAction(
viewModel: ApplicationPlatformVM,
viewModel: UiTronViewModel,
uiTronDataProviderFactory: UiTronDataProviderFactory = UiTronDataProviderFactory()
) {
@@ -62,18 +62,14 @@ fun HandlePublishEventAction(
}
}
private fun handlePropertyUpdate(
stateKey: String?,
layoutId: String?,
viewModel: ApplicationPlatformVM
) {
private fun handlePropertyUpdate(stateKey: String?, layoutId: String?, viewModel: UiTronViewModel) {
stateKey?.let { viewModel.handle[layoutId + BaseProperty.PROPERTY_SUFFIX] = it }
}
private fun handleDataUpdate(
uiTronData: Any?,
subscriberData: SubscriberEventData,
viewModel: ApplicationPlatformVM,
viewModel: UiTronViewModel,
uiTronDataProviderFactory: UiTronDataProviderFactory
) {
uiTronData?.let {

View File

@@ -95,7 +95,7 @@ moengage-rich-notification = "4.3.2"
navi-alfred = "1.5.0"
navi-guarddog = "1.10.0"
navi-pulse = "1.3.0"
navi-uitron = "1.7.0"
navi-uitron = "1.8.0"
navigation = "2.5.3"
okhttp-bom = "4.12.0"
otaliastudios-cameraview = "2.7.2"

View File

@@ -0,0 +1,14 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.base.cache.model
data class NaviCacheEntityDetails(
val data: NaviCacheEntity? = null,
val isComingFromAltSource: Boolean? = null,
val isCurrentAndAltDataSame: Boolean? = null
)

View File

@@ -10,6 +10,7 @@ package com.navi.base.cache.repository
import com.navi.base.cache.dao.NaviCacheDao
import com.navi.base.cache.model.NaviCacheAltSourceEntity
import com.navi.base.cache.model.NaviCacheEntity
import com.navi.base.cache.model.NaviCacheEntityDetails
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@@ -45,6 +46,22 @@ interface NaviCacheRepository {
},
emitMultipleValues: Boolean = false
): Flow<NaviCacheEntity?>
fun getDataAndFetchFromAltSourceWithDetails(
key: String,
version: Long? = null,
getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity,
isCurrentAndAltDataSame:
(currentData: NaviCacheEntity?, altData: NaviCacheAltSourceEntity) -> Boolean =
fun(currentData, altData): Boolean {
val currentDataValue = currentData?.value ?: return false
val altDataValue = altData.value ?: return false
return currentData.version == altData.version &&
currentDataValue.hashCode() == altDataValue.hashCode()
},
emitMultipleValues: Boolean = false
): Flow<NaviCacheEntityDetails?>
}
class NaviCacheRepositoryImpl @Inject constructor(private val naviCacheDao: NaviCacheDao) :
@@ -165,6 +182,62 @@ class NaviCacheRepositoryImpl @Inject constructor(private val naviCacheDao: Navi
}
}
override fun getDataAndFetchFromAltSourceWithDetails(
key: String,
version: Long?,
getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity,
isCurrentAndAltDataSame:
(currentData: NaviCacheEntity?, altData: NaviCacheAltSourceEntity) -> Boolean,
emitMultipleValues: Boolean
) = flow {
var isValueEmitted = false
val currentValueInDB = get(key = key)
if (currentValueInDB != null) { // Its a valid value
emit(NaviCacheEntityDetails(data = currentValueInDB, isComingFromAltSource = false))
isValueEmitted = true
}
val naviCacheValueEntityFromAltSource = getDataFromAltSource.invoke()
if (
!naviCacheValueEntityFromAltSource.isSuccess ||
naviCacheValueEntityFromAltSource.value == null
) { // alternate source data invalid
return@flow
}
val naviCacheEntity =
NaviCacheEntity(
key = key,
value = naviCacheValueEntityFromAltSource.value,
version = naviCacheValueEntityFromAltSource.version ?: 1,
ttl = naviCacheValueEntityFromAltSource.ttl,
clearOnLogout = naviCacheValueEntityFromAltSource.clearOnLogout,
metaData = naviCacheValueEntityFromAltSource.metaData
)
val isDataSame =
isCurrentAndAltDataSame(currentValueInDB, naviCacheValueEntityFromAltSource)
if (isDataSame.not()) {
// Data from Alt source is different
save(naviCacheEntity = naviCacheEntity)
}
// If multiple values are required or no value is emitted yet then emit latest value
if (emitMultipleValues || !isValueEmitted) {
emit(
NaviCacheEntityDetails(
data = naviCacheEntity,
isComingFromAltSource = true,
isCurrentAndAltDataSame = isDataSame
)
)
}
}
private suspend fun checkIfDBValueIsValidElseRemoveEntry(
naviCacheEntity: NaviCacheEntity?,
version: Long?,

View File

@@ -22,6 +22,7 @@ import com.navi.common.uitron.model.action.RedeemCoinAction
import com.navi.common.uitron.model.action.RewardsAndReferralApiAction
import com.navi.common.uitron.model.action.SdkExitAction
import com.navi.common.uitron.model.action.SubmitFeedbackAction
import com.navi.common.uitron.model.action.V3HomeAction
import com.navi.common.uitron.model.action.ValidateVpaAction
import com.navi.uitron.deserializer.BaseUiTronTriggerApiActionDeserializer
import com.navi.uitron.model.action.TriggerApiAction
@@ -63,6 +64,7 @@ class UiTronTriggerApiActionDeserializer : BaseUiTronTriggerApiActionDeserialize
context?.deserialize(jsonObject, ValidateVpaAction::class.java)
ApiType.RedeemCoins.name ->
context?.deserialize(jsonObject, RedeemCoinAction::class.java)
ApiType.V3Home.name -> context?.deserialize(jsonObject, V3HomeAction::class.java)
else -> super.deserialize(json, typeOfT, context)
}
}

View File

@@ -60,7 +60,8 @@ enum class ApiType {
SdkExitAction,
RewardsAndReferralAction,
ValidateVpa,
RedeemCoins
RedeemCoins,
V3Home
}
enum class SourceType {

View File

@@ -0,0 +1,12 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.navi.common.uitron.model.action
import com.navi.uitron.model.action.TriggerApiAction
data class V3HomeAction(val showLoader: Boolean? = null) : TriggerApiAction()