| name | android-kotlin-coroutines |
| description | Use when implementing async operations with Kotlin coroutines, Flow, StateFlow, or managing concurrency in Android apps. |
| allowed-tools | ["Read","Write","Edit","Bash","Grep","Glob"] |
Android - Kotlin Coroutines
Asynchronous programming patterns using Kotlin coroutines and Flow in Android.
Key Concepts
Coroutine Basics
class UserViewModel : ViewModel() {
fun loadUser(id: String) {
viewModelScope.launch {
try {
val user = userRepository.getUser(id)
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
}
fun fetchUserAsync(id: String): Deferred<User> {
return viewModelScope.async {
userRepository.getUser(id)
}
}
}
suspend fun fetchUserFromNetwork(id: String): User {
return withContext(Dispatchers.IO) {
api.getUser(id)
}
}
Dispatchers
withContext(Dispatchers.Main) {
textView.text = "Updated"
}
withContext(Dispatchers.IO) {
val data = api.fetchData()
database.save(data)
}
withContext(Dispatchers.Default) {
val result = expensiveComputation(data)
}
val limitedDispatcher = Dispatchers.IO.limitedParallelism(4)
Flow Basics
fun getUsers(): Flow<List<User>> = flow {
while (true) {
val users = api.getUsers()
emit(users)
delay(30_000)
}
}
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<UserEntity>>
}
viewModelScope.launch {
userRepository.getUsers()
.catch { e -> _uiState.value = UiState.Error(e) }
.collect { users ->
_uiState.value = UiState.Success(users)
}
}
StateFlow and SharedFlow
class SearchViewModel : ViewModel() {
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
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
suspend fun loadDashboard(): Dashboard = coroutineScope {
val userDeferred = async { userRepository.getUser() }
val ordersDeferred = async { orderRepository.getOrders() }
val notificationsDeferred = async { notificationRepository.getNotifications() }
Dashboard(
user = userDeferred.await(),
orders = ordersDeferred.await(),
notifications = notificationsDeferred.await()
)
}
suspend fun loadWithTimeout(): Data {
return withTimeout(5000) {
api.fetchData()
}
}
suspend fun loadWithTimeoutOrNull(): Data? {
return withTimeoutOrNull(5000) {
api.fetchData()
}
}
Exception Handling
suspend fun safeApiCall(): Result<User> = runCatching {
api.getUser()
}
fun loadUser() {
viewModelScope.launch {
safeApiCall()
.onSuccess { user ->
_uiState.value = UiState.Success(user)
}
.onFailure { error ->
_uiState.value = UiState.Error(error.message)
}
}
}
class MyViewModel : ViewModel() {
private val supervisorJob = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Main + supervisorJob)
fun loadMultiple() {
scope.launch {
userRepository.getUser()
}
scope.launch {
orderRepository.getOrders()
}
}
}
Flow Operators
userRepository.getUsers()
.map { users -> users.filter { it.isActive } }
.distinctUntilChanged()
.collect { activeUsers -> updateUI(activeUsers) }
val combined: Flow<Pair<User, Settings>> = combine(
userRepository.getUser(),
settingsRepository.getSettings()
) { user, settings ->
Pair(user, settings)
}
searchQuery
.debounce(300)
.flatMapLatest { query ->
if (query.isEmpty()) flowOf(emptyList())
else searchRepository.search(query)
}
.collect { results -> updateResults(results) }
api.fetchData()
.retry(3) { cause ->
if (cause is IOException) {
delay(1000 * (2.0.pow(retryCount)).toLong())
true
} else false
}
Lifecycle-Aware Collection
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
UserContent(uiState)
}
class UserFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUI(state)
}
}
}
}
}
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 {
dao.getUser(id)?.let { emit(it.toDomain()) }
val networkUser = api.getUser(id)
dao.insertUser(networkUser.toEntity())
emit(networkUser.toDomain())
}
.flowOn(dispatcher)
.catch { e ->
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) {
ensureActive()
val count = input.read(data)
if (count == -1) break
buffer.write(data, 0, count)
}
buffer.toByteArray()
}
}
}
fun pollData(): Flow<Data> = flow {
while (currentCoroutineContext().isActive) {
emit(api.fetchData())
delay(5000)
}
}
Debounce and Throttle
@Composable
fun SearchField(onSearch: (String) -> Unit) {
var query by remember { mutableStateOf("") }
LaunchedEffect(query) {
delay(300)
if (query.isNotEmpty()) {
onSearch(query)
}
}
TextField(value = query, onValueChange = { query = it })
}
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 {
fetchData()
}
Good:
viewModelScope.launch {
fetchData()
}
Blocking Calls on Main Thread
Bad:
fun loadData() {
runBlocking {
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 {
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:
fun getUsers(): Flow<List<User>> = userDao.getAllUsers()
Good:
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