"Implementing Functional Reactive Programming in Your Android Code for Long-Term Success: Part 2"

Functional reactive programming (FRP) merges the dynamic nature of reactive programming with the structured function composition of functional programming. It streamlines complex operations, crafts elegant user interfaces, and manages state seamlessly. Due to these and many other clear benefits, FRP is gaining significant traction in both mobile and web development.

However, grasping this programming paradigm is not a simple task—even experienced developers might ask: “What exactly is FRP?” In Part 1 of this tutorial, we established FRP’s core principles: functional programming and reactive programming. This part will equip you to apply it practically, with an overview of valuable libraries and a step-by-step sample implementation.

While geared towards Android developers, the concepts explored here are applicable and advantageous to any developer familiar with general-purpose programming languages.

Embarking on FRP: System Design

The FRP paradigm revolves around a continuous cycle of states and events: State -> Event -> State' -> Event' -> State'' -> …. (Remember, ', pronounced “prime,” denotes a new iteration of the same variable.) Each FRP program begins with an initial state that undergoes updates with every event it encounters. This program consists of elements mirroring those in a reactive program:

  • State
  • Event
  • The declarative pipeline (represented as FRPViewModel function)
  • Observable (represented as StateFlow)

Here, we’ve substituted the general reactive elements with tangible Android components and libraries:

Two main blue boxes, "StateFlow" and "State," have two main paths between them. The first is via "Observes (listens for changes)." The second is via "Notifies (of latest state)," to blue box "@Composable (JetpackCompose)," which goes via "Transforms user input to" to blue box "Event," which goes via "Triggers" to blue box "FRPViewModel function," and finally via "Produces (new state)." "State" then also connects back to "FRPViewModel function" via "Acts as input for."
The functional reactive programming cycle in Android.

Several Android libraries and tools can facilitate your FRP journey, also relevant to functional programming:

  • Ivy FRP: A library I developed for educational purposes in this tutorial. Consider it a starting point for your FRP approach, not for production use due to limited support. (I’m currently its sole maintainer.)
  • Arrow: A leading and widely used Kotlin library for FP, which we’ll also employ in our sample app. It offers nearly everything needed to implement functional programming in Kotlin while remaining lightweight.
  • Jetpack Compose: Android’s current toolkit for native UI development and the third library we’ll utilize. It’s indispensable for modern Android development—I recommend learning and migrating your UI to it if you haven’t already.
  • Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/): Kotlin’s asynchronous reactive data stream API. While not directly used in this tutorial, it integrates well with common Android libraries like RoomDB, [Retrofit, and Jetpack. Flow works seamlessly with coroutines and offers reactivity. For instance, when used with RoomDB, Flow ensures your app always operates on the latest data. Any table change instantly propagates to flows reliant on that table.
  • Kotest: A testing platform providing property-based testing support relevant to pure FP domain code.

Building a Sample Feet/Meters Conversion App

Let’s illustrate FRP in action within an Android app. We’ll construct a simple app converting values between meters (m) and feet (ft).

For this tutorial, I’m focusing solely on code snippets crucial for understanding FRP, simplified from my full converter sample app. If you’re following along in Android Studio, create your project with a Jetpack Compose activity, and include install Arrow* and Ivy FRP. Ensure your minSdk version is 28 or higher and you’re using Kotlin 1.6+ or later.

* Note: There’s now a newer version of Arrow, but the rest of this tutorial hasn’t been tested with it yet.

State

Let’s define our app’s state:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ConvState.kt
enum class ConvType {
	METERS_TO_FEET, FEET_TO_METERS
}

data class ConvState(
    val conversion: ConvType,
    val value: Float,
    val result: Option<String>
)

Our state class is quite straightforward:

  • conversion: Specifies the conversion type—feet to meters or vice versa.
  • value: The float input by the user, to be converted.
  • result: An optional result representing a successful conversion.

Next, we need to manage user input as an event.

Event

ConvEvent is defined as a sealed class to represent user input:

1
2
3
4
5
6
7
8
// ConvEvent.kt
sealed class ConvEvent {
    data class SetConversionType(val conversion: ConvType) : ConvEvent()

    data class SetValue(val value: Float) : ConvEvent()

    object Convert : ConvEvent()
}

Let’s examine the roles of its members:

  • SetConversionType: Determines whether we’re converting from feet to meters or meters to feet.
  • SetValue: Sets the numerical values used for conversion.
  • Convert: Executes the conversion of the inputted value using the chosen conversion type.

Now, let’s proceed to our view model.

The Declarative Pipeline: Event Handler and Function Composition

The view model houses our event handler and function composition (declarative pipeline) code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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
    }
// ...
}

Before dissecting the implementation, let’s clarify some Ivy FRP library-specific objects.

FRPViewModel<S,E> is an abstract view model base implementing the FRP architecture. Our code needs to implement these methods:

  • val _state: Sets the initial state value (Ivy FRP uses Flow for reactive data streams).
  • handleEvent(Event): suspend () -> S: Asynchronously produces the subsequent state based on an Event. The implementation launches a new coroutine for each event.
  • stateVal(): S: Returns the current state.
  • updateState((S) -> S): S Updates the ViewModel’s state.

Let’s now look at some function composition methods:

  • then: Combines two functions.
  • asParamTo: Generates a function g() = f(t) from f(T) and a value t (of type T).
  • thenInvokeAfter: Composes two functions and then invokes them.

updateState and thenInvokeAfter are helper methods shown in the next code snippet, used in our remaining view model code.

The Declarative Pipeline: Additional Function Implementations

Our view model also contains implementations for setting conversion type and value, performing conversions, and formatting the final result:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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)
}

With an understanding of our Ivy FRP helper functions, let’s analyze the code. Starting with the core: convert. This function takes the state (ConvState) as input and produces a function returning a new state containing the converted input. In pseudocode: State (ConvState) -> Value (Float) -> Converted value (Float) -> Result (Option<String>).

Handling the Event.SetValue event is simple; it updates the state with the event’s value (i.e., the user-inputted number). Handling the Event.SetConversionType event is more intriguing; it performs two actions:

  • Updates the state with the chosen conversion type (ConvType).
  • Utilizes convert to convert the current value based on the selected type.

Through composition, we can leverage the convert: State -> State function as input for other compositions. You’ll notice the code above isn’t purely functional: We mutate protected abstract val _state: MutableStateFlow<S> in FRPViewModel, introducing side effects when using updateState {}. Achieving entirely pure FP code for Android in Kotlin isn’t feasible.

Composing impure functions can lead to unpredictable outcomes, making a hybrid approach most practical: Utilize pure functions primarily, ensuring any impure functions have controlled side effects. This is precisely what we’ve achieved above.

Observable and UI

Our last step is defining our app’s UI, bringing the converter to life.

A large gray rectangle with four arrows pointing to it from the right. From top to bottom, the first arrow, labeled "Buttons," points to two smaller rectangles: a dark blue left rectangle with the uppercased text "Meters to feet" and a light blue right rectangle with the text "Feet to meters." The second arrow, labeled "TextField," points to a white rectangle with left-aligned text, "100.0." The third arrow, labeled "Button," points to a left-aligned green rectangle with the text "Convert." The last arrow, labeled "Text," points to left-aligned blue text reading: "Result: 328.08ft."
A mock-up of the app’s UI.

Our app’s UI will be visually basic, as the focus is demonstrating FRP, not elaborate design using Jetpack Compose.

1
2
3
4
5
6
7
// ConverterScreen.kt
@Composable
fun BoxWithConstraintsScope.ConverterScreen(screen: ConverterScreen) {
    FRP<ConvState, ConvEvent, ConverterViewModel> { state, onEvent ->
        UI(state, onEvent)
    }
}

The UI code uses fundamental Jetpack Compose principles with minimal code. However, one noteworthy function is FRP<ConvState, ConvEvent, ConverterViewModel>. FRP is a composable function from the Ivy FRP framework, performing several tasks:

  • Instantiates the view model using @HiltViewModel.
  • Observes the view model’s State using Flow.
  • Propagates events to the ViewModel with the code onEvent: (Event) -> Unit).
  • Provides a @Composable higher-order function handling event propagation and receiving the latest state.
  • Optionally allows passing initialEvent, called once the app starts.

Here’s the Ivy FRP library implementation of the FRP function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@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)
}

The complete converter example code is in GitHub, and the entire UI code resides within the UI function of the ConverterScreen.kt file. To experiment with the app or code, clone the Ivy FRP repository and run the sample app in Android Studio. Your emulator might need increased storage for the app to function correctly.

Towards a Cleaner Android Architecture with FRP

With a solid grasp of functional programming, reactive programming, and now functional reactive programming, you’re equipped to harness the advantages of FRP and build cleaner, more maintainable Android architecture.

The Toptal Engineering Blog thanks Tarun Goyal for reviewing the code samples presented in this article.

Licensed under CC BY-NC-SA 4.0