Binding Android UI with Kotlin Flow

Yang
5 min readOct 29, 2019

Modern Android codebases are becoming increasingly reactive. With concepts and patterns such as MVI, Redux, Unidirectional 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 they can easily be mapped to RxJava Observable.

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

Kotlin Flow

kotlinx.coroutines 1.3 introduced Flow, which is an important addition to the library which finally has support for cold streams. 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:

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.

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:

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

Here’s the complete implementation:

Some UI widgets might hold a state internally such as the current value of a Slider (a recently added Material Component) 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.

@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:

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.

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 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 now that the roadmap for 1.0 is complete.

The library is inspired by Jake’s 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:

// 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.

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:

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:

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 on GitHub if you think a useful binding is missing or you want a new binding added to the library.

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.

😀

--

--