Prepare su código Android para el futuro, Parte 2: Programación reactiva funcional en acción

Publicado: 2022-09-08

La programación reactiva funcional (FRP) es un paradigma que combina la reactividad de la programación reactiva con la composición de funciones declarativas de la programación funcional. Simplifica tareas complejas, crea interfaces de usuario elegantes y administra el estado sin problemas. Debido a estos y muchos otros beneficios claros, el uso de FRP se está generalizando en el desarrollo web y móvil.

Eso no significa que comprender este paradigma de programación sea fácil; incluso los desarrolladores experimentados pueden preguntarse: "¿Qué es exactamente FRP?" En la Parte 1 de este tutorial, definimos los conceptos fundamentales de FRP: programación funcional y programación reactiva. Esta entrega lo preparará para aplicarlo, con una descripción general de bibliotecas útiles y una implementación de muestra detallada.

Este artículo está escrito pensando en los desarrolladores de Android, pero los conceptos son relevantes y beneficiosos para cualquier desarrollador con experiencia en lenguajes de programación en general.

Primeros pasos con FRP: diseño del sistema

El paradigma FRP es un ciclo interminable de estados y eventos: State -> Event -> State' -> Event' -> State'' -> … . (Como recordatorio, ' , pronunciado “principal”, indica una nueva versión de la misma variable). Cada programa FRP comienza con un estado inicial que se actualizará con cada evento que reciba. Este programa incluye los mismos elementos que los de un programa reactivo:

  • Estado
  • Evento
  • La canalización declarativa (indicada como FRPViewModel function )
  • Observable (indicado como StateFlow )

Aquí, hemos reemplazado los elementos reactivos generales con componentes y bibliotecas reales de Android:

Dos cuadros azules principales, "StateFlow" y "State", tienen dos rutas principales entre ellos. La primera es a través de "Observa (escucha los cambios)". El segundo es a través de "Notifica (del último estado)" al cuadro azul "@Composable (JetpackCompose)", que va a través de "Transforma la entrada del usuario en" al cuadro azul "Evento", que va a través de "Activadores" al cuadro azul " Función FRPViewModel", y finalmente a través de "Produce (nuevo estado)". "Estado" luego también se conecta de nuevo a la "función FRPViewModel" a través de "Actúa como entrada para".
El ciclo funcional de programación reactiva en Android.

Exploración de bibliotecas y herramientas de FRP

Hay una variedad de bibliotecas y herramientas de Android que pueden ayudarlo a comenzar con FRP y que también son relevantes para la programación funcional:

  • Ivy FRP : esta es una biblioteca que escribí que se utilizará con fines educativos en este tutorial. Está pensado como un punto de partida para su enfoque de FRP, pero no está diseñado para su uso en producción, ya que carece del soporte adecuado. (Actualmente soy el único ingeniero que lo mantiene).
  • Arrow : esta es una de las mejores y más populares bibliotecas de Kotlin para FP, una que también usaremos en nuestra aplicación de muestra. Proporciona casi todo lo que necesita para ser funcional en Kotlin sin dejar de ser relativamente ligero.
  • Jetpack Compose : este es el conjunto de herramientas de desarrollo actual de Android para crear una interfaz de usuario nativa y es la tercera biblioteca que usaremos hoy. Es esencial para los desarrolladores modernos de Android. Recomiendo aprenderlo e incluso migrar su interfaz de usuario si aún no lo ha hecho.
  • Flujo : esta es la API de flujo de datos reactivo asíncrono de Kotlin; aunque no estamos trabajando con él en este tutorial, es compatible con muchas bibliotecas comunes de Android, como RoomDB, Retrofit y Jetpack. Flow funciona a la perfección con corrutinas y proporciona reactividad. Cuando se usa con RoomDB, por ejemplo, Flow garantiza que su aplicación siempre funcionará con los datos más recientes. Si ocurre un cambio en una tabla, los flujos dependientes de esta tabla recibirán el nuevo valor inmediatamente.
  • Kotest : esta plataforma de prueba ofrece soporte de prueba basado en propiedades relevante para el código de dominio FP puro.

Implementación de una aplicación de conversión de pies/metros de muestra

Veamos un ejemplo de FRP en funcionamiento en una aplicación de Android. Crearemos una aplicación simple que convierta valores entre metros (m) y pies (ft).

A los efectos de este tutorial, solo estoy cubriendo las partes del código vitales para comprender FRP, modificadas por simplicidad de mi aplicación de muestra de convertidor completo. Si desea continuar en Android Studio, cree su proyecto con una actividad Jetpack Compose e instale Arrow e Ivy FRP. Necesitará una versión minSdk de 28 o superior y una versión de idioma de Kotlin 1.6+.

Estado

Comencemos definiendo el estado de nuestra aplicación.

 // ConvState.kt enum class ConvType { METERS_TO_FEET, FEET_TO_METERS } data class ConvState( val conversion: ConvType, val value: Float, val result: Option<String> )

Nuestra clase de estado se explica por sí misma:

  • conversion : un tipo que describe lo que estamos convirtiendo: pies a metros o metros a pies.
  • value : el valor flotante que ingresa el usuario, que convertiremos más adelante.
  • result : Un resultado opcional que representa una conversión exitosa.

A continuación, debemos manejar la entrada del usuario como un evento.

Evento

ConvEvent como una clase sellada para representar la entrada del usuario:

 // ConvEvent.kt sealed class ConvEvent { data class SetConversionType(val conversion: ConvType) : ConvEvent() data class SetValue(val value: Float) : ConvEvent() object Convert : ConvEvent() }

Examinemos los propósitos de sus miembros:

  • SetConversionType : elige si estamos convirtiendo de pies a metros o de metros a pies.
  • SetValue : establece los valores numéricos que se utilizarán para la conversión.
  • Convert : realiza la conversión del valor ingresado utilizando el tipo de conversión.

Ahora, continuaremos con nuestro modelo de vista.

La canalización declarativa: controlador de eventos y composición de funciones

El modelo de vista contiene nuestro controlador de eventos y el código de composición de funciones (canalización declarativa):

 // ConverterViewModel.kt @HiltViewModel class ConverterViewModel @Inject constructor() : FRPViewModel<ConvState, ConvEvent>() { companion object { const val METERS_FEET_CONST = 3.28084f } // set initial state override val _state: MutableStateFlow<ConvState> = MutableStateFlow( ConvState( conversion = ConvType.METERS_TO_FEET, value = 1f, result = None ) ) override suspend fun handleEvent(event: ConvEvent): suspend () -> ConvState = when (event) { is ConvEvent.SetConversionType -> event asParamTo ::setConversion then ::convert is ConvEvent.SetValue -> event asParamTo ::setValue is ConvEvent.Convert -> stateVal() asParamTo ::convert } // ... }

Antes de analizar la implementación, analicemos algunos objetos específicos de la biblioteca Ivy FRP.

FRPViewModel<S,E> es una base de modelo de vista abstracta que implementa la arquitectura FRP. En nuestro código, necesitamos implementar los siguientes métodos:

  • val _state : define el valor inicial del estado (Ivy FRP usa Flow como un flujo de datos reactivo).
  • handleEvent(Event): suspend () -> S : Produce el siguiente estado de forma asíncrona dado un Event . La implementación subyacente lanza una nueva rutina para cada evento.
  • stateVal(): S : Devuelve el estado actual.
  • updateState((S) -> S): S Actualiza el estado de ViewModel .

Ahora, veamos algunos métodos relacionados con la composición de funciones:

  • then : Compone dos funciones juntas.
  • asParamTo : Produce una función g() = f(t) a partir de f(T) y un valor t (de tipo T ).
  • thenInvokeAfter : Compone dos funciones y luego las invoca.

updateState y thenInvokeAfter son métodos auxiliares que se muestran en el siguiente fragmento de código; se utilizarán en nuestro código de modelo de vista restante.

La canalización declarativa: implementaciones de funciones adicionales

Nuestro modelo de vista también contiene implementaciones de funciones para establecer nuestro tipo y valor de conversión, realizar las conversiones reales y formatear nuestro resultado final:

 // ConverterViewModel.kt @HiltViewModel class ConverterViewModel @Inject constructor() : FRPViewModel<ConvState, ConvEvent>() { // ... private suspend fun setConversion(event: ConvEvent.SetConversionType) = updateState { it.copy(conversion = event.conversion) } private suspend fun setValue(event: ConvEvent.SetValue) = updateState { it.copy(value = event.value) } private suspend fun convert( state: ConvState ) = state.value asParamTo when (stateVal().conversion) { ConvType.METERS_TO_FEET -> ::convertMetersToFeet ConvType.FEET_TO_METERS -> ::convertFeetToMeters } then ::formatResult thenInvokeAfter { result -> updateState { it.copy(result = Some(result)) } } private fun convertMetersToFeet(meters: Float): Float = meters * METERS_FEET_CONST private fun convertFeetToMeters(ft: Float): Float = ft / METERS_FEET_CONST private fun formatResult(result: Float): String = DecimalFormat("###,###.##").format(result) }

Con una comprensión de nuestras funciones auxiliares de Ivy FRP, estamos listos para analizar el código. Comencemos con la funcionalidad principal: convert . convert acepta el estado ( ConvState ) como entrada y produce una función que genera un nuevo estado que contiene el resultado de la entrada convertida. En pseudocódigo, podemos resumirlo como: State (ConvState) -> Value (Float) -> Converted value (Float) -> Result (Option<String>) .

El manejo de eventos Event.SetValue es sencillo; simplemente actualiza el estado con el valor del evento (es decir, el usuario ingresa un número para convertir). Sin embargo, manejar el evento Event.SetConversionType es un poco más interesante porque hace dos cosas:

  • Actualiza el estado con el tipo de conversión seleccionado ( ConvType ).
  • Utiliza convert para convertir el valor actual en función del tipo de conversión seleccionado.

Usando el poder de la composición, podemos usar la función convert: State -> State como entrada para otras composiciones. Es posible que haya notado que el código demostrado anteriormente no es puro: estamos mutando protected abstract val _state: MutableStateFlow<S> en FRPViewModel , lo que genera efectos secundarios cada vez que usamos updateState {} . El código FP completamente puro para Android en Kotlin no es factible.

Dado que la composición de funciones que no son puras puede generar resultados impredecibles, un enfoque híbrido es el más práctico: use funciones puras en su mayor parte y asegúrese de que las funciones impuras tengan efectos secundarios controlados. Esto es exactamente lo que hemos hecho arriba.

Observable y IU

Nuestro paso final es definir la interfaz de usuario de nuestra aplicación y dar vida a nuestro convertidor.

Un gran rectángulo gris con cuatro flechas apuntando hacia él desde la derecha. De arriba a abajo, la primera flecha, denominada "Botones", apunta a dos rectángulos más pequeños: un rectángulo azul oscuro a la izquierda con el texto en mayúsculas "Metros a pies" y un rectángulo azul claro a la derecha con el texto "Pies a metros". La segunda flecha, etiquetada como "TextField", apunta a un rectángulo blanco con texto alineado a la izquierda, "100.0". La tercera flecha, denominada "Botón", apunta a un rectángulo verde alineado a la izquierda con el texto "Convertir". La última flecha, etiquetada como "Texto", apunta al texto azul alineado a la izquierda que dice: "Resultado: 328.08 pies".
Una maqueta de la interfaz de usuario de la aplicación.

La interfaz de usuario de nuestra aplicación será un poco "fea", pero el objetivo de este ejemplo es demostrar FRP, no crear un diseño atractivo con Jetpack Compose.

 // ConverterScreen.kt @Composable fun BoxWithConstraintsScope.ConverterScreen(screen: ConverterScreen) { FRP<ConvState, ConvEvent, ConverterViewModel> { state, onEvent -> UI(state, onEvent) } }

Nuestro código de interfaz de usuario utiliza los principios básicos de Jetpack Compose en la menor cantidad de líneas de código posible. Sin embargo, hay una función interesante que vale la pena mencionar: FRP<ConvState, ConvEvent, ConverterViewModel> . FRP es una función componible del marco Ivy FRP, que hace varias cosas:

  • Instancia el modelo de vista usando @HiltViewModel .
  • Observa el State del modelo de vista mediante el flujo.
  • Propaga eventos a ViewModel con el código onEvent: (Event) -> Unit) .
  • Proporciona una función de orden superior @Composable que realiza la propagación de eventos y recibe el estado más reciente.
  • Opcionalmente, proporciona una forma de pasar initialEvent , que se llama una vez que se inicia la aplicación.

Así es como se implementa la función FRP en la biblioteca Ivy FRP:

 @Composable inline fun <S, E, reified VM : FRPViewModel<S, E>> BoxWithConstraintsScope.FRP( initialEvent: E? = null, UI: @Composable BoxWithConstraintsScope.( state: S, onEvent: (E) -> Unit ) -> Unit ) { val viewModel: VM = viewModel() val state by viewModel.state().collectAsState() if (initialEvent != null) { onScreenStart { viewModel.onEvent(initialEvent) } } UI(state, viewModel::onEvent) }

Puede encontrar el código completo del ejemplo del convertidor en GitHub, y el código completo de la interfaz de usuario se puede encontrar en la función de UI de usuario del archivo ConverterScreen.kt . Si desea experimentar con la aplicación o el código, puede clonar el repositorio Ivy FRP y ejecutar la aplicación de sample en Android Studio. Es posible que su emulador necesite más almacenamiento antes de que la aplicación pueda ejecutarse.

Arquitectura de Android más limpia con FRP

Con una sólida comprensión básica de la programación funcional, la programación reactiva y, finalmente, la programación reactiva funcional, está listo para aprovechar los beneficios de FRP y crear una arquitectura de Android más limpia y fácil de mantener.

El blog de ingeniería de Toptal agradece a Tarun Goyal por revisar los ejemplos de código presentados en este artículo.