Защитите свой Android-код на будущее, часть 2: функциональное реактивное программирование в действии
Опубликовано: 2022-09-08Функциональное реактивное программирование (FRP) — это парадигма, сочетающая реактивность реактивного программирования с декларативной композицией функций функционального программирования. Он упрощает сложные задачи, создает элегантные пользовательские интерфейсы и плавно управляет состоянием. Благодаря этим и многим другим очевидным преимуществам использование FRP становится основным в мобильных и веб-разработках.
Это не означает, что понять эту парадигму программирования легко — даже опытные разработчики могут задаться вопросом: «Что такое FRP?» В части 1 этого руководства мы определили основополагающие концепции FRP: функциональное программирование и реактивное программирование. Этот выпуск подготовит вас к его применению, включая обзор полезных библиотек и подробный пример реализации.
Эта статья написана для разработчиков Android, но концепции актуальны и полезны для любого разработчика, имеющего опыт работы с общими языками программирования.
Начало работы с FRP: проектирование системы
Парадигма FRP представляет собой бесконечный цикл состояний и событий: State -> Event -> State' -> Event' -> State'' -> …
. (Напоминаем, '
, произносимое как «прайм», указывает на новую версию той же самой переменной.) Каждая программа FRP начинается с начального состояния, которое будет обновляться с каждым полученным событием. Эта программа включает в себя те же элементы, что и реактивная программа:
- Состояние
- Мероприятие
- Декларативный конвейер (обозначается как
FRPViewModel function
) - Наблюдаемый (обозначается как
StateFlow
)
Здесь мы заменили общие реактивные элементы реальными компонентами и библиотеками Android:
Изучение библиотек и инструментов FRP
Существует множество библиотек и инструментов для Android, которые помогут вам начать работу с FRP, а также имеют отношение к функциональному программированию:
- Ivy FRP : это написанная мной библиотека, которая будет использоваться в образовательных целях в этом руководстве. Он предназначен в качестве отправной точки для вашего подхода к FRP, но не предназначен для использования в производстве, поскольку ему не хватает надлежащей поддержки. (В настоящее время я единственный инженер, поддерживающий его.)
- Arrow : это одна из лучших и самых популярных библиотек Kotlin для FP, которую мы также будем использовать в нашем примере приложения. Он предоставляет почти все, что вам нужно для работы в Kotlin, оставаясь при этом относительно легким.
- Jetpack Compose : это текущий набор инструментов разработки Android для создания собственного пользовательского интерфейса и третья библиотека, которую мы будем использовать сегодня. Это важно для современных разработчиков Android — я бы рекомендовал изучить его и даже перенести свой пользовательский интерфейс, если вы еще этого не сделали.
- Flow : это API асинхронного реактивного потока данных Kotlin; хотя мы не работаем с ним в этом руководстве, он совместим со многими распространенными библиотеками Android, такими как RoomDB, Retrofit и Jetpack. Flow без проблем работает с сопрограммами и обеспечивает реактивность. Например, при использовании с RoomDB Flow гарантирует, что ваше приложение всегда будет работать с самыми последними данными. Если произойдет изменение в таблице, потоки, зависящие от этой таблицы, немедленно получат новое значение.
- Kotest : Эта тестовая платформа предлагает поддержку тестирования на основе свойств, относящуюся к чистому коду предметной области FP.
Реализация примера приложения для преобразования футов в метры
Давайте посмотрим на пример FRP в приложении для Android. Мы создадим простое приложение, которое преобразует значения между метрами (м) и футами (ft).
Для целей этого руководства я описываю только части кода, жизненно важные для понимания FRP, измененные для простоты из моего примера приложения полного конвертера. Если вы хотите продолжить в Android Studio, создайте свой проект с действием Jetpack Compose и установите Arrow и Ivy FRP. Вам понадобится minSdk
версии 28 или выше и языковая версия Kotlin 1.6+.
Состояние
Начнем с определения состояния нашего приложения.
// ConvState.kt enum class ConvType { METERS_TO_FEET, FEET_TO_METERS } data class ConvState( val conversion: ConvType, val value: Float, val result: Option<String> )
Наш класс состояний говорит сам за себя:
-
conversion
: Тип, описывающий, что мы конвертируем между футами в метры или метрами в футы. -
value
: число с плавающей запятой, которое вводит пользователь, которое мы преобразуем позже. -
result
: необязательный результат, представляющий успешное преобразование.
Далее нам нужно обработать пользовательский ввод как событие.
Мероприятие
Мы определили ConvEvent
как закрытый класс для представления пользовательского ввода:
// ConvEvent.kt sealed class ConvEvent { data class SetConversionType(val conversion: ConvType) : ConvEvent() data class SetValue(val value: Float) : ConvEvent() object Convert : ConvEvent() }
Давайте рассмотрим цели его членов:
-
SetConversionType
: выбирает, конвертируем ли мы из футов в метры или из метров в футы. -
SetValue
: устанавливает числовые значения, которые будут использоваться для преобразования. -
Convert
: выполняет преобразование введенного значения с использованием типа преобразования.
Теперь мы продолжим работу с нашей моделью представления.
Декларативный конвейер: обработчик событий и композиция функций
Модель представления содержит код нашего обработчика событий и композиции функций (декларативный конвейер):
// 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 } // ... }
Прежде чем анализировать реализацию, давайте разберем несколько объектов, характерных для библиотеки Ivy FRP.
FRPViewModel<S,E>
— это базовая модель абстрактного представления, которая реализует архитектуру FRP. В нашем коде нам нужно реализовать следующие методы:
-
val _state
: определяет начальное значение состояния (Ivy FRP использует Flow в качестве реактивного потока данных). -
handleEvent(Event): suspend () -> S
: асинхронно создает следующее состояние с учетомEvent
. Базовая реализация запускает новую сопрограмму для каждого события. -
stateVal(): S
: возвращает текущее состояние. -
updateState((S) -> S): S
Обновляет состояниеViewModel
.
Теперь давайте рассмотрим несколько методов, связанных с композицией функций:
-
then
: объединяет две функции. -
asParamTo
: создает функциюg() = f(t)
изf(T)
и значениеt
(типаT
). -
thenInvokeAfter
: объединяет две функции, а затем вызывает их.
updateState
и thenInvokeAfter
— вспомогательные методы, показанные в следующем фрагменте кода; они будут использоваться в нашем оставшемся коде модели представления.
Декларативный конвейер: дополнительные реализации функций
Наша модель представления также содержит реализации функций для установки типа и значения преобразования, выполнения фактических преобразований и форматирования конечного результата:
// 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) }
Поняв вспомогательные функции Ivy FRP, мы готовы анализировать код. Начнем с основной функциональности: convert
. convert
принимает состояние ( ConvState
) в качестве входных данных и создает функцию, которая выводит новое состояние, содержащее результат преобразованного ввода. В псевдокоде мы можем резюмировать это как: State (ConvState) -> Value (Float) -> Converted value (Float) -> Result (Option<String>)
.
Обработка события Event.SetValue
проста; он просто обновляет состояние значением из события (т. е. пользователь вводит число для преобразования). Однако обработка события Event.SetConversionType
немного интереснее, поскольку она выполняет две функции:
- Обновляет состояние выбранным типом преобразования (
ConvType
). - Использует
convert
для преобразования текущего значения на основе выбранного типа преобразования.
Используя возможности композиции, мы можем использовать функцию convert: State -> State
в качестве входных данных для других композиций. Вы могли заметить, что приведенный выше код не является чистым: мы protected abstract val _state: MutableStateFlow<S>
в FRPViewModel
, что приводит к побочным эффектам всякий раз, когда мы используем updateState {}
. Полностью чистый код FP для Android на Kotlin невозможен.
Поскольку составление нечистых функций может привести к непредсказуемым результатам, гибридный подход является наиболее практичным: по большей части используйте чистые функции и убедитесь, что любые нечистые функции имеют контролируемые побочные эффекты. Это именно то, что мы сделали выше.
Наблюдаемый и пользовательский интерфейс
Наш последний шаг — определить пользовательский интерфейс нашего приложения и воплотить в жизнь наш конвертер.
Пользовательский интерфейс нашего приложения будет немного «уродливым», но целью этого примера является демонстрация FRP, а не создание красивого дизайна с помощью Jetpack Compose.
// ConverterScreen.kt @Composable fun BoxWithConstraintsScope.ConverterScreen(screen: ConverterScreen) { FRP<ConvState, ConvEvent, ConverterViewModel> { state, onEvent -> UI(state, onEvent) } }
Наш код пользовательского интерфейса использует базовые принципы Jetpack Compose в наименьшем количестве возможных строк кода. Однако стоит упомянуть одну интересную функцию: FRP<ConvState, ConvEvent, ConverterViewModel>
. FRP
— это компонуемая функция из фреймворка Ivy FRP, которая делает несколько вещей:
- Создает экземпляр модели представления с помощью
@HiltViewModel
. - Наблюдает за
State
модели представления, используя Flow. - Распространяет события на
ViewModel
с помощью кодаonEvent: (Event) -> Unit)
. - Предоставляет функцию высшего порядка
@Composable
, которая выполняет распространение событий и получает последнее состояние. - При необходимости предоставляет способ передать
initialEvent
, который вызывается после запуска приложения.
Вот как реализована функция FRP
в библиотеке 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) }
Вы можете найти полный код примера конвертера на GitHub, а весь код пользовательского интерфейса можно найти в функции UI
файла ConverterScreen.kt
. Если вы хотите поэкспериментировать с приложением или кодом, вы можете клонировать репозиторий Ivy FRP и запустить sample
приложения в Android Studio. Вашему эмулятору может потребоваться дополнительное хранилище, прежде чем приложение сможет работать.
Более чистая архитектура Android с FRP
Обладая глубоким базовым пониманием функционального программирования, реактивного программирования и, наконец, функционального реактивного программирования, вы готовы воспользоваться преимуществами FRP и построить более чистую и удобную в сопровождении архитектуру Android.
Блог Toptal Engineering выражает благодарность Таруну Гоялу за рассмотрение примеров кода, представленных в этой статье.