Saltar a contenido

AA2 · Ejercicios de corrutinas y Flow

Igual que en la unidad anterior: parte del esqueleto, escribe la solución en un .kt y pega el main para comprobarla. Tu salida debe coincidir con el resultado esperado, obtenido ejecutando la solución modelo de verdad. Necesitas la dependencia kotlinx-coroutines-core en tu proyecto (en Android Studio ya viene con Compose; en IntelliJ añádela a Gradle). Los marcados con ⭐ son más difíciles.

Ejercicio AA2.1

Dos peticiones a la vez. Escribe cargarTodo() para que pedirPerfil y pedirAmigos se ejecuten en paralelo (no una tras otra) y devuelva ambos resultados.

suspend fun pedirPerfil(): String { delay(300); return "Ana" }
suspend fun pedirAmigos(): List<String> { delay(300); return listOf("Luis", "Marta") }

suspend fun cargarTodo(): Pair<String, List<String>> {
    // ...
}

Para comprobarlo:

fun main() = runBlocking {
    val inicio = System.currentTimeMillis()
    val (perfil, amigos) = cargarTodo()
    val ms = System.currentTimeMillis() - inicio
    println("$perfil · $amigos")
    println("¿en paralelo (menos de 500 ms)? ${ms < 500}")
}

Resultado esperado:

Ana · [Luis, Marta]
¿en paralelo (menos de 500 ms)? true
Solución modelo (bloqueada)

🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:

suspend fun pedirPerfil(): String { delay(300); return "Ana" }
suspend fun pedirAmigos(): List<String> { delay(300); return listOf("Luis", "Marta") }
suspend fun cargarTodo(): Pair<String, List<String>> = coroutineScope {
    val perfil = async { pedirPerfil() }
    val amigos = async { pedirAmigos() }
// ... (resto de la solución bloqueado)

Ejercicio AA2.2

Cancelar de verdad. contar() se cancela a sí misma al llegar a 3, pero sigue contando hasta 5. Arréglala con una sola línea para que respete la cancelación y se detenga.

// Cuenta de 1 a 5 imprimiendo cada número. Se cancela a sí misma al llegar a 3.
// Hoy imprime los cinco: arréglala para que se detenga tras el 3.
suspend fun contar() = coroutineScope {
    for (i in 1..5) {
        println("número $i")
        if (i == 3) coroutineContext.job.cancel()
    }
}

Para comprobarlo:

fun main() = runBlocking {
    val trabajo = launch(Dispatchers.Default) { contar() }
    trabajo.join()
    println("¿cancelado? ${trabajo.isCancelled}")
}

Resultado esperado:

número 1
número 2
número 3
¿cancelado? true
Solución modelo (bloqueada)

🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:

suspend fun contar() = coroutineScope {
    for (i in 1..5) {
        ensureActive()
        println("número $i")
        if (i == 3) coroutineContext.job.cancel()
// ... (resto de la solución bloqueado)

Ejercicio AA2.3

Que falle una no hunde a las demás. Lanza las tres tareas a la vez y devuelve solo los resultados de las que terminan bien. La B falla, y aun así deben salir A ok y C ok.

suspend fun tarea(nombre: String, falla: Boolean): String {
    delay(50)
    if (falla) throw IllegalStateException("$nombre falló")
    return "$nombre ok"
}

// Lanza las tres tareas a la vez y devuelve los resultados de las que NO fallan.
suspend fun lanzarTodas(): List<String> {
    // ...
}

Para comprobarlo:

fun main() = runBlocking {
    println(lanzarTodas())
}

Resultado esperado:

[A ok, C ok]
Solución modelo (bloqueada)

🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:

suspend fun tarea(nombre: String, falla: Boolean): String {
    delay(50)
    if (falla) throw IllegalStateException("$nombre falló")
    return "$nombre ok"
}
// ... (resto de la solución bloqueado)

Ejercicio AA2.4

Operadores. Completa multiplos() para que emita, de los números del 1 al 20, los múltiplos de 3, multiplicados por 10, y solo los 4 primeros.

// Los números del 1 al 20 que sean múltiplos de 3, multiplicados por 10, quedándote solo con los 4 primeros.
fun multiplos(): Flow<Int> = (1..20).asFlow() // + los operadores que faltan

Para comprobarlo:

fun main() = runBlocking {
    println(multiplos().toList())
}

Resultado esperado:

[30, 60, 90, 120]
Solución modelo (bloqueada)

🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:

fun multiplos(): Flow<Int> = (1..20).asFlow()
    .filter { it % 3 == 0 }
    .map { it * 10 }
    .take(4)
fun main() = runBlocking {
// ... (resto de la solución bloqueado)

Ejercicio AA2.5

Búsqueda mientras escribes. Escribe consultas: tras 100 ms de calma, sin repetir la misma consulta seguida, ignorando las de menos de 3 letras y cancelando una búsqueda en curso si llega otra. Cada búsqueda debe producir el texto buscado: seguido de la consulta.

// Recibe lo que el usuario va escribiendo y devuelve los textos por los que realmente se buscaría:
// espera 100 ms de calma, no repite, ignora menos de 3 letras y descarta la búsqueda anterior si llega otra.
fun consultas(teclado: Flow<String>): Flow<String> {
    // ...
}

Para comprobarlo:

fun main() = runBlocking {
    val teclado = flow {
        emit("a"); delay(20)
        emit("an"); delay(20)
        emit("ana"); delay(300)          // pausa: se busca «ana»
        emit("ana"); delay(300)          // repetida: no se busca otra vez
        emit("anab"); delay(20)
        emit("anabe"); delay(300)        // pausa: se busca «anabe»
    }
    consultas(teclado).collect { println(it) }
}

Resultado esperado:

buscado: ana
buscado: anabe
Solución modelo (bloqueada)

🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:

@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
fun consultas(teclado: Flow<String>): Flow<String> = teclado
    .debounce(100)
    .distinctUntilChanged()
    .filter { it.length >= 3 }
// ... (resto de la solución bloqueado)

Ejercicio AA2.6

⭐ Total calculado. Construye con combine y stateIn (arranque inmediato, valor inicial PrecioUi(0, 0, 0)) un estado cuyo total sea unidades × precio y se recalcule al cambiar cualquiera de los dos.

data class PrecioUi(val unidades: Int, val precioUnidad: Int, val total: Int)

// Combina las unidades y el precio por unidad en un StateFlow<PrecioUi> con el total calculado.
fun construir(unidades: Flow<Int>, precio: Flow<Int>, ambito: CoroutineScope): StateFlow<PrecioUi> {
    // ...
}

Para comprobarlo:

fun main() = runBlocking {
    val unidades = MutableStateFlow(1)
    val precio = MutableStateFlow(10)
    val ui = construir(unidades, precio, CoroutineScope(Dispatchers.Unconfined))
    println(ui.value)
    unidades.value = 3
    println(ui.value)
    precio.value = 12
    println(ui.value)
}

Resultado esperado:

PrecioUi(unidades=1, precioUnidad=10, total=10)
PrecioUi(unidades=3, precioUnidad=10, total=30)
PrecioUi(unidades=3, precioUnidad=12, total=36)
Solución modelo (bloqueada)

🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:

data class PrecioUi(val unidades: Int, val precioUnidad: Int, val total: Int)
fun construir(unidades: Flow<Int>, precio: Flow<Int>, ambito: CoroutineScope): StateFlow<PrecioUi> =
    combine(unidades, precio) { u, p -> PrecioUi(u, p, u * p) }
        .stateIn(ambito, SharingStarted.Eagerly, PrecioUi(0, 0, 0))
fun main() = runBlocking {
// ... (resto de la solución bloqueado)