ð android-kotlin-coroutines
Use when implementing async operations with Kotlin coroutines, Flow, StateFlow, or managing concurrency in Android apps.
Overview
Asynchronous programming patterns using Kotlin coroutines and Flow in Android.
Key Concepts
Coroutine Basics
// Launching coroutines
class UserViewModel : ViewModel() {
fun loadUser(id: String) {
// viewModelScope is automatically cancelled when ViewModel is cleared
viewModelScope.launch {
try {
val user = userRepository.getUser(id)
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
}
// For operations that return a value
fun fetchUserAsync(id: String): Deferred<User> {
return viewModelScope.async {
userRepository.getUser(id)
}
}
}
// Suspend functions
suspend fun fetchUserFromNetwork(id: String): User {
return withContext(Dispatchers.IO) {
api.getUser(id)
}
}
Dispatchers
// Main - UI operations
withContext(Dispatchers.Main) {
textView.text = "Updated"
}
// IO - Network, database, file operations
withContext(Dispatchers.IO) {
val data = api.fetchData()
database.save(data)
}
// Default - CPU-intensive work
withContext(Dispatchers.Default) {
val result = expensiveComputation(data)
}
// Custom dispatcher for limited parallelism
val limitedDispatcher = Dispatchers.IO.limitedParallelism(4)
Flow Basics
// Creating flows
fun getUsers(): Flow<List<User>> = flow {
while (true) {
val users = api.getUsers()
emit(users)
delay(30_000) // Poll every 30 seconds
}
}
// Flow from Room
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<UserEntity>>
}
// Collecting flows
viewModelScope.launch {
userRepository.getUsers()
.catch { e -> _uiState.value = UiState.Error(e) }
.collect { users ->
_uiState.value = UiState.Success(users)
}
}
StateFlow and SharedFlow
class SearchViewModel : ViewModel() {
// StateFlow - always has a current value
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
// SharedFlow - for events without initial value
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
// Derived state from flow
val searchResults: StateFlow<List<Item>> = _searchQuery
.debounce(300)
.filter { it.length >= 2 }
.flatMapLatest { query ->
searchRepository.search(query)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun updateQuery(query: String) {
_searchQuery.value = query
}
fun sendEvent(event: UiEvent) {
viewModelScope.launch {
_events.emit(event)
}
}
}
Best Practices
Structured Concurrency
// Good: Using coroutineScope for parallel operations
suspend fun loadDashboard(): Dashboard = coroutineScope {
val userDeferred = async { userRepository.getUser() }
val ordersDeferred = async { orderRepository.getOrders() }
val notificationsDeferred = async { notificationRepository.getNotifications() }
// All complete or all fail together
Dashboard(
user = userDeferred.await(),
orders = ordersDeferred.await(),
notifications = notificationsDeferred.await()
)
}
// With timeout
suspend fun loadWithTimeout(): Data {
return withTimeout(5000) {
api.fetchData()
}
}
// Or with nullable result on timeout
suspend fun loadWithTimeoutOrNull(): Data? {
return withTimeoutOrNull(5000) {
api.fetchData()
}
}
Exception Handling
// Using runCatching
suspend fun safeApiCall(): Result<User> = runCatching {
api.getUser()
}
// Handling in ViewModel
fun loadUser() {
viewModelScope.launch {
safeApiCall()
.onSuccess { user ->
_uiState.value = UiState.Success(user)
}
.onFailure { error ->
_uiState.value = UiState.Error(error.message)
}
}
}
// SupervisorJob for independent child failures
class MyViewModel : ViewModel() {
private val supervisorJob = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Main + supervisorJob)
fun loadMultiple() {
scope.launch {
// This failure won't cancel other children
userRepository.getUser()
}
scope.launch {
// This continues even if above fails
orderRepository.getOrders()
}
}
}
Flow Operators
// Transformation operators
userRepository.getUsers()
.map { users -> users.filter { it.isActive } }
.distinctUntilChanged()
.collect { activeUsers -> updateUI(activeUsers) }
// Combining flows
val combined: Flow<Pair<User, Settings>> = combine(
userRepository.getUser(),
settingsRepository.getSettings()
) { user, settings ->
Pair(user, settings)
}
// FlatMapLatest for search
searchQuery
.debounce(300)
.flatMapLatest { query ->
if (query.isEmpty()) flowOf(emptyList())
else searchRepository.search(query)
}
.collect { results -> updateResults(results) }
// Retry with exponential backoff
api.fetchData()
.retry(3) { cause ->
if (cause is IOException) {
delay(1000 * (2.0.pow(retryCount)).toLong())
true
} else false
}
Lifecycle-Aware Collection
// In Compose - collectAsStateWithLifecycle
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
UserContent(uiState)
}
// In Activity/Fragment - repeatOnLifecycle
class UserFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUI(state)
}
}
}
}
}
// Multiple flows
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.users.collect { updateUserList(it) }
}
launch {
viewModel.events.collect { handleEvent(it) }
}
}
}
Common Patterns
Repository Pattern with Flow
class UserRepository(
private val api: UserApi,
private val dao: UserDao,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
fun getUser(id: String): Flow<User> = flow {
// Emit cached data first
dao.getUser(id)?.let { emit(it.toDomain()) }
// Fetch from network
val networkUser = api.getUser(id)
dao.insertUser(networkUser.toEntity())
emit(networkUser.toDomain())
}
.flowOn(dispatcher)
.catch { e ->
// Log error, emit from cache if available
dao.getUser(id)?.let { emit(it.toDomain()) }
?: throw e
}
suspend fun refreshUsers() {
withContext(dispatcher) {
val users = api.getUsers()
dao.deleteAll()
dao.insertAll(users.map { it.toEntity() })
}
}
}
Cancellation Handling
suspend fun downloadFile(url: String): ByteArray {
return withContext(Dispatchers.IO) {
val connection = URL(url).openConnection()
connection.inputStream.use { input ->
val buffer = ByteArrayOutputStream()
val data = ByteArray(4096)
while (true) {
// Check for cancellation
ensureActive()
val count = input.read(data)
if (count == -1) break
buffer.write(data, 0, count)
}
buffer.toByteArray()
}
}
}
// Cancellable flow
fun pollData(): Flow<Data> = flow {
while (currentCoroutineContext().isActive) {
emit(api.fetchData())
delay(5000)
}
}
Debounce and Throttle
// Debounce - wait for pause in emissions
@Composable
fun SearchField(onSearch: (String) -> Unit) {
var query by remember { mutableStateOf("") }
LaunchedEffect(query) {
delay(300) // Debounce
if (query.isNotEmpty()) {
onSearch(query)
}
}
TextField(value = query, onValueChange = { query = it })
}
// In ViewModel
private val _searchQuery = MutableStateFlow("")
val searchResults = _searchQuery
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { query ->
searchRepository.search(query)
}
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
Anti-Patterns
GlobalScope Usage
Bad:
GlobalScope.launch { // Never cancelled, leaks memory
fetchData()
}
Good:
viewModelScope.launch { // Properly scoped
fetchData()
}
Blocking Calls on Main Thread
Bad:
fun loadData() {
runBlocking { // Blocks main thread!
api.fetchData()
}
}
Good:
fun loadData() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
api.fetchData()
}
}
}
Flow Collection Without Lifecycle
Bad:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launch {
viewModel.uiState.collect { // Collects even when in background
updateUI(it)
}
}
}
Good:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { updateUI(it) }
}
}
}
Creating New Flow on Each Call
Bad:
// Creates new flow each time
fun getUsers(): Flow<List<User>> = userDao.getAllUsers()
// Called multiple times, multiple database subscriptions
Good:
// Shared flow, single subscription
val users: StateFlow<List<User>> = userDao.getAllUsers()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
Related Skills
- android-jetpack-compose: UI integration with coroutines
- android-architecture: Architectural patterns using coroutines