Saltar al contenido principal

⏸️ Funciones suspend

📖 ¿Qué es suspend?

Las funciones suspend son funciones especiales que pueden suspender una corrutina. Son eficientes porque pueden esperar sin bloquear el hilo.

💡 Conceptos Básicos

Palabra clave suspend

// Una función suspend puede llamar a otras funciones suspend
suspend fun doSomething() {
delay(1000) // función suspend
println("¡Completado!")
}

fun main() = runBlocking {
doSomething() // Se llama dentro de una corrutina
}

Función normal vs función suspend

// ❌ No se puede llamar a funciones suspend desde funciones normales
fun normalFunction() {
// delay(1000) // ¡Error de compilación!
}

// ✅ Es posible en funciones suspend
suspend fun suspendFunction() {
delay(1000) // ¡OK!
}

🎯 Ejemplos Prácticos

Petición de Red

data class User(val id: String, val name: String)

suspend fun fetchUser(userId: String): User {
delay(1000) // Simulación de latencia de red
return User(userId, "Juan Pérez")
}

fun main() = runBlocking {
println("Iniciando petición")
val user = fetchUser("user123")
println("Usuario recibido: ${user.name}")
}

Ejecución Secuencial

suspend fun prepareFood(): String {
delay(1000)
return "Comida"
}

suspend fun setTable(): String {
delay(500)
return "Mesa"
}

suspend fun prepareMeal() {
println("Iniciando preparación")

val food = prepareFood()
println("$food lista")

val table = setTable()
println("$table lista")

println("¡Toda la preparación terminada!")
}

fun main() = runBlocking {
val time = measureTimeMillis {
prepareMeal()
}
println("Tiempo transcurrido: ${time}ms") // ~1500ms
}

Ejecución Paralela

suspend fun prepareMealParallel() = coroutineScope {
println("Iniciando preparación")

val foodDeferred = async { prepareFood() }
val tableDeferred = async { setTable() }

val food = foodDeferred.await()
val table = tableDeferred.await()

println("$food, $table listos")
}

fun main() = runBlocking {
val time = measureTimeMillis {
prepareMealParallel()
}
println("Tiempo transcurrido: ${time}ms") // ~1000ms (¡paralelo!)
}

🔧 withContext

Cambio de Dispatcher

suspend fun loadData(): String {
return withContext(Dispatchers.IO) {
// Se ejecuta en el hilo IO
delay(1000)
"Datos"
}
}

fun main() = runBlocking {
println("Hilo principal: ${Thread.currentThread().name}")

val data = loadData()

println("De vuelta al principal: ${Thread.currentThread().name}")
println("Datos: $data")
}

Múltiples Contextos

suspend fun processData() {
// Operación IO
val data = withContext(Dispatchers.IO) {
readFromFile()
}

// Operación intensiva de CPU
val processed = withContext(Dispatchers.Default) {
heavyComputation(data)
}

// Hilo principal (actualización de UI, etc.)
withContext(Dispatchers.Main) {
updateUI(processed)
}
}

suspend fun readFromFile(): String {
delay(500)
return "Contenido del archivo"
}

suspend fun heavyComputation(data: String): String {
delay(1000)
return data.uppercase()
}

suspend fun updateUI(data: String) {
println("Actualización de UI: $data")
}

🎨 Patrones Avanzados

Espera Condicional

suspend fun fetchDataIfNeeded(cache: String?): String {
return cache ?: fetchFromNetwork()
}

suspend fun fetchFromNetwork(): String {
delay(1000)
return "Datos nuevos"
}

fun main() = runBlocking {
// Con caché - retorno inmediato
val cached = fetchDataIfNeeded("Datos en caché")
println(cached)

// Sin caché - petición de red
val fresh = fetchDataIfNeeded(null)
println(fresh)
}

Encadenamiento

suspend fun step1(): Int {
delay(500)
return 10
}

suspend fun step2(input: Int): Int {
delay(500)
return input * 2
}

suspend fun step3(input: Int): Int {
delay(500)
return input + 5
}

suspend fun processChain(): Int {
val a = step1()
val b = step2(a)
val c = step3(b)
return c
}

fun main() = runBlocking {
val result = processChain()
println("Resultado final: $result") // 25 (10 * 2 + 5)
}

Manejo de Errores

suspend fun riskyOperation(): String {
delay(1000)
throw Exception("¡Error de red!")
}

suspend fun safeOperation(): String? {
return try {
riskyOperation()
} catch (e: Exception) {
println("Error ocurrido: ${e.message}")
null
}
}

fun main() = runBlocking {
val result = safeOperation()
println("Resultado: ${result ?: "Fallido"}")
}

🔥 Funciones Prácticas

Manejo de Timeout

suspend fun fetchWithTimeout(): String? {
return try {
withTimeout(2000) {
delay(3000) // Tarda demasiado
"Datos"
}
} catch (e: TimeoutCancellationException) {
println("¡Tiempo agotado!")
null
}
}

fun main() = runBlocking {
val result = fetchWithTimeout()
println("Resultado: ${result ?: "Timeout"}")
}

Lógica de Reintento

suspend fun <T> retry(
times: Int = 3,
initialDelay: Long = 100,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
println("Intento ${attempt + 1} fallido: ${e.message}")
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong()
}
return block() // Último intento
}

var attemptCount = 0

suspend fun unstableAPI(): String {
attemptCount++
if (attemptCount < 3) {
throw Exception("Error temporal")
}
return "¡Éxito!"
}

fun main() = runBlocking {
val result = retry(times = 5) {
unstableAPI()
}
println(result)
}

Patrón de Caché

class DataRepository {
private var cache: String? = null

suspend fun getData(forceRefresh: Boolean = false): String {
if (!forceRefresh && cache != null) {
println("Retornando desde caché")
return cache!!
}

println("Obteniendo desde red...")
delay(1000)
val data = "Datos nuevos"
cache = data
return data
}
}

fun main() = runBlocking {
val repo = DataRepository()

// Primera llamada - red
println(repo.getData())

// Segunda llamada - caché
println(repo.getData())

// Actualización forzada
println(repo.getData(forceRefresh = true))
}

🛡️ Funciones Cancelables

Verificación de Cancelación

suspend fun longRunningTask() {
repeat(10) { i ->
if (!isActive) {
println("¡Cancelado!")
return
}

println("Trabajo $i")
delay(500)
}
}

fun main() = runBlocking {
val job = launch {
longRunningTask()
}

delay(2000)
println("¡Cancelando trabajo!")
job.cancelAndJoin()
}

ensureActive

suspend fun heavyWork() {
repeat(10) { i ->
ensureActive() // Lanza excepción si está cancelado

println("Trabajo pesado $i")
Thread.sleep(500) // Simulación de trabajo de CPU
}
}

fun main() = runBlocking {
val job = launch {
try {
heavyWork()
} catch (e: CancellationException) {
println("El trabajo ha sido cancelado")
}
}

delay(2000)
job.cancel()
}

🤔 Preguntas Frecuentes

P1. ¿Las funciones suspend crean hilos?

R: ¡No! suspend solo suspende sin bloquear el hilo.

suspend fun example() {
println("Inicio: ${Thread.currentThread().name}")
delay(1000)
println("Fin: ${Thread.currentThread().name}")
}
// ¡Puede ejecutarse en el mismo hilo!

P2. ¿Y si hago todas las funciones suspend?

R: ¡Es innecesario! Úselo solo cuando haya operaciones asíncronas.

// ❌ Innecesario
suspend fun add(a: Int, b: Int) = a + b

// ✅ Necesario
suspend fun fetchData() = withContext(Dispatchers.IO) {
// Petición de red
}

P3. ¿Puedo llamar funciones normales desde funciones suspend?

R: ¡Por supuesto!

fun normalFunction() = println("Función normal")

suspend fun suspendFunction() {
normalFunction() // ¡OK!
delay(1000)
}

🎬 Conclusión

¡Código asíncrono eficiente con funciones suspend!

Resumen clave:
✅ Suspensión posible con suspend
✅ Espera sin bloquear hilos
✅ Cambio de hilos con withContext
✅ Control de ejecución secuencial/paralela
✅ Trabajos cancelables

Siguiente paso: ¡Aprenda sobre flujos de datos asíncronos en Flow!