android-kotlin-coroutines

Android - Kotlin Coroutines

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "android-kotlin-coroutines" with this command: npx skills add thebushidocollective/han/thebushidocollective-han-android-kotlin-coroutines

Android - Kotlin Coroutines

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&#x3C;UiEvent>()
val events: SharedFlow&#x3C;UiEvent> = _events.asSharedFlow()

// Derived state from flow
val searchResults: StateFlow&#x3C;List&#x3C;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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

android-jetpack-compose

No summary provided by upstream source.

Repository SourceNeeds Review
General

fastapi-async-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

storybook-story-writing

No summary provided by upstream source.

Repository SourceNeeds Review
General

atomic-design-fundamentals

No summary provided by upstream source.

Repository SourceNeeds Review