Prepare seu código Android para o futuro, parte 2: programação reativa funcional em ação
Publicados: 2022-09-08A programação reativa funcional (FRP) é um paradigma que combina a reatividade da programação reativa com a composição da função declarativa da programação funcional. Ele simplifica tarefas complexas, cria interfaces de usuário elegantes e gerencia o estado sem problemas. Devido a esses e muitos outros benefícios claros, o uso do FRP está se tornando popular no desenvolvimento móvel e web.
Isso não significa que entender esse paradigma de programação é fácil – até mesmo desenvolvedores experientes podem se perguntar: “O que exatamente é FRP?” Na Parte 1 deste tutorial, definimos os conceitos fundamentais do FRP: programação funcional e programação reativa. Esta parte irá prepará-lo para aplicá-lo, com uma visão geral de bibliotecas úteis e uma implementação de exemplo detalhada.
Este artigo foi escrito com os desenvolvedores Android em mente, mas os conceitos são relevantes e benéficos para qualquer desenvolvedor com experiência em linguagens de programação gerais.
Introdução ao FRP: design do sistema
O paradigma FRP é um ciclo infinito de estados e eventos: State -> Event -> State' -> Event' -> State'' -> …
. (Como lembrete, '
, pronunciado “prime”, indica uma nova versão da mesma variável.) Todo programa FRP começa com um estado inicial que será atualizado a cada evento que receber. Este programa inclui os mesmos elementos de um programa reativo:
- Estado
- Evento
- O pipeline declarativo (indicado como
FRPViewModel function
) - Observável (indicado como
StateFlow
)
Aqui, substituímos os elementos reativos gerais por componentes e bibliotecas reais do Android:
Explorando bibliotecas e ferramentas de FRP
Há uma variedade de bibliotecas e ferramentas do Android que podem ajudá-lo a começar com o FRP e que também são relevantes para a programação funcional:
- Ivy FRP : Esta é uma biblioteca que escrevi que será usada para fins educacionais neste tutorial. Ele serve como ponto de partida para sua abordagem ao FRP, mas não se destina ao uso em produção como está, pois não possui suporte adequado. (Atualmente, sou o único engenheiro a mantê-lo.)
- Arrow : Esta é uma das melhores e mais populares bibliotecas Kotlin para FP, uma que também usaremos em nosso aplicativo de exemplo. Ele fornece quase tudo o que você precisa para funcionar em Kotlin, mantendo-se relativamente leve.
- Jetpack Compose : Este é o kit de ferramentas de desenvolvimento atual do Android para criar UI nativa e é a terceira biblioteca que usaremos hoje. É essencial para desenvolvedores Android modernos - eu recomendo aprender e até mesmo migrar sua interface do usuário, se você ainda não o fez.
- Flow : Esta é a API de fluxo de dados reativo assíncrono do Kotlin; embora não estejamos trabalhando com ele neste tutorial, ele é compatível com muitas bibliotecas comuns do Android, como RoomDB, Retrofit e Jetpack. O Flow funciona perfeitamente com corrotinas e fornece reatividade. Quando usado com o RoomDB, por exemplo, o Flow garante que seu aplicativo sempre funcionará com os dados mais recentes. Se ocorrer uma alteração em uma tabela, os fluxos dependentes dessa tabela receberão o novo valor imediatamente.
- Kotest : Esta plataforma de teste oferece suporte de teste baseado em propriedade relevante para o código de domínio FP puro.
Implementando um aplicativo de conversão de pés/metros de amostra
Vamos ver um exemplo de FRP funcionando em um aplicativo Android. Vamos criar um aplicativo simples que converte valores entre metros (m) e pés (ft).
Para os propósitos deste tutorial, estou cobrindo apenas as partes do código vitais para entender o FRP, modificadas para simplificar do meu aplicativo de amostra de conversor completo. Se você quiser acompanhar no Android Studio, crie seu projeto com uma atividade Jetpack Compose e instale Arrow e Ivy FRP. Você precisará de uma versão minSdk
de 28 ou superior e uma versão de idioma do Kotlin 1.6+.
Estado
Vamos começar definindo o estado do nosso aplicativo.
// ConvState.kt enum class ConvType { METERS_TO_FEET, FEET_TO_METERS } data class ConvState( val conversion: ConvType, val value: Float, val result: Option<String> )
Nossa classe de estado é bastante autoexplicativa:
-
conversion
: Um tipo que descreve entre o que estamos convertendo - pés para metros ou metros para pés. -
value
: O float que o usuário insere, que converteremos mais tarde. -
result
: um resultado opcional que representa uma conversão bem-sucedida.
Em seguida, precisamos tratar a entrada do usuário como um evento.
Evento
Definimos ConvEvent
como uma classe selada para representar a entrada do usuário:
// ConvEvent.kt sealed class ConvEvent { data class SetConversionType(val conversion: ConvType) : ConvEvent() data class SetValue(val value: Float) : ConvEvent() object Convert : ConvEvent() }
Vamos examinar os propósitos de seus membros:
-
SetConversionType
: Escolhe se estamos convertendo de pés para metros ou de metros para pés. -
SetValue
: Define os valores numéricos, que serão usados para a conversão. -
Convert
: Realiza a conversão do valor inserido utilizando o tipo de conversão.
Agora, continuaremos com nosso modelo de exibição.
O pipeline declarativo: manipulador de eventos e composição de funções
O modelo de exibição contém nosso manipulador de eventos e código de composição de função (pipeline declarativo):
// 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 analisar a implementação, vamos detalhar alguns objetos específicos da biblioteca Ivy FRP.
FRPViewModel<S,E>
é uma base de modelo de visão abstrata que implementa a arquitetura FRP. Em nosso código, precisamos implementar os seguintes métodos:
-
val _state
: Define o valor inicial do estado (Ivy FRP está usando Flow como um fluxo de dados reativo). -
handleEvent(Event): suspend () -> S
: Produz o próximo estado assincronamente dado umEvent
. A implementação subjacente lança uma nova corrotina para cada evento. -
stateVal(): S
: Retorna o estado atual. -
updateState((S) -> S): S
Atualiza o estado doViewModel
.
Agora, vamos ver alguns métodos relacionados à composição de funções:
-
then
: Compõe duas funções juntas. -
asParamTo
: Produz uma funçãog() = f(t)
def(T)
e um valort
(do tipoT
). -
thenInvokeAfter
: Compõe duas funções e as invoca.
updateState
e thenInvokeAfter
são métodos auxiliares mostrados no próximo trecho de código; eles serão usados em nosso código de modelo de exibição restante.
O pipeline declarativo: implementações de funções adicionais
Nosso modelo de visualização também contém implementações de funções para definir nosso tipo e valor de conversão, realizar as conversões reais e formatar nosso 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) }
Com uma compreensão de nossas funções auxiliares Ivy FRP, estamos prontos para analisar o código. Vamos começar com a funcionalidade principal: convert
. convert
aceita o estado ( ConvState
) como entrada e produz uma função que gera um novo estado contendo o resultado da entrada convertida. Em pseudocódigo, podemos resumi-lo como: State (ConvState) -> Value (Float) -> Converted value (Float) -> Result (Option<String>)
.
A manipulação de eventos Event.SetValue
é direta; ele simplesmente atualiza o estado com o valor do evento (ou seja, o usuário insere um número a ser convertido). No entanto, manipular o evento Event.SetConversionType
é um pouco mais interessante porque faz duas coisas:
- Atualiza o estado com o tipo de conversão selecionado (
ConvType
). - Usa
convert
para converter o valor atual com base no tipo de conversão selecionado.
Usando o poder da composição, podemos usar a função convert: State -> State
como entrada para outras composições. Você deve ter notado que o código demonstrado acima não é puro: Estamos mudando protected abstract val _state: MutableStateFlow<S>
em FRPViewModel
, resultando em efeitos colaterais sempre que usamos updateState {}
. Código FP completamente puro para Android em Kotlin não é viável.
Como a composição de funções que não são puras pode levar a resultados imprevisíveis, uma abordagem híbrida é a mais prática: use funções puras na maioria das vezes e certifique-se de que quaisquer funções impuras tenham efeitos colaterais controlados. Isso é exatamente o que fizemos acima.
Observável e IU
Nossa etapa final é definir a interface do usuário do nosso aplicativo e dar vida ao nosso conversor.
A interface do nosso aplicativo será um pouco “feia”, mas o objetivo deste exemplo é demonstrar o FRP, não criar um design bonito usando o Jetpack Compose.
// ConverterScreen.kt @Composable fun BoxWithConstraintsScope.ConverterScreen(screen: ConverterScreen) { FRP<ConvState, ConvEvent, ConverterViewModel> { state, onEvent -> UI(state, onEvent) } }
Nosso código de interface do usuário usa os princípios básicos do Jetpack Compose no menor número de linhas de código possível. No entanto, há uma função interessante que vale a pena mencionar: FRP<ConvState, ConvEvent, ConverterViewModel>
. FRP
é uma função combinável da estrutura Ivy FRP, que faz várias coisas:
- Instancia o modelo de exibição usando
@HiltViewModel
. - Observa o
State
do modelo de exibição usando Flow. - Propaga eventos para o
ViewModel
com o códigoonEvent: (Event) -> Unit)
. - Fornece uma função de ordem superior
@Composable
que executa a propagação de eventos e recebe o estado mais recente. - Opcionalmente, fornece uma maneira de passar
initialEvent
, que é chamado assim que o aplicativo é iniciado.
Veja como a função FRP
é implementada na 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) }
Você pode encontrar o código completo do exemplo do conversor no GitHub, e todo o código da interface do usuário pode ser encontrado na função de UI
do usuário do arquivo ConverterScreen.kt
. Se quiser experimentar o aplicativo ou o código, você pode clonar o repositório Ivy FRP e executar o aplicativo de sample
no Android Studio. Seu emulador pode precisar de maior armazenamento antes que o aplicativo possa ser executado.
Arquitetura Android mais limpa com FRP
Com um forte entendimento básico de programação funcional, programação reativa e, finalmente, programação reativa funcional, você está pronto para colher os benefícios do FRP e criar uma arquitetura Android mais limpa e sustentável.
O Toptal Engineering Blog agradece a Tarun Goyal por revisar os exemplos de código apresentados neste artigo.