Kotlin Coroutines per Android: Guida Completa 2026

Guida approfondita alle coroutine Kotlin per lo sviluppo Android: funzioni suspend, scope, dispatcher, Flow e pattern avanzati.

Guida completa alle Kotlin Coroutines per Android

Le coroutine Kotlin hanno trasformato radicalmente la programmazione asincrona su Android. Il periodo dei callback annidati e delle deprecate AsyncTask appartiene al passato: con le coroutine, lo sviluppatore scrive codice asincrono dall'aspetto sincrono, mantenendo prestazioni elevate e leggibilità del codice.

Perché le coroutine?

Le coroutine sono leggere (migliaia possono girare su un singolo thread), supportano la cancellazione nativa e si integrano perfettamente con Jetpack e l'ecosistema Android moderno.

Comprendere i Fondamenti

Prima di analizzare il codice, è fondamentale capire cosa rende le coroutine uno strumento così potente.

Cos'è una Coroutine?

Una coroutine è un'istanza di computazione sospendibile. A differenza dei thread, le coroutine non bloccano: sospendono la propria esecuzione e liberano il thread per altre operazioni.

BasicCoroutine.ktkotlin
import kotlinx.coroutines.*

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

Funzioni Suspend: Il Cuore delle Coroutine

La parola chiave suspend indica che una funzione può sospendere l'esecuzione della coroutine senza bloccare il thread.

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

Regola fondamentale: una funzione suspend può essere chiamata solo da un'altra funzione suspend o dall'interno di una coroutine.

Coroutine Scope

Lo scope definisce il ciclo di vita delle coroutine. Si tratta di un aspetto cruciale per evitare memory leak.

viewModelScope: Lo Scope per i ViewModel

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: Per Activity e Fragment

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)
                    }
                }
            }
        }
    }
}
Attenzione ai memory leak

Non utilizzare mai GlobalScope in un'applicazione Android. Le coroutine lanciate con GlobalScope non sono legate ad alcun ciclo di vita e possono causare memory leak.

Dispatcher: Controllare l'Esecuzione

I dispatcher determinano su quale thread viene eseguita la coroutine.

I 4 Dispatcher Principali

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

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

Esecuzione Parallela con async/await

Per eseguire operazioni in parallelo e combinarne i risultati, si utilizza async.

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

Con async, tutte e 3 le chiamate vengono eseguite in parallelo. Se ciascuna richiesta impiega 1 secondo, il tempo totale è circa 1 secondo anziché 3 secondi in sequenza.

Gestione degli Errori

try/catch Classico

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

Pattern Result Wrapper

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

Pronto a superare i tuoi colloqui su Android?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Cancellazione: Ripulire Correttamente

Le coroutine supportano la cancellazione cooperativa. Si tratta di un aspetto essenziale per evitare sprechi di risorse.

Cancellazione Automatica con gli Scope

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

Verificare la Cancellazione

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

Non intercettare mai CancellationException: se si cattura Exception, è necessario rilanciare CancellationException affinché la cancellazione si propaghi correttamente.

Flow: Programmazione Reattiva

Flow rappresenta l'equivalente di RxJava Observable nel mondo delle coroutine, ma con un'API più semplice e un'integrazione nativa.

Creare e Raccogliere un Flow

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

Operatori Flow Essenziali

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

Pattern Avanzati

Retry con 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

Conclusione

Le coroutine Kotlin sono diventate uno strumento imprescindibile per lo sviluppo Android moderno. Offrono un approccio elegante e performante alla programmazione asincrona, perfettamente integrato con l'ecosistema Jetpack.

Checklist

  • ✅ Utilizzare viewModelScope e lifecycleScope per evitare memory leak
  • ✅ Scegliere il Dispatcher corretto (Main, IO, Default)
  • ✅ Gestire gli errori con try/catch o il pattern Result wrapper
  • ✅ Implementare la cancellazione cooperativa
  • ✅ Utilizzare Flow per flussi di dati reattivi
  • ✅ Preferire StateFlow per lo stato UI e SharedFlow per gli eventi

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Padroneggiare le coroutine garantisce un vantaggio significativo nei progetti Android e nei colloqui tecnici. La pratica costante e l'esplorazione dei pattern avanzati sono la chiave per raggiungere la competenza completa.

Sfida del giorno

Sapresti trovare il bug in Android?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 2 aprile 2026

Tag

#kotlin
#coroutines
#android
#async
#concurrency

Condividi

Articoli correlati