Dominar las Coroutines de Kotlin: Guía Completa 2026
Aprende a dominar las coroutines de Kotlin para desarrollo Android: funciones suspend, scopes, dispatchers y patrones avanzados.

Las coroutines de Kotlin transformaron la programación asíncrona en Android. Atrás quedaron los callbacks anidados y el obsoleto AsyncTask: con coroutines, el código asíncrono se escribe de forma secuencial sin sacrificar rendimiento ni mantenibilidad.
Las coroutines son ligeras (miles pueden ejecutarse en un solo hilo), cancelables de forma nativa e integradas con Jetpack y el ecosistema moderno de Android.
Fundamentos Esenciales
Antes de escribir código, es clave entender qué hace tan poderosas a las coroutines.
¿Qué es una Coroutine?
Una coroutine es una instancia de cómputo suspendible. A diferencia de los hilos, las coroutines no bloquean: suspenden su ejecución y liberan el hilo para otras tareas.
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L) // Suspend without blocking
println("World!")
}
println("Hello,")
}
// Output: Hello, World!Funciones Suspend: El Corazón de las Coroutines
La palabra clave suspend indica que una función puede suspender la ejecución de la coroutine sin bloquear el hilo.
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)
}Regla de oro: Una función suspend solo puede invocarse desde otra función suspend o desde dentro de una coroutine.
Coroutine Scopes
El scope define el ciclo de vida de las coroutines. Esto es fundamental para evitar fugas de memoria.
viewModelScope: El Scope para 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: Para Activities y 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)
}
}
}
}
}
}Nunca se debe usar GlobalScope en una aplicación Android. Las coroutines lanzadas con GlobalScope no están vinculadas a ningún ciclo de vida y pueden causar fugas de memoria.
Dispatchers: Control de Ejecución
Los dispatchers determinan en qué hilo se ejecuta la coroutine.
Los 4 Dispatchers Principales
// 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: Cambio de Dispatcher
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)
}Ejecución Paralela con async/await
Para ejecutar tareas en paralelo y combinar resultados, se utiliza async.
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()
)
}
}Con async, las 3 llamadas se ejecutan en paralelo. Si cada una tarda 1 segundo, el tiempo total es ~1 segundo en lugar de 3 segundos secuenciales.
Manejo de Errores
try/catch Clásico
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)
}
}
}Patrón Result Wrapper
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)
}
}¿Listo para aprobar tus entrevistas de Android?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Cancelación: Limpieza Correcta
Las coroutines soportan cancelación cooperativa. Esto es esencial para evitar fugas de recursos.
Cancelación Automática con 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
}
}
}Verificación de Cancelación
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()
}Nunca absorber CancellationException: si se captura Exception, hay que relanzar CancellationException para que la cancelación se propague correctamente.
Flow: Programación Reactiva
Flow es el equivalente de RxJava Observable en coroutines, pero más simple e integrado.
Crear y Recolectar un Flow
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()
}Operadores Esenciales de Flow
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()
)Patrones Avanzados
Retry con Backoff Exponencial
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()
} ?: defaultValueConclusión
Las coroutines de Kotlin se han convertido en una herramienta esencial para el desarrollo Android moderno. Ofrecen un enfoque elegante y eficiente para la programación asíncrona, perfectamente integrado con el ecosistema Jetpack.
Checklist
- ✅ Usar
viewModelScopeylifecycleScopepara evitar fugas - ✅ Elegir el Dispatcher correcto (Main, IO, Default)
- ✅ Manejar errores con try/catch o Result wrapper
- ✅ Implementar cancelación cooperativa
- ✅ Usar Flow para flujos de datos reactivos
- ✅ Preferir StateFlow para estado UI, SharedFlow para eventos
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Dominar las coroutines otorga una ventaja significativa en proyectos Android y entrevistas técnicas. La práctica constante y la exploración de patrones avanzados consolidan estas habilidades.
¿Sabrías detectar el bug en Android?
Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Escrito por
Anthony Fillion-MailletFundador de SharpSkill
Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.
Actualizado el 2 de abril de 2026
Etiquetas
Compartir
Artículos relacionados

Kotlin Flow vs StateFlow vs SharedFlow: preguntas de entrevista de Android en 2026
Las preguntas sobre Kotlin Flow vs StateFlow vs SharedFlow que hacen los entrevistadores de Android en 2026, con respuestas claras, una tabla comparativa y código listo para producción.

Kotlin 2.3 para Android: Desestructuración por Nombre, KMP y Preguntas de Entrevista 2026
Preguntas de entrevista sobre Kotlin 2.3 para desarrolladores Android en 2026. Desestructuración por nombre, KMP, parámetros de contexto, Flow y coroutines con ejemplos de código.

Modularización Android en 2026: Arquitectura Multi-Módulo y Preguntas de Entrevista
Guía completa sobre modularización Android en 2026. Aprende a estructurar una arquitectura multi-módulo, usar catálogos de versiones Gradle y dominar las preguntas de entrevista técnica esenciales.