作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Iliyan Germanov的头像

Iliyan Germanov

伊利扬是一名Android开发者和首席技术官,他创立了四家初创公司,开发了几款顶级应用, including Ivy Wallet, 它获得了10个YouTube技术社区“最佳UI/UX”奖. 他擅长函数式编程、UX、Kotlin和Haskell.

Previously At

Reddit
Share

函数式响应式编程(FRP)是一种范式,它将响应式编程中的反应性与来自 函数式编程. 它简化了复杂的任务,创建了优雅的用户界面,并平滑地管理状态. Due to these and many other clear benefits在移动和web开发中,FRP的使用将成为主流.

这并不意味着理解这种编程范式很容易——甚至经验丰富的开发人员可能会想:“究竟是什么 is FRP?” In Part 1 of this tutorial, 我们定义了FRP的基本概念:函数式编程和响应式编程. 本文将为您应用它做准备, 提供有用库的概述和详细的示例实现.

这篇文章是由 Android developers in mind, 但是,这些概念对任何具有通用编程语言经验的开发人员都是相关且有益的.

开始与FRP:系统设计

FRP范式是状态和事件的无限循环: State -> Event -> State' -> Event' -> State'' -> …. (As a reminder, ',发音为“prime”,表示同一变量的新版本.)每个FRP程序都以初始状态开始,该状态将随着它接收到的每个事件而更新. 该程序包含与a中相同的元素 reactive program:

  • State
  • Event
  • 声明性管道(表示为 FRPViewModel函数)
  • 可观察(表示为 StateFlow)

这里,我们用实元素代替了一般的反应式元素 Android components and libraries:

两个主要的蓝色框,“StateFlow”和“State”,在它们之间有两条主要路径. 第一种是通过“观察”(监听变化).第二种是通过“通知(最新状态)”,@Composable (JetpackCompose),,它通过“将用户输入转换为蓝框”事件,,它通过“触发器”到蓝框“FRPViewModel函数”,,最后通过“生成(新状态)”.“状态”然后也连接回“FRPViewModel函数”通过“作为输入的行为。."
Android中的函数式响应式编程周期.

探索FRP库和工具

有多种 Android libraries 以及可以帮助您开始使用FRP的工具, 这也与函数式编程有关:

  • Ivy FRP这是我编写的一个库,将在本教程中用于教育目的. 它是作为你的FRP方法的起点,但不打算用于生产使用,因为它缺乏适当的支持. (我是目前唯一维护它的工程师.)
  • Arrow这是最好也是最受欢迎的一种 Kotlin 我们也将在我们的示例应用程序中使用它. 它提供了在Kotlin中实现功能所需的几乎所有内容,同时保持相对轻量级.
  • Jetpack Compose这是Android当前用于构建原生UI的开发工具包,也是我们今天要使用的第三个库. 对于现代Android开发人员来说,它是必不可少的——我建议您学习它,如果您还没有学习它,甚至可以迁移您的UI.
  • Flow: This is Kotlin’s asynchronous reactive datastream API; althought we’re not working with it in this tutorial, 它与许多常见的Android库兼容,比如RoomDB, Retrofit, and Jetpack. 流与协同程序无缝地工作,并提供反应性. 例如,当与RoomDB一起使用时,Flow可确保您的应用程序始终使用最新数据. 如果表中发生了更改, 依赖于此表的流将立即接收到新值.
  • Kotest这个测试平台提供了与纯FP领域代码相关的基于属性的测试支持.

实现一个示例英尺/米转换应用程序

让我们看一个FRP在an中的应用 Android app. 我们将创建一个简单的应用程序来转换米(m)和英尺(ft)之间的值。.

为本教程的目的, 我只介绍了对理解FRP至关重要的部分代码, 修改为简单起见,从我的完整的转换器样本应用程序. 如果你想在Android Studio中学习, 使用Jetpack Compose活动创建项目, and install Arrow* and Ivy FRP. You will need a minSdk 28或更高版本以及Kotlin 1的语言版本.6+.

* 免责声明:现在有了 新版绿箭侠 但是本教程的其余部分还没有针对它进行测试.

State

让我们从定义应用的状态开始.

// ConvState.kt
enum class ConvType {
	METERS_TO_FEET, FEET_TO_METERS
}

data class ConvState(
    val转换:ConvType,
    val value: Float,
    val result: Option
)

我们的状态类是不言自明的:

  • conversion:一种描述我们在英尺到米或米到英尺之间进行转换的类型.
  • value:用户输入的浮点数,稍后将对其进行转换.
  • result:表示转换成功的可选结果.

接下来,我们需要将用户输入作为事件处理.

Event

We defined ConvEvent 作为一个密封类来表示用户输入:

// ConvEvent.kt
密封类ConvEvent {
    数据类setconverversiontype (val转换:ConvType): ConvEvent()

    数据类SetValue(val值:Float): ConvEvent()

    对象转换:ConvEvent()
}

让我们来看看其成员的目的:

  • SetConversionType:选择我们是从英尺转换到米还是从米转换到英尺.
  • SetValue:设置将用于转换的数值.
  • Convert:使用转换类型对输入的值进行转换.

现在,我们将继续我们的视图模型.

声明式管道:事件处理程序和函数组合

视图模型包含我们的事件处理程序和函数组合(声明式管道)代码:

/ / ConverterViewModel.kt
@HiltViewModel
class ConverterViewModel @Inject constructor() : FRPViewModel() {
    companion object {
        const val METERS_FEET_CONST = 3.28084f
    }

    // set initial state
    override val _state: MutableStateFlow = MutableStateFlow(
        ConvState(
            转换= 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 是实现FRP架构的抽象视图模型基础吗. 在我们的代码中,我们需要实现以下方法:

  • val _state:定义状态的初始值(Ivy FRP使用Flow作为响应性数据流).
  • handleEvent(Event): suspend () -> S:在给定的条件下异步产生下一个状态 Event. 底层实现为每个事件启动一个新的协程.
  • stateVal(): S:返回当前状态.
  • updateState((S) -> S): S Updates the ViewModel’s state.

现在,让我们来看看几个与函数组合相关的方法:

  • then:将两个函数组合在一起.
  • asParamTo: Produces a function g() = f(t) from f(T) and a value t (of type T).
  • thenInvokeAfter:组合两个函数,然后调用它们.

updateState and thenInvokeAfter are helper methods shown in the next code snippet; they will be used in our remaining view model code.

声明式管道:附加的函数实现

我们的视图模型还包含用于设置转换类型和值的函数实现, 执行实际的转换, 格式化我们的最终结果:

/ / ConverterViewModel.kt
@HiltViewModel
class ConverterViewModel @Inject constructor() : FRPViewModel() {
// ...
    设置转换(事件:ConvEvent).SetConversionType) =
        updateState { it.复制(转换=事件).conversion) }

    private suspend(事件:ConvEvent.SetValue) =
        updateState { it.copy(value = event.value) }

    Private suspend(暂停)
        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))}
    }

    convertMetersToFeet(meters: Float): Float = meters * METERS_FEET_CONST
    private fun convertfettmeters (ft: Float): Float = ft / METERS_FEET_CONST

    private fun formatResult(result: Float): String =
        DecimalFormat(“# # #,# # #.##").format(result)
}

了解了Ivy FRP辅助函数之后,我们就可以开始分析代码了. 让我们从核心功能开始: convert. convert accepts the state (ConvState)作为输入,并产生一个函数,该函数输出包含转换后的输入结果的新状态. 在伪代码中,我们可以将其总结为: State (ConvState) -> Value (Float) -> Converted value (Float) -> Result (Option).

The Event.SetValue event handling is straightforward; it simply updates the state with the value from the event (i.e.,用户输入要转换的数字). However, handling the Event.SetConversionType Event更有趣,因为它做两件事:

  • 使用所选转换类型更新状态(ConvType).
  • Uses convert 根据所选转换类型转换当前值.

利用构图的力量,我们可以用 convert: State -> State 作为其他组合的输入. 您可能已经注意到,上面演示的代码不是纯粹的:我们正在发生变异 protected abstract val _state: MutableStateFlow in FRPViewModel,每次使用都会产生副作用 updateState {}. 完全纯FP代码的Android Kotlin isn’t feasible.

因为组合不纯的函数会导致不可预测的结果, 混合方法是最实用的:在大多数情况下使用纯函数, 确保任何不纯的函数都有可控的副作用. 这正是我们在上面所做的.

Observable and UI

我们的最后一步是定义我们的应用程序的UI,并使我们的转换器的生活.

一个大的灰色矩形,有四个箭头从右边指向它. From top to bottom, the first arrow, labeled "Buttons,,指向两个较小的矩形:左边的深蓝色矩形,大写文本“米到英尺”;右边的浅蓝色矩形,文本“英尺到米”.第二个箭头,标记为“TextField”,指向一个左对齐的白色矩形文本“100”.0.第三个箭头,标签为“Button”,指向一个左对齐的绿色矩形,文本为“Convert”.最后一个标记为“文本”的箭头指向左对齐的蓝色文本:“Result: 328。.08ft."
应用程序UI的模型.

我们的应用UI会有点“难看”,但这个例子的目的是演示FRP, 而不是使用Jetpack Compose构建一个美丽的设计.

// ConverterScreen.kt
@Composable
乐趣BoxWithConstraintsScope.ConverterScreen(screen: ConverterScreen) {
    FRP { state, onEvent ->
        UI(state, onEvent)
    }
}

我们的UI代码在尽可能少的代码行中使用了基本的Jetpack Compose原则. 然而,有一个有趣的函数值得一提: FRP. FRP 是Ivy FRP框架中的一个可组合函数,它做几件事:

  • 实例化视图模型 @HiltViewModel.
  • 观察视图模型 State using Flow.
  • 将事件传播到 ViewModel with the code onEvent: (Event) -> Unit).
  • Provides a @Composable 执行事件传播并接收最新状态的高阶函数.
  • 可选地提供传递的方式 initialEvent,在应用程序启动时调用.

Here’s how the FRP 函数在Ivy玻璃钢库中实现:

@Composable
inline fun > BoxWithConstraintsScope.FRP(
    initialEvent: E? = null,
    UI: @Composable BoxWithConstraintsScope.(
        state: S,
        onEvent: (E) -> Unit
    ) -> Unit
) {
    val viewModel: VM = viewModel()
    通过viewModel.state().collectAsState()

    if (initialEvent != null) {
        onScreenStart {
            viewModel.onEvent(initialEvent)
        }
    }

    UI(状态,viewModel:: onEvent)
}

可以找到转换器示例的完整代码 GitHub,完整的UI代码可以在 UI function of the ConverterScreen.kt file. 如果你想尝试应用程序或代码, 您可以克隆Ivy FRP存储库并运行 sample app in Android Studio. 您的模拟器可能需要 increased storage 在应用程序运行之前.

使用FRP清洁Android架构

对函数式编程有较强的基础理解, reactive programming, and, finally, 函数式响应式编程, 您已经准备好收获FRP的好处,并构建更干净,更易于维护的Android架构.

Toptal Engineering博客向 Tarun Goyal 查看本文中提供的代码示例.

了解基本知识

  • 函数式编程的好处是什么?

    函数式编程(FP)使用声明式风格, 在其他优势中,哪一种提高了可读性. 此外,FP函数是纯函数,因此不会产生副作用.

  • 响应式编程的好处是什么?

    在响应式编程中, 应用程序对数据或事件更改作出反应,而不是请求有关更改的信息. 这将产生响应更快的UI和改进的用户体验.

  • 为什么响应式编程是功能性的?

    响应式编程和函数式编程是两个独立的范例. 然而,函数式响应式编程结合了这两者,并获得了两者的好处.

  • 什么是Android中的函数式响应式编程?

    函数式响应式编程(FRP)是一种使用声明式函数组合的响应式编程模式.

聘请Toptal这方面的专家.
Hire Now
Iliyan Germanov的头像
Iliyan Germanov

Located in Sofia, Bulgaria

Member since March 30, 2022

About the author

伊利扬是一名Android开发者和首席技术官,他创立了四家初创公司,开发了几款顶级应用, including Ivy Wallet, 它获得了10个YouTube技术社区“最佳UI/UX”奖. 他擅长函数式编程、UX、Kotlin和Haskell.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

Reddit

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.