Functional operations over Views in ViewGroup using Kotlin

Collections, iterators, arrays, sequences… all share a good set of useful functions that help do transformations, sortings and other kind of operations over their items. But there are parts in Android SDK where this is not available due to the way the classes are constructed.

For instance, we can’t directly get a list of the views inside a ViewGroup, so these kind of operations are not possible. But not everything is lost. In Kotlin, we have a way to prepare any kind of data to work with these operations. The trick is easy: we just need to create a Sequence. In our case, the sequence will be a set of Views in sequential order. We just need to implement its single function, which returns an Iterator.

If we have a Sequence, the world of functional operations is open for us to use them. So let’s start with that.

Note: Read till the end of the article

As lakedaemon666 suggests in comments, there’s an easier way to get the same result without using a sequence. I’ll leave the original text for the record, but I suggest you to take a look to the alternative solution.

Creating a Sequence from a ViewGroup

As mentioned before, we’ll create an iterator, which must know if there is a next item, and which one it is. We can create an extension function, which will be available for any ViewGroup and descendant classes A simple way to do that:

[kotlin]
fun ViewGroup.asSequence(): Sequence<View> = object : Sequence<View> {

override fun iterator(): Iterator<View> = object : Iterator<View> {
private var nextValue: View? = null
private var done = false
private var position: Int = 0

override public fun hasNext(): Boolean {
if (nextValue == null && !done) {
nextValue = getChildAt(position)
position++
if (nextValue == null) done = true
}
return nextValue != null
}

override fun next(): View {
if (!hasNext()) {
throw NoSuchElementException()
}
val answer = nextValue
nextValue = null
return answer!!
}
}
}
[/kotlin]

Retrieving a recursive list of views

It’d be very useful to have a list of views we can apply functions to. So we could first create a list of the Views in the first level, and then use it to retrieve the views inside the rest of ViewGroups inside the parent in a recursive way.

Let’s create a new extension property for ViewGroup. An extension property is very similar to an extension function, and can be applied to any class:

[kotlin]
public val ViewGroup.views: List<View>
get() = asSequence().toList()
[/kotlin]

With this, we could create a recursive function that returns all the Views inside any ViewGroup in the layout:

[kotlin]
public val ViewGroup.viewsRecursive: List<View>
get() = views flatMap {
when (it) {
is ViewGroup -> it.viewsRecursive
else -> listOf(it)
}
}
[/kotlin]

With flatMap, we convert all the multiple lists from every result into one single list. It will iterate over any view, and if it’s a ViewGroup, it will ask for its own views. Otherwise, it’ll return a list with a single item.

Usage examples

Now we can get the viewsRecursive property to perform any operation we can think of. Here you can see a couple of examples. I created a simple layout that looks like this:

Views from a ViewGroup

[xml]
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world"/>

<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello Java"/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Hello Kotlin"/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="Hello Scala"/>

</FrameLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="horizontal">

<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Check 1"/>

<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Check 2"/>

<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Check 3"/>

<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Check 4"/>

</LinearLayout>

</RelativeLayout>
[/xml]

This is the code that, for instance, can be applied in MainActivity.onCreate(). It will convert the Hello Kotlin! string to uppercase, and the even checkboxes to checked:

[kotlin]
val container: ViewGroup = find(R.id.container)
val views = container.viewsRecursive

// Set Kotlin TextView to Upper
val kotlinText = views.first {
it is TextView && it.text.toString().contains("Kotlin")
} as TextView
kotlinText.text = kotlinText.text.toString().toUpperCase()

// Set even checkboxes as checked, and odd as unchecked
views filter {
it is CheckBox
} forEach {
with(it as CheckBox) {
val number = text.toString().removePrefix("Check ").toInt()
setChecked(number % 2 == 0)
}
}
[/kotlin]

Views from a ViewGroup

Alternative solution

As lakedaemon666 mentions in comments (thanks for the explanation), there’s no much sense in creating a sequence if we are iterating over the whole sequence right after that. Sequences are meant for lazy iteration, for instance, when reading the lines of a file. Only the necessary items will be requested. However, in this situation we are using all of them, so a plain list will be enough.

Besides, there’s a much easier way to create a list of views. We can rely on ranges to generate a list of the indexes of the views and map them to the list of views we need. Everything in just one line:

[kotlin]
public val ViewGroup.views: List<View>
get() = (0..getChildCount() – 1) map { getChildAt(it) }
[/kotlin]

Conclusion

This is a silly example, but with this idea you’ll probably be able to make all your code more functional, and stop depending on loops and other flow controls which are more typical from iterative programming.

And remember you can learn this and many other things about Kotlin in the book I’m writing: Kotlin for Android Developers, where you will learn Kotlin by creating an Android App from the ground up.

7 thoughts on “Functional operations over Views in ViewGroup using Kotlin”

  1. Creating a sequence (a lazy iterator), to immediately turn it to list sounds like weird to me.
    When you go to the trouble to create a sequence (iterator), you usually keep using sequences (operators transform sequences into sequences) instead of list to avoid Set/List construction and copy and also to keep things lazy, instead of eager.

    If you want a list or a set, you can still use toList() or toSet() at the end of your pipeline.

    If you want a list, It might be simpler to do
    public val ViewGroup.views: List
    get() = (0..getChildCount()-1).map({getChildAt(it)})

    And if you want a sequence, you don’t have to use an extension fonction as you can use the built in asSequence for IntRange :
    public val ViewGroup.views: Sequence
    get() = (0..getChildCount()-1).asSequence().map({getChildAt(it)})

    Last but not least, kotlin sequences obviously don’t use java 8 streams on Android yet, so stuff like parallel computation (which is one of the best parts of streams) is not available yet.

    Personnaly, till now, I haven’t used sequences that much. Using lists usually does the job.

    1. Antonio Leiva

      Sounds like a great idea! I’m still learning both Kotlin and functional programming and I’m sure many things can be done in a better way (that’s an example). Thanks for taking your time to explain it in detail. I’ll update the article to reflect your comment.

  2. Updated for Kotlin 1.1 without errors/warnings/messages:

    val ViewGroup.views: List
    get() = (0..childCount – 1).map { getChildAt(it) }

  3. Hi I also would like to give a little simpler code:

    “`
    private val ViewGroup.items: Iterable get() = Iterable {
    var i = 0
    object : Iterator {
    override fun hasNext(): Boolean = i < childCount
    override fun next(): View = getChildAt(i).also { i++ }
    }
    }

    private val ViewGroup.itemsList get() = (0 until childCount).map { getChildAt(it) }
    “`

Comments are closed.