Tools
Tools: The Ultimate Guide to Kotlin Concurrency for Building a Super Android App 🚀
2026-02-13
0 views
admin
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 hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse 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)
} Enter fullscreen mode Exit fullscreen mode 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)
} Enter fullscreen mode Exit fullscreen mode 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)
} Enter fullscreen mode Exit fullscreen mode 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)
} Enter fullscreen mode Exit fullscreen mode 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
) Enter fullscreen mode Exit fullscreen mode 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 Enter fullscreen mode Exit fullscreen mode 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) } }
} Enter fullscreen mode Exit fullscreen mode 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()
} Enter fullscreen mode Exit fullscreen mode 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) }
} Enter fullscreen mode Exit fullscreen mode 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() }
} Enter fullscreen mode Exit fullscreen mode 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) } }
} Enter fullscreen mode Exit fullscreen mode 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) Enter fullscreen mode Exit fullscreen mode 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) } Enter fullscreen mode Exit fullscreen mode 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() Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
runBlocking { }
Thread.sleep() CODE_BLOCK:
runBlocking { }
Thread.sleep() CODE_BLOCK:
withContext(Dispatchers.IO) { heavyWork()
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
withContext(Dispatchers.IO) { heavyWork()
} CODE_BLOCK:
withContext(Dispatchers.IO) { heavyWork()
} CODE_BLOCK:
val balance by viewModel.balanceFlow.collectAsStateWithLifecycle() Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
val balance by viewModel.balanceFlow.collectAsStateWithLifecycle() CODE_BLOCK:
val balance by viewModel.balanceFlow.collectAsStateWithLifecycle() CODE_BLOCK:
LaunchedEffect(Unit) { viewModel.loadDashboard()
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
LaunchedEffect(Unit) { viewModel.loadDashboard()
} CODE_BLOCK:
LaunchedEffect(Unit) { viewModel.loadDashboard()
} CODE_BLOCK:
LaunchedEffect(viewModel.errorFlow) { viewModel.errorFlow.collect { snackbarHostState.showSnackbar(it.message) }
} Enter fullscreen mode Exit fullscreen mode 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)
} Enter fullscreen mode Exit fullscreen mode 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 } }
} Enter fullscreen mode Exit fullscreen mode 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 } }
} Enter fullscreen mode Exit fullscreen mode 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 } }
} Enter fullscreen mode Exit fullscreen mode 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) }
} Enter fullscreen mode Exit fullscreen mode 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() Enter fullscreen mode Exit fullscreen mode 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
how-totutorialguidedev.toaiservernetworkgit