Tools: The Ultimate Guide to Kotlin Concurrency for Building a Super Android App ๐Ÿš€

Tools: The Ultimate Guide to Kotlin Concurrency for Building a Super Android App ๐Ÿš€

How We Use Kotlin Coroutines & Flow in Enterprise Android ## 1. Sequential Orchestration (Profile โ†’ Portfolio) ## Real Scenario ## ViewModel Implementation ## Why This Pattern Is Important ## 2. Parallel Dashboard Aggregation (combine) ## Scenario ## Why combine instead of zip? ## 3. Strict Regulatory Pairing (zip) ## Scenario ## Why zip here? ## 4. Detail Loading + Dependent Call ## Scenario ## Why trigger child call inside onEach? ## 5. UI State for Compose (Immutable) ## 6. Compose UI (Reactive + Clean) ## 7. Reactive Sensitive Value Visibility (Flow-Based) ## Scenario: ## 8. Handling Inactivity (PIN / Login Expire) ## 9. Prevent Multiple Button Clicks (Throttle) ## Throttle First ## 10. Backpressure Handling (High Frequency Events) ## Debounce + FlatMapLatest ## 11. Avoid Blocking User Experience ## 12. Jetpack Compose Concurrency Patterns ## 13. Advanced Retry with Exponential Backoff ## 13.1 Production Pattern: Conditional Exponential Backoff ## Scenario ## What Happens Here? ## 14. Token Expiry + Automatic Refresh ## Core Rule ## Token Coordinator ## 14.1 Token Refresh Orchestration Pattern (Flow Level) ## Repository-Level Flow Orchestration ## Scenario ## What Happens Here? ## 14.2 Even Cleaner: Reusable Token Wrapper ## 14.3 What Happens with Parallel Calls + Token Expiry? ## What happens? ## About Auth Orchestration ## Final Thought In this article, I'll show how we use Coroutines + Flow in production inside a digital wealth management app. This is not theoretical coroutine usage. This is orchestration for: When user opens Portfolio screen: This is how real fintech orchestration should look. combine reacts when either emits. Asset value might refresh independently of account list. In real apps: combine supports this naturally. Before showing Trade screen: Because both documents are mandatory before proceeding. No partial rendering allowed. When user selects an investment account: ViewModel State Holder No LiveData and No manual observers. Pure Flow ----> StateFlow ----> Compose. Instead of Rx sensor logic, we use Flow: Use a shared flow for session events. Double execution is not a fintech issue. It's a concurrency failure - and every serious app must prevent it. flatMapLatest cancels previous request. Or better - push heavy work to repository layer. In financial systems: Retry is not retry(3). Retry must be: Refreshing portfolio valuation from market service. This prevents backend abuse while improving reliability. In fintech/investment systems: If you don't orchestrate this properly: Token refresh must be: Why Mutex? If 5 APIs fail with 401 at the same time: Now the important part: We do NOT handle token inside every ViewModel. We handle it at repository layer. Fetching portfolio summary. This keeps ViewModel clean. For large systems, create a reusable wrapper: Why This Pattern Scales In this kind of app, Token management is not an interceptor problem only. It is a concurrency orchestration problem. App must be design with these: In fintech apps, concurrency is not optimization. Coroutines and Flow are not async tools. They are: Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to ? It will become hidden in your post, but will still be visible via the comment's permalink. as well , this person and/or CODE_BLOCK: private fun loadInvestorOverview() { getInvestorProfileUseCase .execute(Unit) .onStart { setLoading(true) } .catch { error -> setLoading(false) showInvestorError(error) } .flatMapConcat { profile -> _uiState.update { it.copy( investorName = "${profile.firstName} ${profile.lastName}" ) } getPortfolioAccountsUseCase.execute(profile.investorId) } .catch { error -> setLoading(false) handlePortfolioError(error) } .onEach { accounts -> _uiState.update { it.copy( portfolioAccounts = accounts.map { PortfolioItem( accountId = it.accountId, productType = it.productType ) } ) } } .onCompletion { setLoading(false) } .launchIn(viewModelScope) } CODE_BLOCK: private fun loadInvestorOverview() { getInvestorProfileUseCase .execute(Unit) .onStart { setLoading(true) } .catch { error -> setLoading(false) showInvestorError(error) } .flatMapConcat { profile -> _uiState.update { it.copy( investorName = "${profile.firstName} ${profile.lastName}" ) } getPortfolioAccountsUseCase.execute(profile.investorId) } .catch { error -> setLoading(false) handlePortfolioError(error) } .onEach { accounts -> _uiState.update { it.copy( portfolioAccounts = accounts.map { PortfolioItem( accountId = it.accountId, productType = it.productType ) } ) } } .onCompletion { setLoading(false) } .launchIn(viewModelScope) } CODE_BLOCK: private fun loadInvestorOverview() { getInvestorProfileUseCase .execute(Unit) .onStart { setLoading(true) } .catch { error -> setLoading(false) showInvestorError(error) } .flatMapConcat { profile -> _uiState.update { it.copy( investorName = "${profile.firstName} ${profile.lastName}" ) } getPortfolioAccountsUseCase.execute(profile.investorId) } .catch { error -> setLoading(false) handlePortfolioError(error) } .onEach { accounts -> _uiState.update { it.copy( portfolioAccounts = accounts.map { PortfolioItem( accountId = it.accountId, productType = it.productType ) } ) } } .onCompletion { setLoading(false) } .launchIn(viewModelScope) } CODE_BLOCK: fun initializeDashboard() { val accountsFlow = getInvestmentAccountsUseCase.execute(Unit) val assetValueFlow = getTotalAssetValueUseCase.execute(Unit) accountsFlow .combine(assetValueFlow) { accounts, totalValue -> accounts to totalValue } .onStart { setLoading(true) } .onCompletion { setLoading(false) } .catch { showDashboardError(it) } .onEach { (accounts, totalValue) -> _uiState.update { it.copy( investmentAccounts = accounts, totalAssets = totalValue ) } } .launchIn(viewModelScope) } CODE_BLOCK: fun initializeDashboard() { val accountsFlow = getInvestmentAccountsUseCase.execute(Unit) val assetValueFlow = getTotalAssetValueUseCase.execute(Unit) accountsFlow .combine(assetValueFlow) { accounts, totalValue -> accounts to totalValue } .onStart { setLoading(true) } .onCompletion { setLoading(false) } .catch { showDashboardError(it) } .onEach { (accounts, totalValue) -> _uiState.update { it.copy( investmentAccounts = accounts, totalAssets = totalValue ) } } .launchIn(viewModelScope) } CODE_BLOCK: fun initializeDashboard() { val accountsFlow = getInvestmentAccountsUseCase.execute(Unit) val assetValueFlow = getTotalAssetValueUseCase.execute(Unit) accountsFlow .combine(assetValueFlow) { accounts, totalValue -> accounts to totalValue } .onStart { setLoading(true) } .onCompletion { setLoading(false) } .catch { showDashboardError(it) } .onEach { (accounts, totalValue) -> _uiState.update { it.copy( investmentAccounts = accounts, totalAssets = totalValue ) } } .launchIn(viewModelScope) } CODE_BLOCK: private fun loadRiskDocuments() { getDisclosureTemplateUseCase.execute(DISCLOSURE_FULL) .zip( getDisclosureTemplateUseCase.execute(DISCLOSURE_SUMMARY) ) { full, summary -> full to summary } .onStart { setLoading(true) } .onCompletion { setLoading(false) } .catch { showDocumentError(it) } .onEach { (fullDoc, summaryDoc) -> _uiState.update { it.copy( fullDisclosure = fullDoc, summaryDisclosure = summaryDoc ) } } .launchIn(viewModelScope) } CODE_BLOCK: private fun loadRiskDocuments() { getDisclosureTemplateUseCase.execute(DISCLOSURE_FULL) .zip( getDisclosureTemplateUseCase.execute(DISCLOSURE_SUMMARY) ) { full, summary -> full to summary } .onStart { setLoading(true) } .onCompletion { setLoading(false) } .catch { showDocumentError(it) } .onEach { (fullDoc, summaryDoc) -> _uiState.update { it.copy( fullDisclosure = fullDoc, summaryDisclosure = summaryDoc ) } } .launchIn(viewModelScope) } CODE_BLOCK: private fun loadRiskDocuments() { getDisclosureTemplateUseCase.execute(DISCLOSURE_FULL) .zip( getDisclosureTemplateUseCase.execute(DISCLOSURE_SUMMARY) ) { full, summary -> full to summary } .onStart { setLoading(true) } .onCompletion { setLoading(false) } .catch { showDocumentError(it) } .onEach { (fullDoc, summaryDoc) -> _uiState.update { it.copy( fullDisclosure = fullDoc, summaryDisclosure = summaryDoc ) } } .launchIn(viewModelScope) } CODE_BLOCK: fun loadAccountDetail(accountId: String, refresh: Boolean = false) { getAccountDetailUseCase .execute(accountId) .onStart { setLoading(true) } .onCompletion { _uiState.update { it.copy(isRefreshing = false) } } .catch { handleDetailError(it) } .onEach { detail -> _uiState.update { it.copy( selectedAccount = detail, hasDividendOption = detail.dividendOptions.isNotEmpty() ) } loadTransactionHistory(accountId, refresh) } .launchIn(viewModelScope) } CODE_BLOCK: fun loadAccountDetail(accountId: String, refresh: Boolean = false) { getAccountDetailUseCase .execute(accountId) .onStart { setLoading(true) } .onCompletion { _uiState.update { it.copy(isRefreshing = false) } } .catch { handleDetailError(it) } .onEach { detail -> _uiState.update { it.copy( selectedAccount = detail, hasDividendOption = detail.dividendOptions.isNotEmpty() ) } loadTransactionHistory(accountId, refresh) } .launchIn(viewModelScope) } CODE_BLOCK: fun loadAccountDetail(accountId: String, refresh: Boolean = false) { getAccountDetailUseCase .execute(accountId) .onStart { setLoading(true) } .onCompletion { _uiState.update { it.copy(isRefreshing = false) } } .catch { handleDetailError(it) } .onEach { detail -> _uiState.update { it.copy( selectedAccount = detail, hasDividendOption = detail.dividendOptions.isNotEmpty() ) } loadTransactionHistory(accountId, refresh) } .launchIn(viewModelScope) } COMMAND_BLOCK: data class PortfolioUiState( val isLoading: Boolean = false, val investorName: String = "", val investmentAccounts: List<PortfolioItem> = emptyList(), val totalAssets: AssetValue? = null, val selectedAccount: AccountDetail? = null, val hasDividendOption: Boolean = false, val fullDisclosure: Document? = null, val summaryDisclosure: Document? = null ) COMMAND_BLOCK: data class PortfolioUiState( val isLoading: Boolean = false, val investorName: String = "", val investmentAccounts: List<PortfolioItem> = emptyList(), val totalAssets: AssetValue? = null, val selectedAccount: AccountDetail? = null, val hasDividendOption: Boolean = false, val fullDisclosure: Document? = null, val summaryDisclosure: Document? = null ) COMMAND_BLOCK: data class PortfolioUiState( val isLoading: Boolean = false, val investorName: String = "", val investmentAccounts: List<PortfolioItem> = emptyList(), val totalAssets: AssetValue? = null, val selectedAccount: AccountDetail? = null, val hasDividendOption: Boolean = false, val fullDisclosure: Document? = null, val summaryDisclosure: Document? = null ) COMMAND_BLOCK: private val _uiState = MutableStateFlow(PortfolioUiState()) val uiState: StateFlow<PortfolioUiState> = _uiState COMMAND_BLOCK: private val _uiState = MutableStateFlow(PortfolioUiState()) val uiState: StateFlow<PortfolioUiState> = _uiState COMMAND_BLOCK: private val _uiState = MutableStateFlow(PortfolioUiState()) val uiState: StateFlow<PortfolioUiState> = _uiState CODE_BLOCK: @Composable fun PortfolioScreen(viewModel: PortfolioViewModel) { val state by viewModel.uiState.collectAsStateWithLifecycle() if (state.isLoading) { CircularProgressIndicator() } Text(text = "Welcome ${state.investorName}") state.totalAssets?.let { Text("Total Assets: ${it.formatted}") } LazyColumn { items(state.investmentAccounts) { account -> Text(account.accountId) } } } CODE_BLOCK: @Composable fun PortfolioScreen(viewModel: PortfolioViewModel) { val state by viewModel.uiState.collectAsStateWithLifecycle() if (state.isLoading) { CircularProgressIndicator() } Text(text = "Welcome ${state.investorName}") state.totalAssets?.let { Text("Total Assets: ${it.formatted}") } LazyColumn { items(state.investmentAccounts) { account -> Text(account.accountId) } } } CODE_BLOCK: @Composable fun PortfolioScreen(viewModel: PortfolioViewModel) { val state by viewModel.uiState.collectAsStateWithLifecycle() if (state.isLoading) { CircularProgressIndicator() } Text(text = "Welcome ${state.investorName}") state.totalAssets?.let { Text("Total Assets: ${it.formatted}") } LazyColumn { items(state.investmentAccounts) { account -> Text(account.accountId) } } } CODE_BLOCK: val sensitiveVisibilityFlow = combine(deviceOrientationFlow, userInteractionFlow) { isFaceDown, isTouching -> !isFaceDown && isTouching }.distinctUntilChanged() In ViewModel: sensitiveVisibilityFlow .onEach { visible -> _uiState.update { it.copy(isPortfolioVisible = visible) } } .launchIn(viewModelScope) Compose: AnimatedVisibility(visible = state.isPortfolioVisible) { PortfolioValueSection() } CODE_BLOCK: val sensitiveVisibilityFlow = combine(deviceOrientationFlow, userInteractionFlow) { isFaceDown, isTouching -> !isFaceDown && isTouching }.distinctUntilChanged() In ViewModel: sensitiveVisibilityFlow .onEach { visible -> _uiState.update { it.copy(isPortfolioVisible = visible) } } .launchIn(viewModelScope) Compose: AnimatedVisibility(visible = state.isPortfolioVisible) { PortfolioValueSection() } CODE_BLOCK: val sensitiveVisibilityFlow = combine(deviceOrientationFlow, userInteractionFlow) { isFaceDown, isTouching -> !isFaceDown && isTouching }.distinctUntilChanged() In ViewModel: sensitiveVisibilityFlow .onEach { visible -> _uiState.update { it.copy(isPortfolioVisible = visible) } } .launchIn(viewModelScope) Compose: AnimatedVisibility(visible = state.isPortfolioVisible) { PortfolioValueSection() } CODE_BLOCK: object SessionManager { private val _sessionExpired = MutableSharedFlow<Unit>() val sessionExpired = _sessionExpired.asSharedFlow() suspend fun expire() { _sessionExpired.emit(Unit) } } CODE_BLOCK: object SessionManager { private val _sessionExpired = MutableSharedFlow<Unit>() val sessionExpired = _sessionExpired.asSharedFlow() suspend fun expire() { _sessionExpired.emit(Unit) } } CODE_BLOCK: object SessionManager { private val _sessionExpired = MutableSharedFlow<Unit>() val sessionExpired = _sessionExpired.asSharedFlow() suspend fun expire() { _sessionExpired.emit(Unit) } } CODE_BLOCK: viewModelScope.launch { SessionManager.sessionExpired.collect { navigator.navigateToLogin() } } CODE_BLOCK: viewModelScope.launch { SessionManager.sessionExpired.collect { navigator.navigateToLogin() } } CODE_BLOCK: viewModelScope.launch { SessionManager.sessionExpired.collect { navigator.navigateToLogin() } } COMMAND_BLOCK: fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow { var lastTime = 0L collect { value -> val current = System.currentTimeMillis() if (current - lastTime >= windowDuration) { lastTime = current emit(value) } } } COMMAND_BLOCK: fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow { var lastTime = 0L collect { value -> val current = System.currentTimeMillis() if (current - lastTime >= windowDuration) { lastTime = current emit(value) } } } COMMAND_BLOCK: fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow { var lastTime = 0L collect { value -> val current = System.currentTimeMillis() if (current - lastTime >= windowDuration) { lastTime = current emit(value) } } } CODE_BLOCK: buttonClicks .throttleFirst(1000) .onEach { viewModel.sendMoney() } .launchIn(scope) CODE_BLOCK: buttonClicks .throttleFirst(1000) .onEach { viewModel.sendMoney() } .launchIn(scope) CODE_BLOCK: buttonClicks .throttleFirst(1000) .onEach { viewModel.sendMoney() } .launchIn(scope) CODE_BLOCK: searchQuery .debounce(300) .distinctUntilChanged() .flatMapLatest { query -> repository.searchStocks(query) } CODE_BLOCK: searchQuery .debounce(300) .distinctUntilChanged() .flatMapLatest { query -> repository.searchStocks(query) } CODE_BLOCK: searchQuery .debounce(300) .distinctUntilChanged() .flatMapLatest { query -> repository.searchStocks(query) } CODE_BLOCK: runBlocking { } Thread.sleep() CODE_BLOCK: runBlocking { } Thread.sleep() CODE_BLOCK: runBlocking { } Thread.sleep() CODE_BLOCK: withContext(Dispatchers.IO) { heavyWork() } CODE_BLOCK: withContext(Dispatchers.IO) { heavyWork() } CODE_BLOCK: withContext(Dispatchers.IO) { heavyWork() } CODE_BLOCK: val balance by viewModel.balanceFlow.collectAsStateWithLifecycle() CODE_BLOCK: val balance by viewModel.balanceFlow.collectAsStateWithLifecycle() CODE_BLOCK: val balance by viewModel.balanceFlow.collectAsStateWithLifecycle() CODE_BLOCK: LaunchedEffect(Unit) { viewModel.loadDashboard() } CODE_BLOCK: LaunchedEffect(Unit) { viewModel.loadDashboard() } CODE_BLOCK: LaunchedEffect(Unit) { viewModel.loadDashboard() } CODE_BLOCK: LaunchedEffect(viewModel.errorFlow) { viewModel.errorFlow.collect { snackbarHostState.showSnackbar(it.message) } } CODE_BLOCK: LaunchedEffect(viewModel.errorFlow) { viewModel.errorFlow.collect { snackbarHostState.showSnackbar(it.message) } } CODE_BLOCK: LaunchedEffect(viewModel.errorFlow) { viewModel.errorFlow.collect { snackbarHostState.showSnackbar(it.message) } } CODE_BLOCK: private fun refreshMarketValuation() { getMarketValuationUseCase .execute(Unit) .retryWhen { cause, attempt -> val isNetworkError = cause is IOException val isServerError = cause is HttpException && cause.code() >= 500 if ((isNetworkError || isServerError) && attempt < 3) { val backoffDelay = 1_000L * (2.0.pow(attempt.toDouble())).toLong() delay(backoffDelay) true } else { false } } .onStart { setLoading(true) } .onCompletion { setLoading(false) } .catch { handleMarketError(it) } .onEach { valuation -> _uiState.update { it.copy(marketValuation = valuation) } } .launchIn(viewModelScope) } CODE_BLOCK: private fun refreshMarketValuation() { getMarketValuationUseCase .execute(Unit) .retryWhen { cause, attempt -> val isNetworkError = cause is IOException val isServerError = cause is HttpException && cause.code() >= 500 if ((isNetworkError || isServerError) && attempt < 3) { val backoffDelay = 1_000L * (2.0.pow(attempt.toDouble())).toLong() delay(backoffDelay) true } else { false } } .onStart { setLoading(true) } .onCompletion { setLoading(false) } .catch { handleMarketError(it) } .onEach { valuation -> _uiState.update { it.copy(marketValuation = valuation) } } .launchIn(viewModelScope) } CODE_BLOCK: private fun refreshMarketValuation() { getMarketValuationUseCase .execute(Unit) .retryWhen { cause, attempt -> val isNetworkError = cause is IOException val isServerError = cause is HttpException && cause.code() >= 500 if ((isNetworkError || isServerError) && attempt < 3) { val backoffDelay = 1_000L * (2.0.pow(attempt.toDouble())).toLong() delay(backoffDelay) true } else { false } } .onStart { setLoading(true) } .onCompletion { setLoading(false) } .catch { handleMarketError(it) } .onEach { valuation -> _uiState.update { it.copy(marketValuation = valuation) } } .launchIn(viewModelScope) } CODE_BLOCK: class AuthTokenCoordinator( private val refreshTokenUseCase: RefreshTokenUseCase, private val tokenStorage: TokenStorage ) { private val mutex = Mutex() suspend fun refreshIfNeeded(): String { return mutex.withLock { val newToken = refreshTokenUseCase.execute(Unit).first() tokenStorage.save(newToken) newToken.accessToken } } } CODE_BLOCK: class AuthTokenCoordinator( private val refreshTokenUseCase: RefreshTokenUseCase, private val tokenStorage: TokenStorage ) { private val mutex = Mutex() suspend fun refreshIfNeeded(): String { return mutex.withLock { val newToken = refreshTokenUseCase.execute(Unit).first() tokenStorage.save(newToken) newToken.accessToken } } } CODE_BLOCK: class AuthTokenCoordinator( private val refreshTokenUseCase: RefreshTokenUseCase, private val tokenStorage: TokenStorage ) { private val mutex = Mutex() suspend fun refreshIfNeeded(): String { return mutex.withLock { val newToken = refreshTokenUseCase.execute(Unit).first() tokenStorage.save(newToken) newToken.accessToken } } } COMMAND_BLOCK: fun getPortfolioSummary(): Flow<PortfolioSummary> { return flow { emit(api.getPortfolioSummary()) } .catch { throwable -> if (throwable is HttpException && throwable.code() == 401) { val newToken = authTokenCoordinator.refreshIfNeeded() emit(api.getPortfolioSummaryWithToken(newToken)) } else { throw throwable } } } COMMAND_BLOCK: fun getPortfolioSummary(): Flow<PortfolioSummary> { return flow { emit(api.getPortfolioSummary()) } .catch { throwable -> if (throwable is HttpException && throwable.code() == 401) { val newToken = authTokenCoordinator.refreshIfNeeded() emit(api.getPortfolioSummaryWithToken(newToken)) } else { throw throwable } } } COMMAND_BLOCK: fun getPortfolioSummary(): Flow<PortfolioSummary> { return flow { emit(api.getPortfolioSummary()) } .catch { throwable -> if (throwable is HttpException && throwable.code() == 401) { val newToken = authTokenCoordinator.refreshIfNeeded() emit(api.getPortfolioSummaryWithToken(newToken)) } else { throw throwable } } } COMMAND_BLOCK: fun <T> Flow<T>.withTokenRefresh( authTokenCoordinator: AuthTokenCoordinator, retryBlock: suspend (String) -> T ): Flow<T> { return catch { throwable -> if (throwable is HttpException && throwable.code() == 401) { val newToken = authTokenCoordinator.refreshIfNeeded() emit(retryBlock(newToken)) } else { throw throwable } } } COMMAND_BLOCK: fun <T> Flow<T>.withTokenRefresh( authTokenCoordinator: AuthTokenCoordinator, retryBlock: suspend (String) -> T ): Flow<T> { return catch { throwable -> if (throwable is HttpException && throwable.code() == 401) { val newToken = authTokenCoordinator.refreshIfNeeded() emit(retryBlock(newToken)) } else { throw throwable } } } COMMAND_BLOCK: fun <T> Flow<T>.withTokenRefresh( authTokenCoordinator: AuthTokenCoordinator, retryBlock: suspend (String) -> T ): Flow<T> { return catch { throwable -> if (throwable is HttpException && throwable.code() == 401) { val newToken = authTokenCoordinator.refreshIfNeeded() emit(retryBlock(newToken)) } else { throw throwable } } } COMMAND_BLOCK: fun getPortfolioSummary(): Flow<PortfolioSummary> { return flow { emit(api.getPortfolioSummary()) }.withTokenRefresh(authTokenCoordinator) { newToken -> api.getPortfolioSummaryWithToken(newToken) } } COMMAND_BLOCK: fun getPortfolioSummary(): Flow<PortfolioSummary> { return flow { emit(api.getPortfolioSummary()) }.withTokenRefresh(authTokenCoordinator) { newToken -> api.getPortfolioSummaryWithToken(newToken) } } COMMAND_BLOCK: fun getPortfolioSummary(): Flow<PortfolioSummary> { return flow { emit(api.getPortfolioSummary()) }.withTokenRefresh(authTokenCoordinator) { newToken -> api.getPortfolioSummaryWithToken(newToken) } } CODE_BLOCK: initializeDashboard() โ†’ getInvestmentAccounts() โ†’ getTotalAssetValue() CODE_BLOCK: initializeDashboard() โ†’ getInvestmentAccounts() โ†’ getTotalAssetValue() CODE_BLOCK: initializeDashboard() โ†’ getInvestmentAccounts() โ†’ getTotalAssetValue() - Portfolio summary, Investment accounts - Risk profile validation, Regulatory document loading - Sensitive balance visibility toggle - Parallel dashboard aggregation - Child dependent API triggers - Compose-driven UI state - Backpressure handling - Error handling patterns - Token refresh flows - Retry policies - App preloading optimization - Clean Architecture / MVVM / MVI - Repository pattern - UseCase returns Flow - ViewModel orchestrates flows - Immutable UI state via StateFlow - Compose collects state - Retrofit + Room - No GlobalScope - No blocking calls - Load investor profile - Update greeting header - Fetch portfolio accounts - Map into UI state - Handle stage-specific errors - First error handles profile domain - Second error handles portfolio domain - No nested coroutine blocks - Pipeline remains flat and readable - Cancellation remains structured - Fetch investment accounts - Fetch total asset value Show screen only when both available - Accounts rarely change - Market value changes frequently - Load risk disclosure template - Load risk summary view template - Load account details - Update state - Trigger transaction history call - Only executed after success - No nested coroutineScope - Keeps orchestration centralized in ViewModel - Hide portfolio value when device is face down - Show when user touches screen - Search field - Real-time stock ticker - Network instability is common - Backend throttling happens - Temporary 5xx failures occur - You must retry safely - but not aggressively - Conditional - Intelligent - Exponential - Cancellable - Token-aware - Retries only network + 5xx - Does NOT retry business errors (4xx) - Exponential backoff: 1s โ†’ 2s โ†’ 4s - Automatically cancelled if ViewModel cleared - No blocking threads - Access tokens expire frequently - Multiple API calls may fail simultaneously - Only ONE refresh call must execute - Other calls must wait - You trigger multiple refresh requests - Backend invalidates sessions - Users get forced logout - Centralized - Mutex-protected - Reusable across flows - Without Mutex โ†’ 5 refresh calls - With Mutex โ†’ 1 refresh call - All suspended callers wait safely. - How do we retry original API after token refresh? - API call fails with 401 - Refresh token (mutex protected) - Retry original call - Emit result - Upstream ViewModel never knows refresh happened - Centralized - No duplication - ViewModel unaware of auth complexity - Works with combine / zip / flatMap - First failure triggers refresh - Second waits on mutex - Both resume using new token - combine still works - UI sees only final result - Mutex protection - Repository-level retry - ViewModel isolation - Cancellation safety - Backoff compatibility - UI remains clean - Flows remain declarative - Orchestration remains centralized - System remains resilient - Orchestration engine - Error propagation model - State machine builder - Backpressure handler - Lifecycle-aware execution framework