Kotlin Coroutines meistern: Vollständiger Guide 2026

Kotlin Coroutines für die Android-Entwicklung meistern: Suspend-Funktionen, Scopes, Dispatcher und fortgeschrittene Patterns.

Vollständiger Guide zu Kotlin Coroutines für Android

Kotlin Coroutines haben die asynchrone Programmierung auf Android grundlegend verändert. Die Zeiten von Callback-Verschachtelungen und dem veralteten AsyncTask sind vorbei: Mit Coroutines lässt sich asynchroner Code sequentiell schreiben, ohne Performance oder Wartbarkeit zu opfern.

Warum Coroutines?

Coroutines sind leichtgewichtig (Tausende können auf einem einzigen Thread laufen), nativ abbrechbar und nahtlos in Jetpack und das moderne Android-Ökosystem integriert.

Die Grundlagen verstehen

Bevor es an den Code geht, lohnt sich ein Blick darauf, was Coroutines so leistungsfähig macht.

Was ist eine Coroutine?

Eine Coroutine ist eine Instanz einer suspendierbaren Berechnung. Anders als Threads blockieren Coroutines nicht: Sie suspendieren ihre Ausführung und geben den Thread für andere Aufgaben frei.

BasicCoroutine.ktkotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000L) // Suspend without blocking
        println("World!")
    }
    println("Hello,")
}
// Output: Hello, World!

Suspend-Funktionen: Das Herzstück der Coroutines

Das Schlüsselwort suspend zeigt an, dass eine Funktion die Coroutine-Ausführung suspendieren kann, ohne den Thread zu blockieren.

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)
}

Goldene Regel: Eine Suspend-Funktion kann nur aus einer anderen Suspend-Funktion oder innerhalb einer Coroutine aufgerufen werden.

Coroutine Scopes

Der Scope definiert den Lebenszyklus der Coroutines. Das ist entscheidend, um Memory Leaks zu vermeiden.

viewModelScope: Der Scope für 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: Für Activities und 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)
                    }
                }
            }
        }
    }
}
Vorsicht vor Leaks

GlobalScope sollte in einer Android-Anwendung niemals verwendet werden. Mit GlobalScope gestartete Coroutines sind an keinen Lebenszyklus gebunden und können Memory Leaks verursachen.

Dispatcher: Ausführung steuern

Dispatcher bestimmen, auf welchem Thread die Coroutine ausgeführt wird.

Die 4 wichtigsten Dispatcher

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: Dispatcher wechseln

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)
}

Parallele Ausführung mit async/await

Um Aufgaben parallel auszuführen und ihre Ergebnisse zu kombinieren, wird async verwendet.

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()
        )
    }
}
Performance

Mit async werden alle 3 Aufrufe parallel ausgeführt. Wenn jeder Aufruf 1 Sekunde dauert, beträgt die Gesamtzeit ~1 Sekunde statt 3 Sekunden sequentiell.

Fehlerbehandlung

Klassisches 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-Pattern

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)
    }
}

Bereit für deine Android-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Abbruch: Korrekte Bereinigung

Coroutines unterstützen kooperativen Abbruch. Das ist essenziell, um Ressourcenlecks zu vermeiden.

Automatischer Abbruch mit 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
        }
    }
}

Abbruch prüfen

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()
}

CancellationException niemals schlucken: Beim Fangen von Exception muss CancellationException erneut geworfen werden, damit der Abbruch korrekt propagiert wird.

Flow: Reaktive Programmierung

Flow ist das Coroutines-Äquivalent zu RxJava Observable, aber einfacher und besser integriert.

Einen Flow erstellen und sammeln

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()
}

Wichtige 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()
    )

Fortgeschrittene Patterns

Retry mit exponentiellem 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

Fazit

Kotlin Coroutines sind zu einem unverzichtbaren Werkzeug für die moderne Android-Entwicklung geworden. Sie bieten einen eleganten und performanten Ansatz für asynchrone Programmierung, perfekt integriert in das Jetpack-Ökosystem.

Checkliste

  • viewModelScope und lifecycleScope verwenden, um Leaks zu vermeiden
  • ✅ Den richtigen Dispatcher wählen (Main, IO, Default)
  • ✅ Fehler mit try/catch oder Result-Wrapper behandeln
  • ✅ Kooperativen Abbruch implementieren
  • ✅ Flow für reaktive Datenströme nutzen
  • ✅ StateFlow für UI-State bevorzugen, SharedFlow für Events

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Die Beherrschung von Coroutines verschafft einen deutlichen Vorteil bei Android-Projekten und technischen Interviews. Regelmäßiges Üben und das Erkunden fortgeschrittener Patterns festigen diese Fähigkeiten.

Tags

#kotlin
#coroutines
#android
#async
#concurrency

Teilen

Verwandte Artikel