NTP-40266 | Revamp for home content deserialization (#15038)

Signed-off-by: Naman Khurmi <naman.khurmi@navi.com>
This commit is contained in:
Naman Khurmi
2025-02-20 17:05:01 +05:30
committed by GitHub
parent e72ca383c1
commit 966c0c5d38
6 changed files with 372 additions and 136 deletions

View File

@@ -129,7 +129,7 @@ android {
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
freeCompilerArgs += ["-Xstring-concat=inline"]
freeCompilerArgs += ["-Xstring-concat=inline","-Xcontext-receivers"]
jvmTarget = '17'
}
lint {

View File

@@ -87,7 +87,7 @@ class HomeReducer : BaseReducer<HpStates, HpEvents> {
is HpEvents.UpdateFrontLayerContent -> {
val updatedList =
updateScreenContent(
renderingFirstTime = previousState.isRenderingFirstTime,
renderingFirstTime = event.isRenderingFirstTime,
newWidgets = event.content,
oldWidgets = previousState.frontLayerContent,
)
@@ -100,7 +100,7 @@ class HomeReducer : BaseReducer<HpStates, HpEvents> {
is HpEvents.UpdatePrioritySectionData -> {
val updatedList =
updateScreenContent(
renderingFirstTime = previousState.isRenderingFirstTime,
renderingFirstTime = true,
newWidgets =
event.prioritySectionData.content ?: previousState.frontLayerContent,
oldWidgets = previousState.frontLayerContent,
@@ -157,7 +157,8 @@ sealed interface HpEvents : UiEvent {
data object RenderedFirstTime : HpEvents
data class UpdateFrontLayerContent(
val content: List<AlchemistWidgetModelDefinition<UiTronResponse>>
val content: List<AlchemistWidgetModelDefinition<UiTronResponse>>,
val isRenderingFirstTime: Boolean,
) : HpEvents
data class UpdatePrioritySectionData(val prioritySectionData: HomePrioritySectionData) :

View File

@@ -0,0 +1,313 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.usecase
import com.navi.analytics.utils.NaviTrackEvent
import com.navi.base.cache.model.NaviCacheAltSourceEntity
import com.navi.base.cache.model.NaviCacheEntity
import com.navi.base.cache.model.NaviCacheEntityInfo
import com.navi.base.cache.repository.NaviCacheRepositoryImpl
import com.navi.base.cache.util.NaviSharedDbKeys
import com.navi.common.alchemist.model.AlchemistScreenDefinition
import com.navi.common.utils.safeAsync
import com.navi.common.utils.safeLaunch
import com.naviapp.BuildConfig
import com.naviapp.home.reducer.HpEvents
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
class HomeContentProcessingUseCase
@Inject
constructor(
private val naviCacheRepository: NaviCacheRepositoryImpl,
private val deserializer: AsyncDeserialization,
) {
companion object {
private const val EVENT_NAME = "naviapp_home_content_processing_event"
private const val EVENT_KEY_CURRENT_STATUS = "currentStatus"
// Cache related events
private const val CACHE_RETRIEVAL_NO_CONTENT = "Cache retrieval - No cached content found"
private const val CACHE_CONTENT_PROCESSING_START = "Starting cache content processing"
private const val CACHE_PRIORITY_SECTION_EXTRACTED = "Priority section extracted"
private const val CACHE_SCREEN_LAYOUT_PROCESSED = "Screen layout processed"
private const val CACHE_REMAINING_CONTENT_PROCESSED = "Remaining content processed"
private const val CACHE_CONTENT_PROCESSING_COMPLETED = "Cache content processing completed"
private const val CACHE_DATA_NOT_FOUND = "Home cache data not found during first launch"
private const val CACHE_DATA_LOADED =
"Home cache data loaded successfully during first launch"
private const val CACHE_DATA_RETRIEVAL_START = "Starting cache data retrieval"
// Network related events
private const val NETWORK_FETCH_START = "Starting network data fetch"
private const val NETWORK_SUBSEQUENT_FETCH_START = "Starting subsequent network fetch"
private const val NETWORK_RESPONSE_NULL = "Network response is null - Context: %s"
private const val NETWORK_ERROR = "Network error - Context: %s"
private const val NETWORK_SUCCESS = "Network success - Context: %s"
// Sync related events
private const val SYNC_STARTED = "Home sync started - First time render: %s"
private const val SYNC_CACHE_OPERATION_COMPLETED =
"Cache operation completed - Data loaded: %s"
private const val SYNC_FIRST_RENDER_NETWORK = "First render completed from network data"
private const val SYNC_REFRESH_CONTENT =
"Refreshing content - First render: %s, Widgets: %d"
}
private fun statusTrackEvent(event: String) {
NaviTrackEvent.trackEvent(
EVENT_NAME,
eventValues = mapOf(EVENT_KEY_CURRENT_STATUS to event),
)
}
private fun statusTrackEventWithContext(eventTemplate: String, vararg args: Any) {
statusTrackEvent(eventTemplate.format(*args))
}
private fun refreshHomeScreenContent(
screenDefinition: AlchemistScreenDefinition,
eventHandler: (HpEvents) -> Unit,
isRenderingFirstTime: Boolean,
) {
eventHandler(HpEvents.UpdateScreenWithoutContent(screenDefinition))
screenDefinition.screenStructure?.content?.widgets?.let { homeWidgets ->
eventHandler(HpEvents.UpdateFrontLayerContent(homeWidgets, isRenderingFirstTime))
}
}
private suspend fun retrieveHomeCacheData(
coroutineScope: CoroutineScope,
eventHandler: (HpEvents) -> Unit,
onCompletion: (NaviCacheEntity?) -> Unit,
) {
val cachedHomeContent = naviCacheRepository.get(NaviSharedDbKeys.HOME_TAB.name)
if (cachedHomeContent == null) {
statusTrackEvent(CACHE_RETRIEVAL_NO_CONTENT)
onCompletion(null)
return
}
statusTrackEvent(CACHE_CONTENT_PROCESSING_START)
deserializer.setCacheEntity(cachedHomeContent.value)
val priorityContentSection = deserializer.getPrioritySection()
eventHandler(HpEvents.UpdatePrioritySectionData(priorityContentSection))
statusTrackEvent(CACHE_PRIORITY_SECTION_EXTRACTED)
val screenLayoutDeferred =
coroutineScope.safeAsync {
val screenLayout = deserializer.getScreenDefinitionWithoutContent()
statusTrackEvent(CACHE_SCREEN_LAYOUT_PROCESSED)
screenLayout?.let {
eventHandler(HpEvents.UpdateScreenWithoutContent(screenLayout))
}
}
coroutineScope
.safeAsync {
val remainingContent = deserializer.getRemainingContent()
statusTrackEvent(CACHE_REMAINING_CONTENT_PROCESSED)
remainingContent?.let { content ->
screenLayoutDeferred.await()
eventHandler(
HpEvents.UpdateFrontLayerContent(
content = content,
isRenderingFirstTime = true,
)
)
}
}
.await()
statusTrackEvent(CACHE_CONTENT_PROCESSING_COMPLETED)
onCompletion(cachedHomeContent)
}
context(CoroutineScope)
fun synchronizeHomeContent(
isFirstTimeRender: Boolean,
fetchHomeContent: suspend () -> NaviCacheAltSourceEntity,
onContentSynchronized: () -> Unit,
eventHandler: (HpEvents) -> Unit,
) {
statusTrackEventWithContext(SYNC_STARTED, isFirstTimeRender)
if (isFirstTimeRender) {
handleFirstTimeRender(fetchHomeContent, onContentSynchronized, eventHandler)
} else {
handleSubsequentFetch(fetchHomeContent, onContentSynchronized, eventHandler)
}
}
private fun CoroutineScope.handleFirstTimeRender(
fetchHomeContent: suspend () -> NaviCacheAltSourceEntity,
onContentSynchronized: () -> Unit,
eventHandler: (HpEvents) -> Unit,
) {
var cacheDataLoaded = false
val cacheLoadOperation =
launchCacheLoad(
eventHandler = eventHandler,
onContentSynchronized = onContentSynchronized,
onCacheLoaded = { cacheDataLoaded = it },
)
launchNetworkFetchAndProcess(
fetchHomeContent = fetchHomeContent,
cacheLoadOperation = cacheLoadOperation,
cacheDataLoaded = cacheDataLoaded,
onContentSynchronized = onContentSynchronized,
eventHandler = eventHandler,
)
}
private fun CoroutineScope.handleSubsequentFetch(
fetchHomeContent: suspend () -> NaviCacheAltSourceEntity,
onContentSynchronized: () -> Unit,
eventHandler: (HpEvents) -> Unit,
) {
safeLaunch(Dispatchers.IO) {
statusTrackEvent(NETWORK_SUBSEQUENT_FETCH_START)
val networkResponse = fetchNetworkData(fetchHomeContent)
processNetworkResponse(
networkResponse = networkResponse,
contextType = ResponseContext.SUBSEQUENT_FETCH,
eventHandler = eventHandler,
onSuccess = { screenContent ->
screenContent?.let {
refreshScreenWithTracking(
screenContent = it,
eventHandler = eventHandler,
isFirstRender = false,
onContentSynchronized = onContentSynchronized,
)
}
},
)
}
}
private fun CoroutineScope.launchCacheLoad(
eventHandler: (HpEvents) -> Unit,
onContentSynchronized: () -> Unit,
onCacheLoaded: (Boolean) -> Unit,
): Deferred<Unit?> =
safeAsync(Dispatchers.IO) {
statusTrackEvent(CACHE_DATA_RETRIEVAL_START)
retrieveHomeCacheData(coroutineScope = this, eventHandler = eventHandler) { cachedEntity
->
if (cachedEntity == null) {
statusTrackEvent(CACHE_DATA_NOT_FOUND)
onCacheLoaded(false)
} else {
statusTrackEvent(CACHE_DATA_LOADED)
onCacheLoaded(true)
onContentSynchronized()
eventHandler(HpEvents.RenderedFirstTime)
}
}
}
private fun CoroutineScope.launchNetworkFetchAndProcess(
fetchHomeContent: suspend () -> NaviCacheAltSourceEntity,
cacheLoadOperation: Deferred<Unit?>,
cacheDataLoaded: Boolean,
onContentSynchronized: () -> Unit,
eventHandler: (HpEvents) -> Unit,
) {
safeLaunch(Dispatchers.IO) {
statusTrackEvent(NETWORK_FETCH_START)
val networkResponse = fetchNetworkData(fetchHomeContent)
processNetworkResponse(
networkResponse = networkResponse,
contextType = ResponseContext.FIRST_LAUNCH,
eventHandler = eventHandler,
onSuccess = { parsedScreenContent ->
cacheLoadOperation.await()
statusTrackEventWithContext(SYNC_CACHE_OPERATION_COMPLETED, cacheDataLoaded)
parsedScreenContent?.let {
refreshScreenWithTracking(
screenContent = it,
eventHandler = eventHandler,
isFirstRender = !cacheDataLoaded,
onContentSynchronized = onContentSynchronized,
)
if (!cacheDataLoaded) {
statusTrackEvent(SYNC_FIRST_RENDER_NETWORK)
eventHandler(HpEvents.RenderedFirstTime)
}
}
},
)
}
}
private suspend fun fetchNetworkData(fetchHomeContent: suspend () -> NaviCacheAltSourceEntity) =
naviCacheRepository.fetchFromAltSourceWithSimilarityCheck(
key = NaviSharedDbKeys.HOME_TAB.name,
version = BuildConfig.VERSION_CODE.toLong(),
getDataFromAltSource = fetchHomeContent,
)
private suspend fun processNetworkResponse(
networkResponse: NaviCacheEntityInfo?,
contextType: ResponseContext,
eventHandler: (HpEvents) -> Unit,
onSuccess: suspend (AlchemistScreenDefinition?) -> Unit,
) {
when {
networkResponse == null -> {
statusTrackEventWithContext(NETWORK_RESPONSE_NULL, contextType)
}
networkResponse.isError -> {
statusTrackEventWithContext(NETWORK_ERROR, contextType)
if (contextType == ResponseContext.FIRST_LAUNCH) {
eventHandler(HpEvents.TriggerErrorState)
}
}
else -> {
statusTrackEventWithContext(NETWORK_SUCCESS, contextType)
val parsedContent = networkResponse.data?.let { deserializer.getScreen(it.value) }
onSuccess(parsedContent)
}
}
}
private fun refreshScreenWithTracking(
screenContent: AlchemistScreenDefinition,
eventHandler: (HpEvents) -> Unit,
isFirstRender: Boolean,
onContentSynchronized: () -> Unit,
) {
statusTrackEventWithContext(
SYNC_REFRESH_CONTENT,
isFirstRender,
screenContent.screenStructure?.content?.widgets?.size ?: 0,
)
refreshHomeScreenContent(
screenDefinition = screenContent,
eventHandler = eventHandler,
isRenderingFirstTime = isFirstRender,
)
onContentSynchronized()
}
private enum class ResponseContext {
FIRST_LAUNCH,
SUBSEQUENT_FETCH,
}
}

View File

@@ -39,10 +39,10 @@ import com.naviapp.home.reducer.HpEffects
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.reducer.HpStates
import com.naviapp.home.respository.HomeRepository
import com.naviapp.home.usecase.AsyncDeserialization
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.models.response.NotificationSettings
import com.naviapp.nux.handler.NewUserExperienceHandler
import com.naviapp.utils.Constants.HomePageConstants.FETCH_HOME_ITEMS_TIMEOUT
@@ -53,13 +53,10 @@ import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout
@HiltViewModel
@@ -68,7 +65,6 @@ class HomeViewModel
constructor(
private val homeRepository: HomeRepository,
private val naviCacheRepository: NaviCacheRepositoryImpl,
private val deserializer: AsyncDeserialization,
private val fetchHomeItemsUseCase: FetchHomeItemsUseCase,
private val selectiveRefreshHandler: SelectiveRefreshHandler,
private val ctaHandler: HandleCtaUseCase,
@@ -78,18 +74,17 @@ constructor(
val videoViewHelper: Lazy<VideoViewHelper>,
val sectionVisibilityTracker: HomePageSectionImpressionTracker,
val postRenderTaskExecutor: PostRenderTaskExecutor,
private val homeContentProcessingUseCase: HomeContentProcessingUseCase,
) :
BaseMviViewModel<HpStates, HpEvents, HpEffects>(
initialState = HpStates(),
reducer = HomeReducer(),
) {
var shouldRefreshHomeApi: Boolean = false
private var homeTabLastUpdateTimestamp: Long = System.currentTimeMillis()
private var homePageRefreshJob: Job? = null
val naviAnalyticsEventTracker by lazy { NaviAnalytics.naviAnalytics.Home() }
private var analyticsStartTs = System.currentTimeMillis()
private var isHomeUIRenderedEventLogged = false
private val mutex = Mutex()
var isErrorNaeTriggered = false
// Internet Connectivity
@@ -132,12 +127,12 @@ constructor(
}
}
private suspend fun fetchHomeDataFromApi(naeScreenName: String) =
private suspend fun fetchHomeDataFromApi(naeScreenName: String, screenHash: String? = null) =
fetchHomeItemsUseCase.fetchHomeItemFromAPI(
connectivityObserver = connectivityObserver,
availableAppVersionCode =
PreferenceManager.getIntPreferenceApp(CURRENT_VERSION_IN_STORE),
screenHash = null,
screenHash = screenHash,
naeScreenName = naeScreenName,
onFailure = { errorMessage, errors ->
setEffect { HpEffects.OnApiFailure(errorMessage, errors) }
@@ -180,11 +175,7 @@ constructor(
}
naviAnalyticsEventTracker.onHomePageApiCall()
analyticsStartTs = System.currentTimeMillis()
fetchHomeDataFromCache(
availableAppVersionCode =
PreferenceManager.getIntPreferenceApp(CURRENT_VERSION_IN_STORE),
naeScreenName = naeScreenName,
)
fetchHomeDataFromCache(naeScreenName = naeScreenName)
}
private fun handleLoggedOutUser(activity: HomePageActivity) {
@@ -196,17 +187,25 @@ constructor(
)
}
private fun fetchHomeDataFromCache(availableAppVersionCode: Int?, naeScreenName: String) {
private fun fetchHomeDataFromCache(naeScreenName: String) {
if (homePageRefreshJob?.isActive == true) return
homePageRefreshJob =
viewModelScope.safeLaunch(Dispatchers.IO) {
handleDataModification()
val screenHash = state.value.screenMetaData?.get(SCREEN_HASH)
fetchAndHandleCacheData(
availableAppVersionCode = availableAppVersionCode,
screenHash = screenHash,
naeScreenName = naeScreenName,
)
with(viewModelScope) {
homeContentProcessingUseCase.synchronizeHomeContent(
isFirstTimeRender = state.value.isRenderingFirstTime,
eventHandler = { sendEvent(it) },
onContentSynchronized = { finishProcessing() },
fetchHomeContent = {
fetchHomeDataFromApi(
naeScreenName = naeScreenName,
screenHash = screenHash,
)
},
)
}
}
}
@@ -216,96 +215,9 @@ constructor(
}
}
private suspend fun fetchAndHandleCacheData(
availableAppVersionCode: Int?,
screenHash: String?,
naeScreenName: String,
) {
naviCacheRepository
.getDataAndFetchFromAltSourceWithSimilarityCheck(
key = NaviSharedDbKeys.HOME_TAB.name,
version = BuildConfig.VERSION_CODE.toLong(),
getDataFromAltSource = {
fetchHomeItemsUseCase.fetchHomeItemFromAPI(
connectivityObserver = connectivityObserver,
availableAppVersionCode = availableAppVersionCode,
screenHash = screenHash,
naeScreenName = naeScreenName,
onFailure = { errorMessage, errors ->
setEffect { HpEffects.OnApiFailure(errorMessage, errors) }
},
noInternetCallback = {
viewModelScope.safeLaunch(Dispatchers.IO) {
_internetConnectivity.emit(ConnectivityObserver.Status.Unavailable)
}
},
)
},
)
.collect { response ->
viewModelScope.safeLaunch(Dispatchers.IO) {
mutex.withLock {
if (response.isError) {
sendEvent(HpEvents.TriggerErrorState)
}
response.data?.let { data ->
if (state.value.isRenderingFirstTime) {
handleFirstLoad(data)
} else handleSubsequentLoad(data)
}
}
}
}
}
private suspend fun handleFirstLoad(data: NaviCacheEntity) {
deserializer.setCacheEntity(data.value)
viewModelScope
.async {
sendEvent(HpEvents.UpdatePrioritySectionData(deserializer.getPrioritySection()))
}
.await()
viewModelScope
.async(Dispatchers.IO) {
val screenDefinitionWithoutContentDeferred = launch {
deserializer.getScreenDefinitionWithoutContent()?.let { screen ->
sendEvent(HpEvents.UpdateScreenWithoutContent(screen))
}
}
launch {
deserializer.getRemainingContent()?.let { remainingItems ->
screenDefinitionWithoutContentDeferred.invokeOnCompletion {
sendEvent(HpEvents.UpdateFrontLayerContent(remainingItems))
sendEvent(HpEvents.RenderedFirstTime)
}
}
}
}
.await()
finishProcessing(data)
}
private suspend fun handleSubsequentLoad(data: NaviCacheEntity) {
viewModelScope
.async {
val screen = deserializer.getScreen(data.value)
screen.let {
sendEvent(HpEvents.UpdateScreenWithoutContent(it))
it.screenStructure?.let { struct ->
struct.content?.widgets?.let { widgets ->
sendEvent(HpEvents.UpdateFrontLayerContent(widgets))
}
}
}
}
.await()
finishProcessing(data)
}
private fun finishProcessing(data: NaviCacheEntity) {
private fun finishProcessing() {
setEffect { HpEffects.OnRenderActions }
updateHomeApiTimestamp()
data.updatedAt.let { homeTabLastUpdateTimestamp = it }
selectiveRefreshHandler.handleSuccessState(this@HomeViewModel)
}

View File

@@ -11,7 +11,6 @@ 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.NaviCacheEntityInfo
import com.navi.base.utils.orTrue
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -55,7 +54,7 @@ interface NaviCacheRepository {
emitOnFailure: Boolean = false,
): Flow<NaviCacheEntity?>
fun getDataAndFetchFromAltSourceWithSimilarityCheck(
suspend fun fetchFromAltSourceWithSimilarityCheck(
key: String,
version: Long? = null,
getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity,
@@ -67,7 +66,7 @@ interface NaviCacheRepository {
return currentData.version == altData.version && currentDataValue == altDataValue
},
): Flow<NaviCacheEntityInfo?>
): NaviCacheEntityInfo?
suspend fun getLastUpdatedTime(key: String): Long
}
@@ -214,30 +213,22 @@ class NaviCacheRepositoryImpl @Inject constructor(private val naviCacheDao: Navi
}
}
override fun getDataAndFetchFromAltSourceWithSimilarityCheck(
override suspend fun fetchFromAltSourceWithSimilarityCheck(
key: String,
version: Long?,
getDataFromAltSource: suspend () -> NaviCacheAltSourceEntity,
isCurrentAndAltDataSame:
(currentData: NaviCacheEntity?, altData: NaviCacheAltSourceEntity) -> Boolean,
) = flow {
): NaviCacheEntityInfo? {
val currentValueInDB = get(key = key)
if (currentValueInDB != null) { // Its a valid value
emit(
NaviCacheEntityInfo(
data = currentValueInDB,
isComingFromAltSource = false,
isError = false,
)
)
}
val entityFromAltSource = getDataFromAltSource.invoke()
if (entityFromAltSource.isSuccess.not() || entityFromAltSource.value.isNullOrEmpty()) {
if (currentValueInDB?.value.isNullOrEmpty().orTrue()) {
emit(NaviCacheEntityInfo(isError = true, data = null))
return if (currentValueInDB == null) {
NaviCacheEntityInfo(isError = true)
} else {
null
}
} else {
val naviCacheEntity =
@@ -254,14 +245,9 @@ class NaviCacheRepositoryImpl @Inject constructor(private val naviCacheDao: Navi
if (isDataSame.not()) {
save(naviCacheEntity = naviCacheEntity)
emit(
NaviCacheEntityInfo(
data = naviCacheEntity,
isError = false,
isComingFromAltSource = true,
)
)
return NaviCacheEntityInfo(data = naviCacheEntity, isComingFromAltSource = true)
}
return null
}
}

View File

@@ -87,10 +87,14 @@ import java.util.Locale
import java.util.zip.GZIPOutputStream
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import okhttp3.Request
import org.json.JSONObject
@@ -652,6 +656,26 @@ fun CoroutineScope.safeLaunch(
}
}
fun <T> CoroutineScope.safeAsync(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T,
): Deferred<T?> {
val exceptionHandler = CoroutineExceptionHandler { _, throwable -> throwable.log() }
return async(exceptionHandler + context, start) {
try {
block()
} catch (e: CancellationException) {
e.log()
null
} catch (e: Exception) {
e.log()
null
}
}
}
fun Activity.startEnterAnimation() {
this.overridePendingTransition(R.anim.parallax_slide_in_right, R.anim.parallax_slide_out_left)
}