Binding Android UI with Kotlin Flow
Tags: android, kotlin, coroutines
Published: 2019-10-29 15:23:30

Modern Android codebases are becoming increasingly **reactive**. With concepts and patterns such as [MVI](http://hannesdorfmann.com/android/model-view-intent), [Redux](https://redux.js.org/), [Unidirectional Data Flow](https://redux.js.org/basics/data-flow), many components of the system are being modelled as **streams**.

UI events can also be modelled as streams of inputs into the system.

Android’s platform and unbundled UI widgets provide listener / callback style APIs, but with [RxBinding](https://github.com/JakeWharton/RxBinding) they can easily be mapped to RxJava `Observable`.

```kotlin
findViewById<Button>(R.id.button).clicks().subscribe {
    // handle button clicked
}
```

### Kotlin Flow

[kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) 1.3 introduced `Flow`, which is an important addition to the library which finally has support for [cold streams](https://medium.com/@elizarov/cold-flows-hot-channels-d74769805f9). It’s (conceptually) a reactive streams implementation based on Kotlin’s suspending functions and channels API.


#### Binding Android UI with Flow
In this post I’m not going to discuss **why** you may or may not want to migrate from **RxJava** to **Kotlin Coroutines** / **Flow**. But let’s see how we can implement the same `clicks()` example above with `Flow`. The API should look something like this:

```kotlin
scope.launch {
    findViewById<Button>(R.id.button)
        .clicks() // this returns a Flow<Unit>
        .collect {
            // handle button clicked
        }
}
```

The **kotlinx.coroutines** library offers many top-level builder functions for creating Flow. One such function is `callbackFlow` which is specifically designed for converting a multi-shot callback API into a `Flow`.

```kotlin
fun View.clicks(): Flow<Unit> = callbackFlow 
    val listener = View.OnClickListener {
        offer(Unit)
    }
    setOnClickListener(listener)
    awaitClose { setOnClickListener(null) }
}
```

The block within `awaitClose` is run when the consumer of the flow cancels the flow collection so this is the perfect place to remove the listener registered earlier.

`offer(…)` pushes a new element into the `SendChannel` which `Flow` uses internally. But the function might throw an exception if the channel is closed for send. We can create an extension function that catches any cancellation exception:

```kotlin
fun <E> SendChannel<E>.safeOffer(value: E) = !isClosedForSend && try {
    offer(value)
} catch (e: CancellationException) {
    false
}
```

Here’s the complete implementation:

```kotlin
@CheckResult
@UseExperimental(ExperimentalCoroutinesApi::class)
fun View.clicks(): Flow<Unit> = callbackFlow {
    checkMainThread()
    val listener = View.OnClickListener {
        safeOffer(Unit)
    }
    setOnClickListener(listener)
    awaitClose { setOnClickListener(null) }
}.conflate()
```

Some UI widgets might hold a state internally such as the current value of a **Slider** (a recently added [Material Component](https://github.com/material-components/material-components-android/blob/master/docs/components/Slider.md)) which you might want to observe with a `Flow`. In this case it might also be useful if the Flow can emit the current value **immediately** when collected, so that you can bind the value to some other UI element as soon as the screen is launched without the value of the slider ever being changed by the user.

```kotlin
@CheckResult
@UseExperimental(ExperimentalCoroutinesApi::class)
fun Slider.valueChanges(emitImmediately: Boolean = false): Flow<Float> = callbackFlow {
    checkMainThread()
    val listener = Slider.OnChangeListener { _, value ->
        safeOffer(value)
    }
    setOnChangeListener(listener)
    awaitClose { setOnChangeListener(null) }
}
    .startWithCurrentValue(emitImmediately) { value }
    .conflate()
```

The optional `emitImmediately` parameter controls whether to emit the current value immediately on flow collection.

When `emitImmediately` is true we add `onStart { emit(value)}` on the original `flow` which is the equivalent of `startWith(value)` in RxJava. This behaviour can again be wrapped in an extension function:

```kotlin
fun <T> Flow<T>.startWithCurrentValue(emitImmediately: Boolean, block: () -> T?): Flow<T> {
    return if (emitImmediately) onStart {
        block()?.run { emit(this) }
    } else this
}
```

As we can see it’s quite easy to implement UI event bindings for Kotlin Flow, thanks to the powerful Coroutines APIs. But there are myriad of other widgets both from the platform and the unbundled libraries (AndroidX), while new components such as **MaterialDatePicker** and **Slider** are being added to [Material Components Android](https://github.com/material-components/material-components-android).

It’d be nice if we have a library of these bindings for Kotlin Flow.

### Introducing FlowBinding

In the past few months I’ve been working on [FlowBinding](https://github.com/ReactiveCircus/FlowBinding) which offers a comprehensive set of binding APIs for Android’s platform and unbundled UI widgets, and I’m delighted to share the first [public release](https://github.com/ReactiveCircus/FlowBinding/releases/tag/0.5.0) now that the roadmap for 1.0 is complete.

The library is inspired by Jake’s [RxBinding](https://github.com/JakeWharton/RxBinding) and aims to cover **most** of the bindings provided by RxBinding, while shifting our focus to supporting more modern **AndroidX** APIs such as **ViewPager2** and the new components in Material Components as they become available.

Bindings are available as independent artifacts:

```groovy
// Platform bindings
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:${flowbinding_version}"
// AndroidX bindings
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:${flowbinding_version}"
implementation "io.github.reactivecircus.flowbinding:flowbinding-core:${flowbinding_version}"
implementation "io.github.reactivecircus.flowbinding:flowbinding-drawerlayout:${flowbinding_version}"
implementation "io.github.reactivecircus.flowbinding:flowbinding-navigation:${flowbinding_version}"
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:${flowbinding_version}"
implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:${flowbinding_version}"
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager2:${flowbinding_version}"
// Material Components bindings
implementation "io.github.reactivecircus.flowbinding:flowbinding-material:${flowbinding_version}"
```

List of specific binding APIs provided is available in [each subproject](https://github.com/ReactiveCircus/FlowBinding#roadmap).

#### Tests

Lots of efforts have been put into testing the library. **All** binding APIs are covered by Android instrumented tests which are run on CI builds.

#### Usage

To observe click events on an Android `View`:

```kotlin
findViewById<Button>(R.id.button)
    .clicks() // binding API available in flowbinding-android
    .onEach {
        // handle button clicked
    }
    .launchIn(uiScope)
```

#### Binding Scope
`launchIn(scope)` is a shorthand for `scope.launch { flow.collect() }` provided by the **kotlinx-coroutines-core** library.

The `uiScope` in the example above is a `CoroutineScope` that defines the lifecycle of this `Flow`. The binding implementation will respect this scope by unregistering the callback / listener automatically when the scope is cancelled.

In the context of Android lifecycle this means the `uiScope` passed in here should be a scope that's bound to the `Lifecycle` of the view the widget lives in.

`androidx.lifecycle:lifecycle-runtime-ktx:2.2.0` introduced an extension property `LifecycleOwner.lifecycleScope: LifecycleCoroutineScope` which will be cancelled when the `Lifecycle` is destroyed.

In an Activity it might look something like this:

```kotlin
class ExampleActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_example)
        findViewById<Button>(R.id.button)
            .clicks()
            .onEach {
                // handle button clicked
            }
            .launchIn(lifecycleScope) // provided by lifecycle-runtime-ktx 
            
    }
}
```

Note that with FlowBinding you no longer need to unregister / remove listeners or callbacks in `onDestroy()` as this is done automatically for you.

#### More Examples

All binding APIs are documented with **Example of usage** which can be found in the source.

You can also find usages of all bindings from the instrumented tests.

#### Roadmap

With the initial release we’ve covered most of the bindings available RxBindings and added bindings for **Material Components** including the new **MaterialDatePicker** and **Slider**. While the library is heavily tested with instrumented tests, the APIs are not yet stable. Our plan is to polish the library by adding any missing bindings and fixing bugs as we work towards 1.0.

Your help would be much appreciated! Please feel free to [create an issue](https://github.com/ReactiveCircus/FlowBinding/issues) on GitHub if you think a useful binding is missing or you want a new binding added to the library.

[FlowBinding](https://github.com/ReactiveCircus/FlowBinding) provides a more fluent and consistent API compared to the stock listeners / callbacks.

One added benefit is that we no longer need to unregister / remove listeners in `onDestroy` method as we can take advantage and Coroutine’s **structured concurrency** and the `lifecycleScope` provided by **AndroidX Lifecycle**.

If your project currently uses **RxBinding** and you are planning to migrate over to Kotlin Coroutines / Flow, **FlowBinding** should be a good replacement option.

I hope this can be of use to some of you. Please feel free to leave a comment below or reach out to me on [twitter](https://twitter.com/ychescale9).
😀

-------------------------

Featured in [Kotlin Weekly #170](https://mailchi.mp/kotlinweekly/kotlin-weekly-170).