Programación Reactiva en Kotlin: Implementación de Flows y Timers

Clase 7 de 33Curso de Android: Integración de APIs nativas

Resumen

La programación reactiva en Kotlin es una herramienta poderosa para manejar flujos de datos asincrónicos, especialmente cuando necesitamos controlar variables como el tiempo en aplicaciones Android. Los Flows de Kotlin nos permiten crear emisiones de datos controladas que pueden ser fundamentales en proyectos de localización, donde la precisión temporal es clave para el seguimiento adecuado.

¿Cómo implementar un temporizador con Flows en Kotlin?

Para controlar las emisiones de tiempo en un proyecto de localización, necesitamos crear un temporizador que emita valores a intervalos regulares. Esto se logra mediante la implementación de Flows, que son la forma en que Kotlin maneja la programación reactiva.

Primero, debemos crear una estructura adecuada en nuestro proyecto:

  1. Crear una carpeta "domain" en la estructura del proyecto
  2. Dentro de domain, crear un archivo llamado "Timer"

La clase Timer será implementada como un singleton (object) para garantizar una única instancia:

object Timer {
    fun timeAndEmits(): Flow<Duration> {
        return flow {
            var lastEmitTime = System.currentTimeMillis()
            while (true) {
                // Esperar 200 milisegundos
                delay(200)
                val currentTime = System.currentTimeMillis()
                val upsTime = Duration.ofMillis(currentTime - lastEmitTime)
                emit(upsTime)
                lastEmitTime = currentTime
            }
        }
    }
}

Este código crea un Flow que emite valores de duración cada 200 milisegundos. El proceso es el siguiente:

  • Capturamos el tiempo inicial con System.currentTimeMillis()
  • Entramos en un bucle infinito
  • Esperamos 200 milisegundos con delay(200)
  • Calculamos el tiempo transcurrido desde la última emisión
  • Emitimos ese valor como un objeto Duration
  • Actualizamos el tiempo de la última emisión

La cadencia de 200 milisegundos es ideal para aplicaciones de localización, ya que proporciona actualizaciones frecuentes sin sobrecargar el sistema.

¿Cómo verificar que nuestro temporizador funciona correctamente?

Para comprobar que nuestro temporizador está funcionando, podemos utilizarlo en la MainActivity:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        lifecycleScope.launch {
            Timer.timeAndEmits().collect {
                Log.d("FlowTimer", "Timer interval: $it")
            }
        }
    }
}

En este código:

  1. Utilizamos lifecycleScope.launch para crear una corrutina vinculada al ciclo de vida de la actividad
  2. Invocamos nuestro método timeAndEmits() del objeto Timer
  3. Utilizamos el operador terminal collect para activar el Flow y procesar cada emisión
  4. Registramos cada emisión en el log para verificar su funcionamiento

Es importante entender que los Flows en Kotlin son "cold", lo que significa que no emiten valores hasta que se activan mediante un operador terminal como collect.

¿Qué son los operadores de Flow y cómo utilizarlos?

Los Flows en Kotlin tienen tres componentes principales:

  1. Builder: Construye el Flow y define cómo se emitirán los valores
  2. Operadores intermedios: Transforman los datos (map, filter, zip, combine, etc.)
  3. Operadores terminales: Activan el Flow y procesan las emisiones (collect, launchIn)

Hay una forma alternativa de activar un Flow utilizando el operador launchIn junto con onEach:

Timer.timeAndEmits()
    .onEach { 
        Log.d("FlowTimer", "Flow timer launchIn: $it") 
    }
    .launchIn(lifecycleScope)

Este enfoque es más declarativo y permite encadenar operadores de manera más clara.

¿Cómo combinar múltiples Flows?

Una de las características más poderosas de los Flows es la capacidad de combinar múltiples fuentes de datos. Podemos crear un segundo Flow que emita números aleatorios:

fun randomFlow(): Flow<Int> {
    return flow {
        while (true) {
            delay(1000)
            emit(Random.nextInt(0, 100))
        }
    }
}

Y luego combinar ambos Flows utilizando el operador zip:

Timer.timeAndEmits()
    .scan(Duration.ZERO) { acc, value -> acc + value }
    .zip(Timer.randomFlow()) { time, random -> Pair(time, random) }
    .onEach { 
        Log.d("ZipFlow", "Time: ${it.first}, Random: ${it.second}") 
    }
    .launchIn(lifecycleScope)

En este ejemplo:

  1. Utilizamos scan para acumular los valores de duración
  2. Combinamos el Flow de tiempo con el Flow de números aleatorios mediante zip
  3. Creamos pares de valores (tiempo, número aleatorio)
  4. Procesamos cada par emitido

Es importante notar que el operador zip emite valores solo cuando ambos Flows han emitido un nuevo valor. En nuestro caso, como el Flow de números aleatorios emite cada segundo, las emisiones combinadas ocurrirán aproximadamente cada segundo, aunque el Flow de tiempo emita cada 200 milisegundos.

La programación reactiva con Flows en Kotlin ofrece una forma elegante y eficiente de manejar datos asincrónicos, especialmente útil en aplicaciones de localización donde el control preciso del tiempo es fundamental. Dominar estos conceptos te permitirá crear aplicaciones más robustas y responsivas. ¿Has utilizado Flows en tus proyectos? Comparte tu experiencia en los comentarios.