loading...

Binding Android UI with Kotlin Flow

ychescale9 profile image Yang Updated on ・5 min read

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.

Discussion

pic
Editor guide
Collapse
ijurcikova profile image
Ivet Jurčíková

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?

Collapse
ychescale9 profile image
Yang Author

Great question! You're right that in this case isClosedForSend should never return true in the callback, but this is only because in the the awaitClose 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 when isClosedForSend is false a subsequent offer call might still throw and hence in the safeOffer function we still need to wrap a try / catch around offer.

Hope that answered your question 😀

Collapse
dector profile image
Denys M.

Don't want to upset you. But there is library which does the same: Corbind.

Are there some reasons why to create new one?

Collapse
ychescale9 profile image
Yang Author

Yes I'm aware of Corbind! A couple of months into developing FlowBinding I found out about Corbind which also provides Flow bindings along with actor and channel bindings which existed before coroutines introduced Flow. I thought about moving on to something else but decided continue developing FlowBinding for a couple of reasons:

  • I want something that only supports cold streams (Flow) which is safer API to expose to the consumer than channels.
  • Testing is really important to me both as a developer and a library consumer so I was hesitant to introduce a library with no tests at all into my codebase. For FlowBinding I invested in building a custom testing framework and CI pipeline to ensure all bindings are tested with instrumented tests on CI, and I really wanted to leverage these to continue developing these bindings which had become a fairly mechanical process.
  • It's also a great exercise for me to be able to explore and interact with most of the UI widgets on Android many of which I'd never needed to touch as an app developer.

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 😀

Collapse
dector profile image
Denys M.

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!

Thread Thread
ychescale9 profile image
Yang Author

Thanks! Will consider it.

Thread Thread
mochadwi profile image
Mochamad Iqbal Dwi Cahyo

both of you FlowBinding and Corbind libraries is awesome!!!

Collapse
pranaypatel profile image
pRaNaY

I just love it. Really appreciate your Efforts.