Prepare su código Android para el futuro, Parte 2: Programación reactiva funcional en acción
Publicado: 2022-09-08La 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:
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 unEvent
. La implementación subyacente lanza una nueva rutina para cada evento. -
stateVal(): S
: Devuelve el estado actual. -
updateState((S) -> S): S
Actualiza el estado deViewModel
.
Ahora, veamos algunos métodos relacionados con la composición de funciones:
-
then
: Compone dos funciones juntas. -
asParamTo
: Produce una funcióng() = f(t)
a partir def(T)
y un valort
(de tipoT
). -
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.
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ódigoonEvent: (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.