MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
After using Coroutines across 22+ Android apps, certain patterns appear in every codebase. Here are the ones that matter — with the mistakes they prevent.
On this page
Coroutines have a learning curve that flattens out once you understand structured concurrency. After using them across every Android app I've built, the same patterns show up every time. Here they are.
Scope determines lifetime. Get this wrong and you get either leaked coroutines or cancelled work.
// ViewModel — tied to ViewModel lifecycle
viewModelScope.launch { }
// Fragment/Activity — tied to view lifecycle
viewLifecycleOwner.lifecycleScope.launch { }
// Repository/service — injected via Hilt
class MyRepository @Inject constructor(
@ApplicationScope private val scope: CoroutineScope
)Never use
GlobalScopeThe
Result<T>sealed class DataResult<out T> {
data class Success<T>(val data: T) : DataResult<T>()
data class Error(val exception: Exception) : DataResult<Nothing>()
object Loading : DataResult<Nothing>()
}
// In repository
suspend fun getUser(id: String): DataResult<User> {
return try {
DataResult.Success(api.getUser(id))
} catch (e: IOException) {
DataResult.Error(e)
}
}
// In ViewModel
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
when (val result = repository.getUser(userId)) {
is DataResult.Success -> _uiState.update { it.copy(user = result.data, isLoading = false) }
is DataResult.Error -> _uiState.update { it.copy(error = result.exception.message, isLoading = false) }
DataResult.Loading -> Unit
}
}Don't launch new coroutines to switch dispatchers — use
withContext// Wrong — creates unnecessary coroutine
viewModelScope.launch {
launch(Dispatchers.IO) {
val data = repository.fetch()
withContext(Dispatchers.Main) {
updateUI(data)
}
}
}
// Right — stays in one coroutine
viewModelScope.launch {
val data = withContext(Dispatchers.IO) { repository.fetch() }
updateUI(data) // already on Main via viewModelScope
}viewModelScopeDispatchers.Main.immediateIOWhen UI state depends on multiple sources:
val uiState: StateFlow<HomeUiState> = combine(
userRepository.currentUser,
notificationsRepository.unreadCount,
settingsRepository.theme
) { user, count, theme ->
HomeUiState(user = user, unreadCount = count, theme = theme)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = HomeUiState()
)stateInWhileSubscribed(5000)When a coroutine is cancelled,
CancellationException// Wrong — swallows cancellation
try {
longRunningOperation()
} catch (e: Exception) {
handleError(e) // this catches CancellationException too
}
// Right
try {
longRunningOperation()
} catch (e: CancellationException) {
throw e // re-throw, let structured concurrency handle it
} catch (e: Exception) {
handleError(e)
}For cleanup on cancellation, use
try/finallytry {
performWork()
} finally {
cleanup() // always runs, even on cancellation
}coroutineScopesupervisorScopesupervisorScope {
val profileDeferred = async { repository.getProfile() }
val feedDeferred = async { repository.getFeed() }
val profile = try { profileDeferred.await() } catch (e: Exception) { null }
val feed = try { feedDeferred.await() } catch (e: Exception) { emptyList() }
// Show what we have, even if one request failed
updateUI(profile, feed)
}Use
supervisorScopecoroutineScopeUse
TestCoroutineDispatcherStandardTestDispatcherrunTest@Test
fun `login success updates ui state`() = runTest {
val viewModel = LoginViewModel(fakeRepository)
viewModel.onLoginClicked("user@example.com", "password")
advanceUntilIdle()
assertEquals(LoginUiState.Success, viewModel.uiState.value)
}advanceUntilIdle()Thread.sleepThese patterns cover 90% of what you need. The rest is context-specific, but gets significantly easier once these are second nature.
Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read