Reduce verbosity in Kotlin Flow

Using Kotlin Flow in our Android app became a real improvement. Beside to be a Kotlin native library based on coroutine, it is easy to understand, to implement and to read.

Reducing verbosity has great advantages: it lowers the noise when we read the code, it saves us time when writing, etc. It makes code cleaner. With the next few tricks, we can improve the readability of our flows, making the code even smoother.

asFlow 🧪

This extension function creates a flow from various collections and sequences.

It converts most of the Iterable to a cold flow. This drastically reduces the number of lines when coding. If a flow emits a sequence of three numbers:

fun numbers(): Flow<Int> = flow { for (i in 1..3) { emit(i) } }

We can use the asFlow extension to get rid of the builder and it becomes much more cleaner.

fun numbers(): Flow<Int> = (1..3).asFlow()

This increases the quality of code, decreasing the lines to only one and improves readability.

flowOf ⚗️

flowOf creates a flow without the full builder declaration too.

The main difference with its sibling asFlow is that it is not restricted to collections. This produces a flow from the elements passing to it. For example, if we create a stream of letters:

fun letters(): Flow<Char> = flow { emit(‘a’); emit(‘b’); emit(‘c’) }

It can be reduced with the help of this simple builder.

fun letters(): Flow<Char> = flowOf(‘a’, ‘b’, ‘c’)

Same as before, it increases readability with only one line of code.

tranform 🔬

Doing chain logic on a flow could lead to a huge amount of code. This is where tranform comes into place.

This powerful operator reduces the verbosity by extracting it into a custom extension. Let’s assume we have a flow to retrieve names, but we only want to get ones starting with a specific letter and capitalize it.

flowOf(“annie”, “andrew”, “jane”, “mike”).filter { it.startsWith(“a”) }.map { return@map it.replaceFirstChar(Char::uppercase) }.collect { println(it) }

tranform generalizes filter and map operators and can be used as a building block for other operators. Under the hood, filter, map and onEach use it. So, instead of the above code, we could write an extension and put our “magic” inside.

fun Flow<String>.getANamesAndCapitalize(): Flow<String> = transform { name -> if (name.startsWith(“a”)) { val newName = name.replaceFirstChar(Char::uppercase); emit(newName) } }

Thanks to this, we move out the logic from the flow process.

flowOf(“annie”, “andrew”, “jane”, “mike”).getANamesAndCapitalize().collect { println(it) }

This awesome operator reduces the logic verbosity to one line. This is pretty convenient to improve the readability.

launchIn 💊

One of the most effective terminal operators to remove flow’s collecting verbosity.

When we collect a flow, we often bind it to a lifecycle scope. This way, if the scope is cancelled, the flow is destroyed as well. @Geev, we mostly attach the scope of a ViewModel to a flow collecting. Then, if the ViewModel is cleared, all flows tied to it are cancelled.

fun fetchNames() { viewModelScope.launch { flowOf(“Jane”, “John”, “Janice”).collect { println(it) } } }; fun fetchCities() { viewModelScope.launch { flowOf(“London”, “New-York”, “Tokyo”).collect { println(it) } } }

By using launchIn, we shrink the coroutine declarations and still launch the flow collections with the ViewModel lifecycle.

fun fetchNames() { flowOf(“Jane”, “John”, “Janice”).onEach { println(it) }.launchIn(viewModelScope) }; fun fetchCities() { flowOf(“London”, “New-York”, “Tokyo”).onEach { println(it) }.launchIn(viewmodelScope) }

Note that because it's a terminal operator, we do not use collect anymore. Indeed, launchIn is a contraction of launch and collect. Instead, we can call onEach to deal with the received items.

This removes the outer brackets making the code more explicit and concise.

asLiveData 💉

Last but not least, this extension does not come from Kotlin Flow but it is part of androidx.lifecycle and it can be useful to keep the ViewModel lean.

In Android, we usually use LiveData to update the UI. Inside our ViewModel, we create a private MutableLiveData and update its value when we receive new items from a flow.

private val country: MutableLiveData<String> = MutableLiveData(); fun getCountry(): LiveData<String> = country; fun fetchCountry() { countriesRepository.fetchCountry().onEach { country.value = it }.launchIn(viewModelScope) }

In the Activity/Fragment, we observe this LiveData and update the UI accordingly.

countriesViewModel.getCountry().observe(this) { country -> countryTextView.text = country }; countriesViewModel.fetchCountry()

To make our ViewModel smaller, we can deal with the asLiveData extension. This terminal operator converts the flow into a LiveData. It combines the onEach and the launchIn into one line. We no longer need to create a MutableLiveData and update manually its value.

val country: LiveData<String> = countriesRepository.fetchCountry().asLiveData(viewModelScope.coroutineContext)

In the Activity/Fragment, this helps to shrink the code too.

countriesViewModel.country.observe(this) { country -> countryTextView.text = country }

This extension decreases the verbosity in both ViewModel and Activity / Fragment.

Smoother flow 🩺

Kotlin Flow has many powerful operators and extensions to reduce verbosity. These few tricks lower the amount of lines making the flow concise and mainly more understandable at first glance. These make our code cleaner and our flow smoother.

If you found this post helpful, feel free to clap! 👏 Thanks for reading.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Florent Blot

Florent Blot

Maker, Mobile developer, Kotlin & Dart enthusiast, IoT & DIY addict. @Geev: https://www.geev.com