Machen Sie Ihren Android-Code zukunftssicher, Teil 2: Funktionale reaktive Programmierung in Aktion

Veröffentlicht: 2022-09-08

Funktionale reaktive Programmierung (FRP) ist ein Paradigma, das die Reaktivität der reaktiven Programmierung mit der deklarativen Funktionskomposition der funktionalen Programmierung kombiniert. Es vereinfacht komplexe Aufgaben, erstellt elegante Benutzeroberflächen und verwaltet den Zustand reibungslos. Aufgrund dieser und vieler anderer klarer Vorteile wird die Verwendung von FRP in der Mobil- und Webentwicklung zum Mainstream.

Das bedeutet nicht, dass es einfach ist, dieses Programmierparadigma zu verstehen – selbst erfahrene Entwickler fragen sich vielleicht: „Was genau ist FRP?“ In Teil 1 dieses Tutorials haben wir die grundlegenden Konzepte von FRP definiert: funktionale Programmierung und reaktive Programmierung. Dieser Teil bereitet Sie mit einem Überblick über nützliche Bibliotheken und einer detaillierten Beispielimplementierung auf die Anwendung vor.

Dieser Artikel wurde für Android-Entwickler geschrieben, aber die Konzepte sind relevant und nützlich für alle Entwickler mit Erfahrung in allgemeinen Programmiersprachen.

Erste Schritte mit FRP: Systemdesign

Das FRP-Paradigma ist ein endloser Kreislauf von Zuständen und Ereignissen: State -> Event -> State' -> Event' -> State'' -> … . (Zur Erinnerung: ' , ausgesprochen „Prime“, zeigt eine neue Version derselben Variablen an.) Jedes FRP-Programm beginnt mit einem Anfangszustand, der mit jedem empfangenen Ereignis aktualisiert wird. Dieses Programm enthält die gleichen Elemente wie ein reaktives Programm:

  • Bundesland
  • Vorfall
  • Die deklarative Pipeline (als FRPViewModel function angegeben)
  • Beobachtbar (angegeben als StateFlow )

Hier haben wir die allgemeinen reaktiven Elemente durch echte Android-Komponenten und -Bibliotheken ersetzt:

Zwei blaue Hauptfelder, „StateFlow“ und „State“, haben zwei Hauptpfade zwischen sich. Die erste ist über "Beobachtet (überwacht Änderungen)." Der zweite geht über „Benachrichtigt (über den neuesten Stand)“ zum blauen Feld „@Composable (JetpackCompose)“, das über „Transforms user input to“ zum blauen Feld „Event“, das über „Triggers“ zum blauen Feld „geht“. FRPViewModel function“ und schließlich über „Produces (new state)“. "State" verbindet sich dann auch wieder mit "FRPViewModel function" über "Acts as input for".
Der funktionale reaktive Programmierzyklus in Android.

Erkunden von FRP-Bibliotheken und -Tools

Es gibt eine Vielzahl von Android-Bibliotheken und -Tools, die Ihnen beim Einstieg in FRP helfen können und die auch für die funktionale Programmierung relevant sind:

  • Ivy FRP : Dies ist eine von mir geschriebene Bibliothek, die in diesem Tutorial für Bildungszwecke verwendet wird. Es ist als Ausgangspunkt für Ihre Herangehensweise an FRP gedacht, ist jedoch nicht für die Verwendung in der Produktion vorgesehen, da es an angemessener Unterstützung mangelt. (Ich bin derzeit der einzige Techniker, der es wartet.)
  • Arrow : Dies ist eine der besten und beliebtesten Kotlin-Bibliotheken für FP, die wir auch in unserer Beispiel-App verwenden werden. Es bietet fast alles, was Sie brauchen, um in Kotlin funktionsfähig zu werden, während es relativ leicht bleibt.
  • Jetpack Compose : Dies ist das aktuelle Entwicklungs-Toolkit von Android zum Erstellen nativer Benutzeroberflächen und die dritte Bibliothek, die wir heute verwenden werden. Es ist für moderne Android-Entwickler unerlässlich – ich würde empfehlen, es zu lernen und sogar Ihre Benutzeroberfläche zu migrieren, falls Sie dies noch nicht getan haben.
  • Flow : Dies ist die asynchrone reaktive Datenstrom-API von Kotlin; Obwohl wir in diesem Tutorial nicht damit arbeiten, ist es mit vielen gängigen Android-Bibliotheken wie RoomDB, Retrofit und Jetpack kompatibel. Flow arbeitet nahtlos mit Coroutinen zusammen und bietet Reaktivität. In Verbindung mit RoomDB stellt Flow beispielsweise sicher, dass Ihre App immer mit den neuesten Daten arbeitet. Bei einer Änderung in einer Tabelle erhalten die von dieser Tabelle abhängigen Flüsse sofort den neuen Wert.
  • Kotest : Diese Testplattform bietet eigenschaftsbasierte Testunterstützung, die für reinen FP-Domänencode relevant ist.

Implementieren einer Beispiel-Fuß/Meter-Konvertierungs-App

Sehen wir uns ein Beispiel für FRP bei der Arbeit in einer Android-App an. Wir erstellen eine einfache App, die Werte zwischen Metern (m) und Fuß (ft) umrechnet.

Für die Zwecke dieses Tutorials behandle ich nur die Teile des Codes, die für das Verständnis von FRP wichtig sind und der Einfachheit halber von meiner vollständigen Konverter-Beispiel-App modifiziert wurden. Wenn Sie in Android Studio mitmachen möchten, erstellen Sie Ihr Projekt mit einer Jetpack Compose-Aktivität und installieren Sie Arrow und Ivy FRP. Sie benötigen eine minSdk Version von 28 oder höher und eine Sprachversion von Kotlin 1.6+.

Bundesland

Beginnen wir damit, den Status unserer App zu definieren.

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

Unsere Zustandsklasse ist ziemlich selbsterklärend:

  • conversion : Ein Typ, der beschreibt, was wir umrechnen – Fuß in Meter oder Meter in Fuß.
  • value : Der Float, den der Benutzer eingibt, den wir später konvertieren werden.
  • result : Ein optionales Ergebnis, das eine erfolgreiche Konvertierung darstellt.

Als nächstes müssen wir die Benutzereingabe als Ereignis behandeln.

Vorfall

Wir ConvEvent als versiegelte Klasse definiert, um die Benutzereingabe darzustellen:

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

Lassen Sie uns die Zwecke seiner Mitglieder untersuchen:

  • SetConversionType : Wählt aus, ob wir von Fuß in Meter oder von Meter in Fuß konvertieren.
  • SetValue : Legt die numerischen Werte fest, die für die Konvertierung verwendet werden.
  • Convert : Führt die Konvertierung des eingegebenen Werts unter Verwendung des Konvertierungstyps durch.

Jetzt werden wir mit unserem Ansichtsmodell fortfahren.

Die deklarative Pipeline: Event-Handler und Funktionskomposition

Das Ansichtsmodell enthält unseren Ereignishandler und den Code für die Funktionszusammensetzung (deklarative Pipeline):

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

Lassen Sie uns vor der Analyse der Implementierung einige Objekte aufschlüsseln, die für die Ivy FRP-Bibliothek spezifisch sind.

FRPViewModel<S,E> ist eine abstrakte Ansichtsmodellbasis, die die FRP-Architektur implementiert. In unserem Code müssen wir die folgenden Methoden implementieren:

  • val _state : Definiert den Anfangswert des Zustands (Ivy FRP verwendet Flow als reaktiven Datenstrom).
  • handleEvent(Event): suspend () -> S : Erzeugt den nächsten Zustand asynchron bei einem Event . Die zugrunde liegende Implementierung startet für jedes Ereignis eine neue Coroutine.
  • stateVal(): S : Gibt den aktuellen Zustand zurück.
  • updateState((S) -> S): S Aktualisiert den Zustand des ViewModel .

Schauen wir uns nun einige Methoden an, die sich auf die Funktionskomposition beziehen:

  • then : Setzt zwei Funktionen zusammen.
  • asParamTo : Erzeugt eine Funktion g() = f(t) aus f(T) und einem Wert t (vom Typ T ).
  • thenInvokeAfter : Setzt zwei Funktionen zusammen und ruft sie dann auf.

updateState und thenInvokeAfter sind Hilfsmethoden, die im nächsten Codeausschnitt gezeigt werden; Sie werden in unserem verbleibenden Ansichtsmodellcode verwendet.

Die deklarative Pipeline: Zusätzliche Funktionsimplementierungen

Unser Ansichtsmodell enthält auch Funktionsimplementierungen zum Festlegen unseres Konvertierungstyps und -werts, zum Durchführen der eigentlichen Konvertierungen und zum Formatieren unseres Endergebnisses:

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

Mit einem Verständnis unserer Ivy FRP-Hilfsfunktionen sind wir bereit, den Code zu analysieren. Beginnen wir mit der Kernfunktionalität: convert . convert akzeptiert den Zustand ( ConvState ) als Eingabe und erzeugt eine Funktion, die einen neuen Zustand ausgibt, der das Ergebnis der konvertierten Eingabe enthält. In Pseudocode können wir es wie folgt zusammenfassen: State (ConvState) -> Value (Float) -> Converted value (Float) -> Result (Option<String>) .

Die Ereignisbehandlung von Event.SetValue ist unkompliziert; es aktualisiert einfach den Zustand mit dem Wert aus dem Ereignis (dh der Benutzer gibt eine umzuwandelnde Zahl ein). Die Behandlung des Events Event.SetConversionType ist jedoch etwas interessanter, da es zwei Dinge tut:

  • Aktualisiert den Status mit dem ausgewählten Conversion-Typ ( ConvType ).
  • Verwendet convert , um den aktuellen Wert basierend auf dem ausgewählten Konvertierungstyp umzuwandeln.

Mithilfe der Kraft der Komposition können wir die Funktion convert: State -> State als Eingabe für andere Kompositionen verwenden. Sie haben vielleicht bemerkt, dass der oben gezeigte Code nicht rein ist: Wir mutieren protected abstract val _state: MutableStateFlow<S> in FRPViewModel , was zu Nebenwirkungen führt, wenn wir updateState {} verwenden. Ganz reiner FP-Code für Android in Kotlin ist nicht machbar.

Da das Zusammenstellen von Funktionen, die nicht rein sind, zu unvorhersehbaren Ergebnissen führen kann, ist ein hybrider Ansatz am praktischsten: Verwenden Sie größtenteils reine Funktionen und stellen Sie sicher, dass alle unreinen Funktionen kontrollierte Nebenwirkungen haben. Genau das haben wir oben getan.

Observable und UI

Unser letzter Schritt besteht darin, die Benutzeroberfläche unserer App zu definieren und unseren Konverter zum Leben zu erwecken.

Ein großes graues Rechteck mit vier Pfeilen, die von rechts darauf zeigen. Von oben nach unten zeigt der erste Pfeil mit der Bezeichnung „Schaltflächen“ auf zwei kleinere Rechtecke: ein dunkelblaues linkes Rechteck mit dem Text „Meter in Fuß“ in Großbuchstaben und ein hellblaues rechtes Rechteck mit dem Text „Fuß in Meter“. Der zweite Pfeil mit der Bezeichnung „TextField“ zeigt auf ein weißes Rechteck mit linksbündigem Text „100.0“. Der dritte Pfeil mit der Bezeichnung „Schaltfläche“ zeigt auf ein linksbündiges grünes Rechteck mit dem Text „Konvertieren“. Der letzte Pfeil mit der Bezeichnung „Text“ zeigt auf den linksbündigen blauen Text mit der Aufschrift „Ergebnis: 328,08 Fuß“.
Ein Modell der Benutzeroberfläche der App.

Die Benutzeroberfläche unserer App wird etwas „hässlich“ sein, aber das Ziel dieses Beispiels ist es, FRP zu demonstrieren, und nicht, ein schönes Design mit Jetpack Compose zu erstellen.

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

Unser UI-Code verwendet grundlegende Jetpack Compose-Prinzipien in möglichst wenigen Codezeilen. Es gibt jedoch eine erwähnenswerte interessante Funktion: FRP<ConvState, ConvEvent, ConverterViewModel> . FRP ist eine zusammensetzbare Funktion aus dem Ivy FRP-Framework, die mehrere Dinge tut:

  • Instanziiert das Ansichtsmodell mit @HiltViewModel .
  • Überwacht den State des Ansichtsmodells mithilfe von Flow.
  • Übermittelt Ereignisse an das ViewModel mit dem Code onEvent: (Event) -> Unit) .
  • Stellt eine @Composable -Funktion höherer Ordnung bereit, die die Ereignisweitergabe durchführt und den neuesten Status empfängt.
  • Bietet optional eine Möglichkeit zum Übergeben von initialEvent , das aufgerufen wird, sobald die App gestartet wird.

So wird die FRP -Funktion in der Ivy FRP-Bibliothek implementiert:

 @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) }

Den vollständigen Code des Konverterbeispiels finden Sie auf GitHub, und der gesamte UI-Code ist in der UI -Funktion der Datei ConverterScreen.kt zu finden. Wenn Sie mit der App oder dem Code experimentieren möchten, können Sie das Ivy FRP-Repository klonen und die sample -App in Android Studio ausführen. Ihr Emulator benötigt möglicherweise mehr Speicherplatz, bevor die App ausgeführt werden kann.

Sauberere Android-Architektur mit FRP

Mit einem starken grundlegenden Verständnis der funktionalen Programmierung, der reaktiven Programmierung und schließlich der funktionalen reaktiven Programmierung sind Sie bereit, die Vorteile von FRP zu nutzen und eine sauberere und besser wartbare Android-Architektur zu erstellen.

Der Toptal Engineering Blog dankt Tarun Goyal für die Überprüfung der in diesem Artikel vorgestellten Codebeispiele.