Pérennité de votre code Android, partie 2 : programmation réactive fonctionnelle en action

Publié: 2022-09-08

La programmation réactive fonctionnelle (FRP) est un paradigme qui combine la réactivité de la programmation réactive avec la composition de fonction déclarative de la programmation fonctionnelle. Il simplifie les tâches complexes, crée des interfaces utilisateur élégantes et gère l'état en douceur. En raison de ces avantages et de nombreux autres avantages évidents, l'utilisation de FRP se généralise dans le développement mobile et Web.

Cela ne veut pas dire qu'il est facile de comprendre ce paradigme de programmation - même les développeurs chevronnés peuvent se demander : "Qu'est-ce que le FRP exactement ?" Dans la partie 1 de ce didacticiel, nous avons défini les concepts fondamentaux de FRP : la programmation fonctionnelle et la programmation réactive. Cet article vous préparera à l'appliquer, avec un aperçu des bibliothèques utiles et un exemple détaillé d'implémentation.

Cet article est écrit en pensant aux développeurs Android, mais les concepts sont pertinents et bénéfiques pour tout développeur ayant une expérience des langages de programmation généraux.

Premiers pas avec FRP : conception du système

Le paradigme FRP est un cycle sans fin d'états et d'événements : State -> Event -> State' -> Event' -> State'' -> … . (Pour rappel, ' , prononcé « premier », indique une nouvelle version de la même variable.) Chaque programme FRP démarre avec un état initial qui sera mis à jour à chaque événement qu'il reçoit. Ce programme comprend les mêmes éléments que ceux d'un programme réactif :

  • État
  • Événement
  • Le pipeline déclaratif (indiqué comme FRPViewModel function )
  • Observable (indiqué comme StateFlow )

Ici, nous avons remplacé les éléments réactifs généraux par de vrais composants et bibliothèques Android :

Deux boîtes bleues principales, "StateFlow" et "State", ont deux chemins principaux entre elles. La première est via "Observe (écoute les changements)". La seconde est via "Notifie (du dernier état)", à la boîte bleue "@Composable (JetpackCompose)", qui passe par "Transforme l'entrée de l'utilisateur en" à la boîte bleue "Evénement", qui passe par "Déclencheurs" à la boîte bleue " Fonction FRPViewModel", et enfin via "Produit (nouvel état)". "State" se reconnecte alors également à la "fonction FRPViewModel" via "Agit comme entrée pour".
Le cycle de programmation réactive fonctionnelle sous Android.

Explorer les bibliothèques et les outils FRP

Il existe une variété de bibliothèques et d'outils Android qui peuvent vous aider à démarrer avec FRP, et qui sont également pertinents pour la programmation fonctionnelle :

  • Ivy FRP : Il s'agit d'une bibliothèque que j'ai écrite et qui sera utilisée à des fins pédagogiques dans ce tutoriel. Il est conçu comme un point de départ pour votre approche du FRP, mais n'est pas destiné à une utilisation en production car il manque d'un support approprié. (Je suis actuellement le seul ingénieur à le maintenir.)
  • Arrow : Il s'agit de l'une des bibliothèques Kotlin les meilleures et les plus populaires pour FP, celle que nous utiliserons également dans notre exemple d'application. Il fournit presque tout ce dont vous avez besoin pour devenir fonctionnel dans Kotlin tout en restant relativement léger.
  • Jetpack Compose : Il s'agit de la boîte à outils de développement actuelle d'Android pour créer une interface utilisateur native et c'est la troisième bibliothèque que nous utiliserons aujourd'hui. Il est essentiel pour les développeurs Android modernes. Je vous recommande de l'apprendre et même de migrer votre interface utilisateur si vous ne l'avez pas déjà fait.
  • Flux : il s'agit de l'API de flux de données réactive asynchrone de Kotlin ; bien que nous ne l'utilisions pas dans ce didacticiel, il est compatible avec de nombreuses bibliothèques Android courantes telles que RoomDB, Retrofit et Jetpack. Flow fonctionne de manière transparente avec les coroutines et offre une réactivité. Lorsqu'il est utilisé avec RoomDB, par exemple, Flow garantit que votre application fonctionnera toujours avec les dernières données. En cas de changement dans une table, les flux dépendant de cette table recevront immédiatement la nouvelle valeur.
  • Kotest : Cette plate-forme de test offre une prise en charge des tests basés sur les propriétés concernant le code de domaine FP pur.

Implémentation d'un exemple d'application de conversion pieds/mètres

Voyons un exemple de FRP au travail dans une application Android. Nous allons créer une application simple qui convertit les valeurs entre mètres (m) et pieds (ft).

Pour les besoins de ce didacticiel, je ne couvre que les parties de code essentielles à la compréhension de FRP, modifiées par souci de simplicité à partir de mon exemple d'application de convertisseur complet. Si vous souhaitez suivre dans Android Studio, créez votre projet avec une activité Jetpack Compose et installez Arrow et Ivy FRP. Vous aurez besoin d'une version minSdk de 28 ou supérieure et d'une version linguistique de Kotlin 1.6+.

État

Commençons par définir l'état de notre application.

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

Notre classe d'état est assez explicite :

  • conversion : un type décrivant ce que nous convertissons entre—pieds en mètres ou mètres en pieds.
  • value : Le float que l'utilisateur saisit, que nous convertirons plus tard.
  • result : un résultat facultatif qui représente une conversion réussie.

Ensuite, nous devons gérer l'entrée de l'utilisateur comme un événement.

Événement

Nous avons défini ConvEvent comme une classe scellée pour représenter l'entrée utilisateur :

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

Examinons les objectifs de ses membres :

  • SetConversionType : Choisit si nous convertissons des pieds en mètres ou des mètres en pieds.
  • SetValue : définit les valeurs numériques qui seront utilisées pour la conversion.
  • Convert : Effectue la conversion de la valeur saisie à l'aide du type de conversion.

Maintenant, nous allons continuer avec notre modèle de vue.

Le pipeline déclaratif : gestionnaire d'événements et composition de fonctions

Le modèle de vue contient notre code de gestionnaire d'événements et de composition de fonction (pipeline déclaratif) :

 // 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 } // ... }

Avant d'analyser l'implémentation, décomposons quelques objets spécifiques à la bibliothèque Ivy FRP.

FRPViewModel<S,E> est une base de modèle de vue abstraite qui implémente l'architecture FRP. Dans notre code, nous devons implémenter les méthodes suivantes :

  • val _state : définit la valeur initiale de l'état (Ivy FRP utilise Flow comme flux de données réactif).
  • handleEvent(Event): suspend () -> S : Produit l'état suivant de manière asynchrone en fonction d'un Event . L'implémentation sous-jacente lance une nouvelle coroutine pour chaque événement.
  • stateVal(): S : Renvoie l'état actuel.
  • updateState((S) -> S): S Met à jour l'état du ViewModel .

Voyons maintenant quelques méthodes liées à la composition de fonctions :

  • then : Compose deux fonctions ensemble.
  • asParamTo : Produit une fonction g() = f(t) à partir de f(T) et une valeur t (de type T ).
  • thenInvokeAfter : compose deux fonctions puis les appelle.

updateState et thenInvokeAfter sont des méthodes d'assistance présentées dans l'extrait de code suivant ; ils seront utilisés dans notre code de modèle de vue restant.

Le pipeline déclaratif : implémentations de fonctions supplémentaires

Notre modèle de vue contient également des implémentations de fonctions pour définir notre type et notre valeur de conversion, effectuer les conversions réelles et formater notre résultat 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) }

Avec une compréhension de nos fonctions d'assistance Ivy FRP, nous sommes prêts à analyser le code. Commençons par la fonctionnalité de base : convert . convert accepte l'état ( ConvState ) en entrée et produit une fonction qui génère un nouvel état contenant le résultat de l'entrée convertie. En pseudocode, nous pouvons le résumer ainsi : State (ConvState) -> Value (Float) -> Converted value (Float) -> Result (Option<String>) .

La gestion de l'événement Event.SetValue est simple ; il met simplement à jour l'état avec la valeur de l'événement (c'est-à-dire que l'utilisateur saisit un nombre à convertir). Cependant, la gestion de l'événement Event.SetConversionType est un peu plus intéressante car il fait deux choses :

  • Met à jour l'état avec le type de conversion sélectionné ( ConvType ).
  • Utilise convert pour convertir la valeur actuelle en fonction du type de conversion sélectionné.

En utilisant la puissance de la composition, nous pouvons utiliser la fonction convert: State -> State comme entrée pour d'autres compositions. Vous avez peut-être remarqué que le code présenté ci-dessus n'est pas pur : nous mutant protected abstract val _state: MutableStateFlow<S> dans FRPViewModel , ce qui entraîne des effets secondaires chaque fois que nous utilisons updateState {} . Un code FP complètement pur pour Android dans Kotlin n'est pas réalisable.

Étant donné que la composition de fonctions qui ne sont pas pures peut conduire à des résultats imprévisibles, une approche hybride est la plus pratique : utilisez des fonctions pures pour la plupart et assurez-vous que toutes les fonctions impures ont des effets secondaires contrôlés. C'est exactement ce que nous avons fait ci-dessus.

Observable et interface utilisateur

Notre dernière étape consiste à définir l'interface utilisateur de notre application et à donner vie à notre convertisseur.

Un grand rectangle gris avec quatre flèches pointant vers lui depuis la droite. De haut en bas, la première flèche, intitulée "Boutons", pointe vers deux rectangles plus petits : un rectangle bleu foncé à gauche avec le texte en majuscule "Mètres en pieds" et un rectangle bleu clair à droite avec le texte "Pieds en mètres". La deuxième flèche, intitulée "TextField", pointe vers un rectangle blanc avec du texte aligné à gauche, "100.0". La troisième flèche, intitulée "Bouton", pointe vers un rectangle vert aligné à gauche avec le texte "Convertir". La dernière flèche, intitulée "Texte", pointe vers le texte bleu aligné à gauche indiquant : "Résultat : 328,08 pieds".
Une maquette de l'interface utilisateur de l'application.

L'interface utilisateur de notre application sera un peu "laide", mais le but de cet exemple est de démontrer le FRP, pas de créer un beau design à l'aide de Jetpack Compose.

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

Notre code d'interface utilisateur utilise les principes de base de Jetpack Compose dans le moins de lignes de code possible. Cependant, il y a une fonction intéressante à mentionner : FRP<ConvState, ConvEvent, ConverterViewModel> . FRP est une fonction composable du framework Ivy FRP, qui fait plusieurs choses :

  • Instancie le modèle de vue à l'aide @HiltViewModel .
  • Observe l' State du modèle de vue à l'aide de Flow.
  • Propage les événements au ViewModel avec le code onEvent: (Event) -> Unit) .
  • Fournit une fonction d'ordre supérieur @Composable qui effectue la propagation des événements et reçoit le dernier état.
  • Fournit éventuellement un moyen de transmettre initialEvent , qui est appelé une fois que l'application démarre.

Voici comment la fonction FRP est implémentée dans la bibliothèque 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) }

Vous pouvez trouver le code complet de l'exemple de convertisseur dans GitHub, et le code complet de l'interface utilisateur se trouve dans la fonction UI du fichier ConverterScreen.kt . Si vous souhaitez expérimenter l'application ou le code, vous pouvez cloner le référentiel Ivy FRP et exécuter l' sample d'application dans Android Studio. Votre émulateur peut avoir besoin d'un espace de stockage accru avant que l'application puisse s'exécuter.

Architecture Android plus propre avec FRP

Avec une solide compréhension de base de la programmation fonctionnelle, de la programmation réactive et, enfin, de la programmation réactive fonctionnelle, vous êtes prêt à tirer parti des avantages du FRP et à créer une architecture Android plus propre et plus maintenable.

Le blog Toptal Engineering exprime sa gratitude à Tarun Goyal pour avoir examiné les exemples de code présentés dans cet article.