Baby steps to Reactive programming with Kotlin
When I started to work with RxJava it was extremely hard to get used to the reactive way of thinking and get a grasp on those wide ranges of operators, but no matter the difficulty the benefits are clear. With reactive programming, the code becomes good structured and beautifully organized.
Iterative solution
Let’s look at a task where we have to change the font inside the TabLayout. You have to go down 3 layers deep in the TabLayout view structure to find each TextView and change the typeface for all of them.
fun TabLayout.setRegularFont() {
for (i in 0..this.childCount - 1) { // 1 range
val child = this.getChildAt(i) // 2 map
if (child is ViewGroup) { // 3 filter
for (j in 0..child.childCount - 1) {
val childViewGroup = child.getChildAt(j)
if (childViewGroup is ViewGroup) {
for (x in 0..childViewGroup.childCount - 1) {
val textView = childViewGroup.getChildAt(x)
if (textView is TextView) {
textView.typeface = typeface
} } } } } } }
This is an iterative solution and it works exactly as we intended but it is barely readable and not maintainer friendly at all.
functional programming
If we look closely Observable<T> and Iterable<T> is very similar to each other, they are both just a sequence of values. They can contain zero to infinite elements but the only difference in this abstraction is that with the Observable values are separated in time, and might not exist yet, with Iterable they are already collected. The Kotlin standard library was built with this methodology in mind and we can enjoy the benefit of high order functions. Now, look at how we can modify the iterative to functional programming step by step.
- Range generation can be achieved much better with
until
binary function, now we can get rid of those ugly "-1"-s in the code. - Looping on a range can be handled as an Iterator. Instead of using the returned range as a for loop parameter just take the index range itself and transform into the corresponding child from the ViewGroup. The
map(transform: (T) -> R)
high order function solves this for you. - Filtering a range based on subtype can be achieved with
filterIsInstance<R>()
where we filter based on the subtype.
Continue executing the same on the remaining elements until you reach the third layer and set the TextViews font type.
fun TabLayout.setRegularFont() {
(0 until this.childCount)
.map { this.getChildAt(it) }
.filterIsInstance<ViewGroup>()
.flatMap { child ->
(0 until child.childCount)
.map { child.getChildAt(it) }
.filterIsInstance<ViewGroup>()
}
.flatMap { childViewGroup ->
(0 until childViewGroup.childCount)
.map { childViewGroup.getChildAt(it) }
.filterIsInstance<TextView>()
}
.forEach { it.typeface = typeface }
}
This is an elegant solution and you can clearly distinguish the building blocks of the algorithm without any documentation.
One more thing…
These blocks not only help you break up your code logic into pieces but helps you reason about your solution. At the second approach, you can realize we are doing the same repetitive action over and over again, the only difference is the filter type.
Don’t fear to look back at your code and see mistakes, think of them like someone else made those silly mistakes and solve it the best way you can!
Move our code into an extension function which later could be useful for others as well:
fun <T : View> ViewGroup.filterChildren() : Iterable<T> {
return (0 until this.childCount)
.map { getChildAt(it) }
.filterIsInstance<T>()
}
And simplify our code:
fun TabLayout.setTextTypeface(typeface: Typeface) {
filterChildren<ViewGroup>()
.flatMap { filterChildren<ViewGroup>() }
.flatMap { filterChildren<TextView>() }
.forEach { it.typeface = typeface }
}