Защитите свой Android-код на будущее, часть 2: функциональное реактивное программирование в действии

Опубликовано: 2022-09-08

Функциональное реактивное программирование (FRP) — это парадигма, сочетающая реактивность реактивного программирования с декларативной композицией функций функционального программирования. Он упрощает сложные задачи, создает элегантные пользовательские интерфейсы и плавно управляет состоянием. Благодаря этим и многим другим очевидным преимуществам использование FRP становится основным в мобильных и веб-разработках.

Это не означает, что понять эту парадигму программирования легко — даже опытные разработчики могут задаться вопросом: «Что такое FRP?» В части 1 этого руководства мы определили основополагающие концепции FRP: функциональное программирование и реактивное программирование. Этот выпуск подготовит вас к его применению, включая обзор полезных библиотек и подробный пример реализации.

Эта статья написана для разработчиков Android, но концепции актуальны и полезны для любого разработчика, имеющего опыт работы с общими языками программирования.

Начало работы с FRP: проектирование системы

Парадигма FRP представляет собой бесконечный цикл состояний и событий: State -> Event -> State' -> Event' -> State'' -> … . (Напоминаем, ' , произносимое как «прайм», указывает на новую версию той же самой переменной.) Каждая программа FRP начинается с начального состояния, которое будет обновляться с каждым полученным событием. Эта программа включает в себя те же элементы, что и реактивная программа:

  • Состояние
  • Мероприятие
  • Декларативный конвейер (обозначается как FRPViewModel function )
  • Наблюдаемый (обозначается как StateFlow )

Здесь мы заменили общие реактивные элементы реальными компонентами и библиотеками Android:

Два основных синих блока, «StateFlow» и «State», имеют два основных пути между собой. Первый — через «Наблюдает (прислушивается к изменениям)». Второй — через «Уведомляет (о последнем состоянии)» в синее поле «@Composable (JetpackCompose)», которое переходит через «Преобразует пользовательский ввод в» в синее поле «Событие», которое переходит через «Триггеры» в синее поле « FRPViewModel» и, наконец, через «Производит (новое состояние)». «Состояние» затем также подключается обратно к «функции FRPViewModel» через «Действует как вход для».
Цикл функционального реактивного программирования в 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 невозможен.

Поскольку составление нечистых функций может привести к непредсказуемым результатам, гибридный подход является наиболее практичным: по большей части используйте чистые функции и убедитесь, что любые нечистые функции имеют контролируемые побочные эффекты. Это именно то, что мы сделали выше.

Наблюдаемый и пользовательский интерфейс

Наш последний шаг — определить пользовательский интерфейс нашего приложения и воплотить в жизнь наш конвертер.

Большой серый прямоугольник с четырьмя стрелками, указывающими на него справа. Сверху вниз первая стрелка с надписью «Кнопки» указывает на два меньших прямоугольника: темно-синий левый прямоугольник с прописным текстом «Метры в футы» и светло-синий правый прямоугольник с текстом «Футы в метры». Вторая стрелка с надписью «TextField» указывает на белый прямоугольник с выровненным по левому краю текстом «100.0». Третья стрелка с надписью «Кнопка» указывает на выровненный по левому краю зеленый прямоугольник с текстом «Преобразовать». Последняя стрелка с надписью «Текст» указывает на выровненный по левому краю синий текст: «Результат: 328,08 фута».
Макет пользовательского интерфейса приложения.

Пользовательский интерфейс нашего приложения будет немного «уродливым», но целью этого примера является демонстрация 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 выражает благодарность Таруну Гоялу за рассмотрение примеров кода, представленных в этой статье.