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:
@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) 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.
😀
Featured in Kotlin Weekly #170.
Top comments (8)
Thanks for your effort and for making the library!
I have one, a bit unrelated question: while introducing
safeOffer
, you wrote: "But the function might throw an exception if the channel is closed for send." - When can this happen? How is it possible that the channel may be closed for send if its superior flow is not?Great question! You're right that in this case
isClosedForSend
should never returntrue
in the callback, but this is only because in the theawaitClose
block we've unregistered / removed the callback from the widget. So this is somewhat a "defensive" approach to make sure we don't crash if we forgot to unregister the callback (which is arguably worse as we usually want to fail early but that's a different conversation).SendChannel.offer
also has other issues (note the 3rd point), so even whenisClosedForSend
isfalse
a subsequentoffer
call might still throw and hence in thesafeOffer
function we still need to wrap a try / catch aroundoffer
.Hope that answered your question 😀
Don't want to upset you. But there is library which does the same: Corbind.
Are there some reasons why to create new one?
Yes I'm aware of Corbind! A couple of months into developing FlowBinding I found out about Corbind which also provides
Flow
bindings along withactor
andchannel
bindings which existed before coroutines introducedFlow
. I thought about moving on to something else but decided continue developing FlowBinding for a couple of reasons:Flow
) which is safer API to expose to the consumer than channels.That said, from a user's perspective if you’re already using Corbind and are happy with it there’s probably little reason to switch; but if you are looking to migrate from RxBinding to a
Flow
equivalent today, I think FlowBinding is a good option.Hope that answered you question 😀
Thanks for you detailed answer!
Have you thought about joining your efforts (e.g. tests) with author of Corbind? I guess, it's win-win situation for the community when instead of few same-functionality libraries we'll have one that offers strong and bug-less implementation.
There is good example in Kotlin-community: Arrow. Authors of two functional libraries joined their efforts to avoid ecosystem fragmentation.
Good luck!
Thanks! Will consider it.
both of you
FlowBinding
andCorbind
libraries is awesome!!!I just love it. Really appreciate your Efforts.