Kotlin Coroutines voor Android: Complete Gids 2026

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

Complete gids over Kotlin Coroutines voor Android

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.

Waarom coroutines?

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.

BasicCoroutine.ktkotlin
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.

SuspendFunction.ktkotlin
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

UserViewModel.ktkotlin
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

UserFragment.ktkotlin
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)
                    }
                }
            }
        }
    }
}
Let op voor geheugenlekken

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

Dispatchers.ktkotlin
// 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

ImageProcessor.ktkotlin
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.

ParallelExecution.ktkotlin
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()
        )
    }
}
Prestaties

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

ErrorHandling.ktkotlin
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

ExceptionHandler.ktkotlin
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

ResultPattern.ktkotlin
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

SearchViewModel.ktkotlin
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

CancellationCheck.ktkotlin
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

FlowBasics.ktkotlin
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

StateFlowVsSharedFlow.ktkotlin
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

FlowOperators.ktkotlin
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

RetryPattern.ktkotlin
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

TimeoutPattern.ktkotlin
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()
} ?: defaultValue

Conclusie

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 viewModelScope en lifecycleScope om 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

#kotlin
#coroutines
#android
#async
#concurrency

Delen

Gerelateerde artikelen