Kotlin Coroutines voor Android: Complete Gids 2026
Uitgebreide gids over Kotlin coroutines voor Android-ontwikkeling: suspend-functies, scopes, dispatchers, Flow en geavanceerde patronen.

Kotlin coroutines hebben de asynchrone programmering op Android ingrijpend veranderd. De tijd van geneste callbacks en de verouderde AsyncTask is voorbij: met coroutines schrijft de ontwikkelaar asynchrone code die eruitziet als synchrone code, terwijl de prestaties en onderhoudbaarheid gewaarborgd blijven.
Coroutines zijn lichtgewicht (duizenden kunnen op een enkele thread draaien), ondersteunen native annulering en integreren naadloos met Jetpack en het moderne Android-ecosysteem.
De Fundamenten Begrijpen
Voordat de code wordt onderzocht, is het belangrijk te begrijpen wat coroutines zo krachtig maakt.
Wat is een Coroutine?
Een coroutine is een instantie van een onderbreekbare berekening. In tegenstelling tot threads blokkeren coroutines niet: ze pauzeren hun uitvoering en maken de thread vrij voor andere taken.
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L) // Suspend without blocking
println("World!")
}
println("Hello,")
}
// Output: Hello, World!Suspend-functies: Het Hart van Coroutines
Het sleutelwoord suspend geeft aan dat een functie de uitvoering van een coroutine kan pauzeren zonder de thread te blokkeren.
suspend fun fetchUserData(userId: String): User {
return withContext(Dispatchers.IO) {
// Network call - runs on an IO thread
apiService.getUser(userId)
}
}
suspend fun fetchUserWithPosts(userId: String): UserWithPosts {
// Sequential execution
val user = fetchUserData(userId)
val posts = fetchUserPosts(userId)
return UserWithPosts(user, posts)
}Gouden regel: een suspend-functie kan alleen worden aangeroepen vanuit een andere suspend-functie of vanuit een coroutine.
Coroutine Scopes
De scope bepaalt de levenscyclus van coroutines. Dit is cruciaal om geheugenlekken te voorkomen.
viewModelScope: De Scope voor ViewModels
class UserViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
_uiState.value = UserUiState.Loading
try {
val user = userRepository.getUser(userId)
_uiState.value = UserUiState.Success(user)
} catch (e: Exception) {
_uiState.value = UserUiState.Error(e.message)
}
}
}
}
sealed class UserUiState {
object Loading : UserUiState()
data class Success(val user: User) : UserUiState()
data class Error(val message: String?) : UserUiState()
}lifecycleScope: Voor Activities en Fragments
class UserFragment : Fragment() {
private val viewModel: UserViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
when (state) {
is UserUiState.Loading -> showLoading()
is UserUiState.Success -> showUser(state.user)
is UserUiState.Error -> showError(state.message)
}
}
}
}
}
}Gebruik nooit GlobalScope in een Android-applicatie. Coroutines die met GlobalScope worden gestart, zijn niet gekoppeld aan een levenscyclus en kunnen geheugenlekken veroorzaken.
Dispatchers: Uitvoering Controleren
Dispatchers bepalen op welke thread een coroutine wordt uitgevoerd.
De 4 Belangrijkste Dispatchers
// Main: main thread (UI)
viewModelScope.launch(Dispatchers.Main) {
textView.text = "UI update"
}
// IO: I/O operations (network, database)
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetchFromNetwork()
}
// Default: CPU-intensive computations
viewModelScope.launch(Dispatchers.Default) {
val result = heavyComputation(data)
}
// Unconfined: inherits caller context (rare use)withContext: Van Dispatcher Wisselen
class ImageProcessor {
suspend fun processImage(bitmap: Bitmap): Bitmap {
return withContext(Dispatchers.Default) {
// CPU-intensive processing on Default
applyFilters(bitmap)
}
}
suspend fun saveToGallery(bitmap: Bitmap) {
withContext(Dispatchers.IO) {
// Disk write on IO
saveToFile(bitmap)
}
}
}
// Usage in ViewModel
viewModelScope.launch {
val processed = imageProcessor.processImage(originalBitmap)
imageProcessor.saveToGallery(processed)
// Automatic return to Main for UI update
_uiState.value = UiState.Success(processed)
}Parallelle Uitvoering met async/await
Om taken parallel uit te voeren en de resultaten te combineren, wordt async gebruikt.
suspend fun loadDashboard(): Dashboard {
return coroutineScope {
// Parallel launch
val userDeferred = async { userRepository.getUser() }
val statsDeferred = async { statsRepository.getStats() }
val notificationsDeferred = async { notificationRepository.getNotifications() }
// Await results
Dashboard(
user = userDeferred.await(),
stats = statsDeferred.await(),
notifications = notificationsDeferred.await()
)
}
}Met async worden alle 3 de aanroepen parallel uitgevoerd. Als elke aanroep 1 seconde duurt, bedraagt de totale tijd ongeveer 1 seconde in plaats van 3 seconden sequentieel.
Foutafhandeling
Klassieke try/catch
viewModelScope.launch {
try {
val user = userRepository.getUser(userId)
_uiState.value = UiState.Success(user)
} catch (e: HttpException) {
_uiState.value = UiState.Error("Server error: ${e.code()}")
} catch (e: IOException) {
_uiState.value = UiState.Error("Network error")
} catch (e: Exception) {
_uiState.value = UiState.Error("Unexpected error")
}
}CoroutineExceptionHandler
class UserViewModel : ViewModel() {
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
_uiState.value = UiState.Error(throwable.message)
Timber.e(throwable, "Error in coroutine")
}
fun loadUser(userId: String) {
viewModelScope.launch(exceptionHandler) {
val user = userRepository.getUser(userId)
_uiState.value = UiState.Success(user)
}
}
}Result Wrapper Patroon
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Throwable) : Result<Nothing>()
}
suspend fun <T> safeApiCall(apiCall: suspend () -> T): Result<T> {
return try {
Result.Success(apiCall())
} catch (e: Exception) {
Result.Error(e)
}
}
// Usage
class UserRepository(private val api: UserApi) {
suspend fun getUser(id: String): Result<User> = safeApiCall {
api.getUser(id)
}
}
// In ViewModel
viewModelScope.launch {
when (val result = userRepository.getUser(userId)) {
is Result.Success -> _uiState.value = UiState.Success(result.data)
is Result.Error -> _uiState.value = UiState.Error(result.exception.message)
}
}Klaar om je Android gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Annulering: Grondig Opruimen
Coroutines ondersteunen cooperatieve annulering. Dit is essentieel om verspilling van resources te voorkomen.
Automatische Annulering met Scopes
class SearchViewModel : ViewModel() {
private var searchJob: Job? = null
fun search(query: String) {
// Cancel previous search
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300) // Debounce
val results = searchRepository.search(query)
_searchResults.value = results
}
}
}Controleren op Annulering
suspend fun processLargeList(items: List<Item>) {
items.forEach { item ->
// Check if coroutine is cancelled
ensureActive()
processItem(item)
}
}
suspend fun downloadFiles(urls: List<String>) = coroutineScope {
urls.map { url ->
async {
try {
downloadFile(url)
} catch (e: CancellationException) {
cleanupPartialDownload(url)
throw e // Re-throw to propagate cancellation
}
}
}.awaitAll()
}Vang CancellationException nooit op: wanneer Exception wordt gevangen, moet CancellationException opnieuw worden gegooid zodat de annulering correct wordt doorgegeven.
Flow: Reactieve Programmering
Flow is het coroutines-equivalent van RxJava Observable, maar eenvoudiger en native geintegreerd.
Een Flow Aanmaken en Verzamelen
fun getUsers(): Flow<List<User>> = flow {
while (true) {
val users = userApi.getUsers()
emit(users)
delay(5000) // Poll every 5 seconds
}
}
// Flow from Room
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
}
// Collecting in ViewModel
viewModelScope.launch {
userDao.getAllUsers()
.catch { e -> _uiState.value = UiState.Error(e.message) }
.collect { users ->
_uiState.value = UiState.Success(users)
}
}StateFlow vs SharedFlow
class EventViewModel : ViewModel() {
// StateFlow: keeps last value, ideal for UI state
private val _uiState = MutableStateFlow(UiState.Initial)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// SharedFlow: for one-shot events (navigation, snackbar)
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun onButtonClick() {
viewModelScope.launch {
_events.emit(UiEvent.NavigateToDetail)
}
}
}
sealed class UiEvent {
object NavigateToDetail : UiEvent()
data class ShowSnackbar(val message: String) : UiEvent()
}Essentiële Flow-operatoren
userRepository.getUsers()
.map { users -> users.filter { it.isActive } }
.distinctUntilChanged()
.debounce(300)
.flatMapLatest { users ->
fetchUserDetails(users)
}
.catch { e ->
emit(emptyList())
}
.onEach { users ->
analytics.logUserCount(users.size)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)Geavanceerde Patronen
Retry met Exponential Backoff
suspend fun <T> retryWithBackoff(
times: Int = 3,
initialDelay: Long = 100,
maxDelay: Long = 1000,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
Timber.w("Attempt ${attempt + 1} failed, retrying in ${currentDelay}ms")
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
return block()
}
// Usage
val user = retryWithBackoff {
userApi.getUser(userId)
}Timeout
suspend fun fetchWithTimeout() {
try {
val result = withTimeout(5000L) {
api.fetchData()
}
processResult(result)
} catch (e: TimeoutCancellationException) {
showError("Request took too long")
}
}
// Or with a default value
val result = withTimeoutOrNull(5000L) {
api.fetchData()
} ?: defaultValueConclusie
Kotlin coroutines zijn onmisbaar geworden voor moderne Android-ontwikkeling. Ze bieden een elegante en performante aanpak van asynchrone programmering, naadloos geintegreerd met het Jetpack-ecosysteem.
Checklist
- ✅ Gebruik
viewModelScopeenlifecycleScopeom geheugenlekken te voorkomen - ✅ Kies de juiste Dispatcher (Main, IO, Default)
- ✅ Handel fouten af met try/catch of het Result wrapper-patroon
- ✅ Implementeer cooperatieve annulering
- ✅ Gebruik Flow voor reactieve datastromen
- ✅ Geef de voorkeur aan StateFlow voor UI-state en SharedFlow voor events
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Het beheersen van coroutines levert een aanzienlijk voordeel op bij Android-projecten en technische sollicitatiegesprekken. Regelmatig oefenen en het verkennen van geavanceerde patronen vormen de sleutel tot volledige beheersing.
Tags
Delen
Gerelateerde artikelen

Kotlin 2.3 voor Android: Naamgebaseerde Destructurering, KMP en Sollicitatievragen 2026
Kotlin 2.3 sollicitatievragen over naamgebaseerde destructurering, Kotlin Multiplatform, contextparameters, coroutines en Flow. Voorbereiding op Android-ontwikkelaarsgesprekken in 2026 met praktische codevoorbeelden.

Jetpack Compose: Geavanceerde Animaties Stap voor Stap
Volledige gids voor geavanceerde Compose-animaties: transities, AnimatedVisibility, Animatable, gestures en performance voor vloeiende Android-interfaces.

De 20 meest gestelde Jetpack Compose interviewvragen in 2026
De 20 meest gestelde Jetpack Compose interviewvragen: recomposition, state management, navigatie, performance en architectuurpatronen.