Protejează viitorul codul tău Android, partea 2: Programarea reactivă funcțională în acțiune
Publicat: 2022-09-08Programarea funcțională reactivă (FRP) este o paradigmă care combină reactivitatea din programarea reactivă cu compoziția declarativă a funcției din programarea funcțională. Simplifică sarcinile complexe, creează interfețe de utilizator elegante și gestionează starea fără probleme. Datorită acestor beneficii și a multor alte beneficii clare, utilizarea FRP devine generală în dezvoltarea mobilă și web.
Asta nu înseamnă că înțelegerea acestei paradigme de programare este ușoară – chiar și dezvoltatorii experimentați se pot întreba: „Ce este exact FRP?” În partea 1 a acestui tutorial, am definit conceptele fundamentale ale FRP: programarea funcțională și programarea reactivă. Această tranșă vă va pregăti să îl aplicați, cu o prezentare generală a bibliotecilor utile și un exemplu de implementare detaliat.
Acest articol este scris având în vedere dezvoltatorii Android, dar conceptele sunt relevante și benefice pentru orice dezvoltator cu experiență în limbaje de programare generale.
Noțiuni introductive cu FRP: Design de sistem
Paradigma FRP este un ciclu nesfârșit de stări și evenimente: State -> Event -> State' -> Event' -> State'' -> …
. (Pentru reamintire, '
, pronunțat „prim”, indică o nouă versiune a aceleiași variabile.) Fiecare program FRP începe cu o stare inițială care va fi actualizată cu fiecare eveniment pe care îl primește. Acest program include aceleași elemente ca cele dintr-un program reactiv:
- Stat
- Eveniment
- Conducta declarativă (indicată ca
FRPViewModel function
) - Observabil (indicat ca
StateFlow
)
Aici, am înlocuit elementele reactive generale cu componente și biblioteci Android reale:
Explorarea bibliotecilor și instrumentelor FRP
Există o varietate de biblioteci și instrumente Android care vă pot ajuta să începeți cu FRP și care sunt, de asemenea, relevante pentru programarea funcțională:
- Ivy FRP : Aceasta este o bibliotecă pe care am scris-o și care va fi folosită în scopuri educaționale în acest tutorial. Este menit ca un punct de plecare pentru abordarea dvs. de FRP, dar nu este destinat utilizării în producție așa cum este, deoarece nu are suport adecvat. (În prezent sunt singurul inginer care îl întreține.)
- Săgeată : Aceasta este una dintre cele mai bune și mai populare biblioteci Kotlin pentru FP, una pe care o vom folosi și în aplicația noastră exemplu. Oferă aproape tot ce aveți nevoie pentru a deveni funcțional în Kotlin, rămânând în același timp relativ ușor.
- Jetpack Compose : Acesta este setul de instrumente de dezvoltare actual al Android pentru construirea unei interfețe de utilizare native și este a treia bibliotecă pe care o vom folosi astăzi. Este esențial pentru dezvoltatorii moderni de Android — aș recomanda să-l învețe și chiar să migreze interfața de utilizare dacă nu ați făcut-o deja.
- Flux : Acesta este API-ul de flux de date reactiv asincron al Kotlin; deși nu lucrăm cu el în acest tutorial, este compatibil cu multe biblioteci Android comune, cum ar fi RoomDB, Retrofit și Jetpack. Flow funcționează perfect cu corutine și oferă reactivitate. Când este utilizat cu RoomDB, de exemplu, Flow se asigură că aplicația dvs. va funcționa întotdeauna cu cele mai recente date. Dacă apare o modificare a unui tabel, fluxurile dependente de acest tabel vor primi imediat noua valoare.
- Kotest : Această platformă de testare oferă suport de testare bazat pe proprietăți relevant pentru codul de domeniu FP pur.
Implementarea unui exemplu de aplicație de conversie picioare/metri
Să vedem un exemplu de FRP la lucru într-o aplicație Android. Vom crea o aplicație simplă care convertește valorile între metri (m) și picioare (ft).
În scopul acestui tutorial, acopăr doar porțiunile de cod esențiale pentru înțelegerea FRP, modificate de dragul simplității din aplicația mea de exemplu de convertor complet. Dacă doriți să urmăriți în Android Studio, creați-vă proiectul cu o activitate Jetpack Compose și instalați Arrow și Ivy FRP. Veți avea nevoie de o versiune minSdk
de 28 sau mai mare și de o versiune lingvistică a Kotlin 1.6+.
Stat
Să începem prin a defini starea aplicației noastre.
// ConvState.kt enum class ConvType { METERS_TO_FEET, FEET_TO_METERS } data class ConvState( val conversion: ConvType, val value: Float, val result: Option<String> )
Clasa noastră de stat este destul de explicită:
-
conversion
: un tip care descrie între ce facem conversia — picioare în metri sau metri în picioare. -
value
: float-ul introdus de utilizator, pe care îl vom converti mai târziu. -
result
: un rezultat opțional care reprezintă o conversie reușită.
În continuare, trebuie să gestionăm intrarea utilizatorului ca un eveniment.
Eveniment
Am definit ConvEvent
ca o clasă sigilată pentru a reprezenta intrarea utilizatorului:
// ConvEvent.kt sealed class ConvEvent { data class SetConversionType(val conversion: ConvType) : ConvEvent() data class SetValue(val value: Float) : ConvEvent() object Convert : ConvEvent() }
Să examinăm scopurile membrilor săi:
-
SetConversionType
: Alege dacă facem conversia de la picioare la metri sau de la metri la picioare. -
SetValue
: Setează valorile numerice, care vor fi folosite pentru conversie. -
Convert
: Efectuează conversia valorii introduse utilizând tipul de conversie.
Acum, vom continua cu modelul nostru de vedere.
Conducta declarativă: gestionarea evenimentelor și compoziția funcției
Modelul de vizualizare conține codul nostru de gestionare a evenimentelor și compoziția funcției (conducta declarativă):
// 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 } // ... }
Înainte de a analiza implementarea, să defalcăm câteva obiecte specifice bibliotecii Ivy FRP.
FRPViewModel<S,E>
este o bază de model de vizualizare abstractă care implementează arhitectura FRP. În codul nostru, trebuie să implementăm următoarele metode:
-
val _state
: Definește valoarea inițială a stării (Ivy FRP utilizează Flow ca flux de date reactiv). -
handleEvent(Event): suspend () -> S
: Produce următoarea stare asincronă având în vedere unEvent
. Implementarea de bază lansează o nouă corutine pentru fiecare eveniment. -
stateVal(): S
: Returnează starea curentă. -
updateState((S) -> S): S
Actualizează stareaViewModel
.
Acum, să ne uităm la câteva metode legate de compoziția funcției:
-
then
: Compune două funcții împreună. -
asParamTo
: Produce o funcțieg() = f(t)
dinf(T)
și o valoaret
(de tipT
). -
thenInvokeAfter
: Compune două funcții și apoi le invocă.
updateState
și thenInvokeAfter
sunt metode de ajutor afișate în următorul fragment de cod; acestea vor fi utilizate în codul de model de vizualizare rămas.
Conducta declarativă: implementări suplimentare de funcții
Modelul nostru de vizualizare conține, de asemenea, implementări de funcții pentru setarea tipului și valorii noastre de conversie, efectuarea conversiilor reale și formatarea rezultatului 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) }
Cu o înțelegere a funcțiilor noastre de ajutor Ivy FRP, suntem gata să analizăm codul. Să începem cu funcționalitatea de bază: convert
. convert
acceptă starea ( ConvState
) ca intrare și produce o funcție care scoate o stare nouă care conține rezultatul intrării convertite. În pseudocod, îl putem rezuma ca: State (ConvState) -> Value (Float) -> Converted value (Float) -> Result (Option<String>)
.
Gestionarea evenimentului Event.SetValue
este simplă; pur și simplu actualizează starea cu valoarea din eveniment (adică, utilizatorul introduce un număr pentru a fi convertit). Cu toate acestea, gestionarea evenimentului Event.SetConversionType
este puțin mai interesantă, deoarece face două lucruri:
- Actualizează starea cu tipul de conversie selectat (
ConvType
). - Utilizează
convert
pentru a converti valoarea curentă pe baza tipului de conversie selectat.
Folosind puterea compoziției, putem folosi funcția de convert: State -> State
ca intrare pentru alte compoziții. Este posibil să fi observat că codul demonstrat mai sus nu este pur: facem mutații protected abstract val _state: MutableStateFlow<S>
în FRPViewModel
, rezultând efecte secundare ori de câte ori folosim updateState {}
. Codul FP complet pur pentru Android în Kotlin nu este fezabil.
Deoarece compunerea funcțiilor care nu sunt pure poate duce la rezultate imprevizibile, o abordare hibridă este cea mai practică: utilizați funcții pure în cea mai mare parte și asigurați-vă că orice funcții impure au efecte secundare controlate. Este exact ceea ce am făcut mai sus.
Observabil și UI
Pasul nostru final este să definim interfața de utilizare a aplicației noastre și să aducem convertorul nostru la viață.
Interfața de utilizare a aplicației noastre va fi puțin „urâtă”, dar scopul acestui exemplu este de a demonstra FRP, nu de a construi un design frumos folosind Jetpack Compose.
// ConverterScreen.kt @Composable fun BoxWithConstraintsScope.ConverterScreen(screen: ConverterScreen) { FRP<ConvState, ConvEvent, ConverterViewModel> { state, onEvent -> UI(state, onEvent) } }
Codul nostru UI folosește principiile de bază Jetpack Compose în cele mai puține linii de cod posibile. Cu toate acestea, există o funcție interesantă care merită menționată: FRP<ConvState, ConvEvent, ConverterViewModel>
. FRP
este o funcție componabilă din cadrul Ivy FRP, care face mai multe lucruri:
- Instanțiază modelul de vizualizare folosind
@HiltViewModel
. - Observă
State
modelului de vizualizare folosind Flow. - Propagează evenimentele către
ViewModel
cu codulonEvent: (Event) -> Unit)
. - Oferă o funcție de ordin superior
@Composable
care realizează propagarea evenimentelor și primește cea mai recentă stare. - Opțional, oferă o modalitate de a trece
initialEvent
, care este apelat odată ce aplicația pornește.
Iată cum este implementată funcția FRP
în 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) }
Puteți găsi codul complet al exemplului de convertizor în GitHub, iar întregul cod UI poate fi găsit în funcția UI
a fișierului ConverterScreen.kt
. Dacă doriți să experimentați cu aplicația sau codul, puteți să clonați depozitul Ivy FRP și să rulați aplicația sample
în Android Studio. Emulatorul dvs. poate avea nevoie de spațiu de stocare sporit înainte ca aplicația să poată rula.
Arhitectură Android mai curată cu FRP
Cu o înțelegere fundamentală puternică a programării funcționale, a programării reactive și, în sfârșit, a programării funcționale reactive, sunteți gata să culegeți beneficiile FRP și să construiți o arhitectură Android mai curată și mai ușor de întreținut.
Blogul Toptal Engineering își exprimă recunoștința lui Tarun Goyal pentru revizuirea mostrelor de cod prezentate în acest articol.