Control de Datos de Localización y Tiempo con State Flows en Kotlin

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

Resumen

La programación reactiva con Kotlin Flow ofrece un enfoque poderoso para manejar datos en tiempo real, especialmente útil en aplicaciones que requieren seguimiento de ubicación. El uso de StateFlows y operadores como flatMapLatest permite crear sistemas robustos de observabilidad que responden dinámicamente a cambios en el estado de la aplicación, manteniendo un control preciso sobre los flujos de datos.

¿Cómo implementar un sistema de seguimiento de ubicación con Kotlin Flow?

El seguimiento de ubicación en aplicaciones móviles requiere un manejo eficiente de flujos de datos en tiempo real. Utilizando Kotlin Flow, podemos crear un sistema que observe la ubicación del dispositivo y registre estos datos junto con información temporal.

Para comenzar, necesitamos crear una variable que represente la ubicación actual y que dependa del estado de observación:

val currentLocation = isObservingLocation.flatMapLatest { isObserving ->
    if (isObserving) {
        locationObserver.observeLocation(intervalMillis = 1000)
    } else {
        emptyFlow()
    }
}.stateIn(
    scope = applicationScope,
    started = SharingStarted.Lazily,
    initialValue = null
)

En este código, estamos utilizando flatMapLatest, que funciona como un selector de canales. Dependiendo del valor de isObservingLocation, cambiamos entre observar la ubicación con intervalos de un segundo o detener completamente el flujo de datos. El operador stateIn asegura que esta información persista durante el ciclo de vida de la aplicación, iniciando el flujo solo cuando algún componente visual se suscribe a la variable.

¿Cómo gestionar el seguimiento y las pausas en la recopilación de datos?

El control preciso sobre cuándo recopilar datos de ubicación es fundamental para crear una experiencia de usuario fluida y para optimizar el uso de recursos. Para esto, implementamos una variable isTracking que nos ayuda a gestionar este comportamiento:

val isTracking = MutableStateFlow(false)

init {
    isTracking
        .distinctUntilChanged()
        .onEach { isTracking ->
            if (!isTracking) {
                val newList = buildList {
                    addAll(locationData.value)
                    add(emptyList())
                }
                locationData.update { newList }
            }
        }
        .flatMapLatest { isTracking ->
            if (isTracking) {
                tickerFlow(1000)
            } else {
                emptyFlow()
            }
        }
        .scan(0L) { acc, _ -> acc + 1 }
        .onEach { elapsedTime.update { it } }
        .launchIn(applicationScope)
}

Este código realiza varias operaciones importantes:

  1. Cuando el estado de seguimiento cambia a pausado, acumulamos la lista anterior de ubicaciones y creamos una nueva lista vacía
  2. Utilizamos isTracking para decidir si emitimos valores de tiempo o pausamos el flujo
  3. En cada emisión de tiempo, acumulamos ese tiempo en la variable elapsedTime
  4. El operador launchIn asegura que este flujo sobreviva durante todo el ciclo de vida de la aplicación

¿Cómo combinar datos de ubicación con información temporal?

La combinación de datos de ubicación con marcas de tiempo nos permite realizar análisis más complejos, como calcular distancias y velocidades. Para lograr esto, creamos un flujo que procesa cada emisión de ubicación:

currentLocation
    .filterNotNull()
    .combine(isTracking) { location, isTracking ->
        if (isTracking) location else null
    }
    .filterNotNull()
    .zip(elapsedTime) { location, time ->
        LocationWithTimestamp(location, time)
    }
    .onEach { locationWithTime ->
        val lastLocations = locationData.value
        if (lastLocations.isNotEmpty()) {
            val updatedSegment = lastLocations.last() + locationWithTime
            val updatedList = lastLocations.dropLast(1) + updatedSegment
            locationData.update { updatedList }
        } else {
            locationData.update { listOf(listOf(locationWithTime)) }
        }
        
        // Calcular distancia total
        val distanceMeters = calculateTotalDistance(locationData.value)
        totalDistance.update { distanceMeters }
    }
    .launchIn(applicationScope)

Este flujo es el corazón de nuestra aplicación de seguimiento de ubicación:

  1. Filtramos valores nulos de ubicación
  2. Combinamos la ubicación con el estado de seguimiento para emitir solo cuando estamos en modo activo
  3. Utilizamos zip para enlazar cada ubicación con su marca de tiempo correspondiente
  4. Actualizamos nuestra lista de segmentos de ubicación, agregando el nuevo punto al segmento actual
  5. Calculamos la distancia total recorrida y actualizamos la variable correspondiente

La estructura de datos que utilizamos (una lista de listas) nos permite mantener segmentos separados cuando el usuario pausa y reanuda el seguimiento, lo que facilita la visualización de rutas discontinuas en la interfaz de usuario.

La programación reactiva con Kotlin Flow nos proporciona herramientas poderosas para manejar flujos de datos complejos de manera elegante y eficiente. Al implementar un sistema de seguimiento de ubicación, podemos aprovechar operadores como flatMapLatest, combine y zip para crear una experiencia de usuario fluida y responsiva. ¿Has implementado alguna vez sistemas similares? Comparte tu experiencia en los comentarios.