Love for Kotlin


I spent a fair amount of time learning how annotation processors work. I made a sample library called ascent on github for injecting custom fonts into fields. It was a great way to learn. The benefit of annotation processors is boilerplate destruction. I’ve always disliked having to write code over and over again, one example of that is converting values from dp to px values in custom views. The designers I’ve worked with love to give me Custom Views, so I’ve frequently run into this problem. Here is a piece of code that was very common in my code base that I really wanted to get rid of:

DisplayMetrics metrics = context.getResources().getDisplayMetrics();
Float lineWidth = TypedValue.applyDimension(type, value, metrics);


Except that I would end up with many lines just like this in my custom views. I really just wanted to stop having to write these lines. One option would have been to inject the values, unfortunately that would just be moving the problem from the custom view to elsewhere. Sure we could store them in an attribute and have the os pull out the right value for us, but in many cases I don’t need to expose these values as attributes because they aren’t going to change frequently if at all, and this isn’t going into a library. So it would be nice to have a concise way of doing these sorts of conversions. Kotlin provides a really nice way for us to do just that.

Extension Functions
Kotlin allows us to add methods to existing classes. We aren’t talking about inheritance. As we are adding methods to existing classes not creating new derived classes with methods. Here is an example of simplifying the above code:

package com.kotlin.demo

import android.util.TypedValue
import android.view.View

fun View.applyDimension(value: Float, type : Int = TypedValue.COMPLEX_UNIT_DIP): Float{
    val metrics  = getResources().getDisplayMetrics()
    val resultPix = TypedValue.applyDimension(type, value, metrics)
    return resultPix
}


This code means that I can now on any View call applyDimension and convert a DP value into raw pixel values. Here is what it would look like in a custom view.

package com.kotlin.demo

import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import com.kotlin.demo.Command

class CustomView : View {

    constructor(context: Context?) : super(context){
        initialize()
    }

    constructor(context: Context?, attrs: AttributeSet) : super(context, attrs) {
        initialize()
    }

    constructor(context: Context?, attrs: AttributeSet, defStyle : Int) : super(context, attrs, defStyle){
        initialize()
    }

    fun initialize(){
        val verticalLineThickness = applyDimension(5f)
        val horizontalLineThickness = applyDimension(10f)
        val paddingBetweenItems = applyDimension(15f)
        val fontTitleSize = applyDimension(32f, TypedValue.COMPLEX_UNIT_SP)
        val subtitleSize = applyDimension(14f, TypedValue.COMPLEX_UNIT_SP)
    }


}

If you notice the last two lines I’m also able to convert scaled pixel values to the raw pixel size as well. Super useful. Also I’m glad that the unit now comes after value, not sure why the api has the unit come before the value. Where else would you use extension functions?

Let’s look at another example.

Have you ever need to get the size of a view before it’s drawn?

fun View.afterLayout(func: View.() -> Unit) {
    viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            if (measuredWidth > 0 && measuredHeight > 0) {
                viewTreeObserver.removeOnGlobalLayoutListener(this)
                func()
            }
        }
    })
}

This allows us to get the width and height. Allowing us to perform a callback like so for any view:

text.afterLayout {
    text.text = text.left.toString()  // simple example of a TextView being populated
}

Now we can improve our layout function by inlining it. Per the docs:

“Using higher-order functions imposes certain runtime penalties: each function is an object, and it captures a closure, i.e. those variables that are accessed in the body of the function. Memory allocations (both for function objects and classes) and virtual calls introduce runtime overhead.

But it appears that in many cases this kind of overhead can be eliminated by inlining the lambda expressions.”

Simply inline the method:

inline fun View.afterLayout(func: View.() -> Unit) {
    // same code as before
}

But, then you run into a problem with func. You’ll see the message “Can’t inline ‘func’ here. It may contain non local returns” Fortunately the docs also cover this:

“Note that some inline functions may call the lambdas passed to them as parameters not directly from the function body, but from another execution context, such as a local object or a nested function. In such cases, non-local control flow is also not allowed in the lambdas. To indicate that, the lambda parameter needs to be marked with the crossinline modifier”

That leaves us with the following:

inline fun View.afterLayout(crossinline func: View.() -> Unit) {

    viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            if (measuredWidth > 0 && measuredHeight > 0) {
                viewTreeObserver.removeOnGlobalLayoutListener(this)
                func()
            }
        }
    })
}

Happy Valentines Day!

Leave a Reply

Your email address will not be published. Required fields are marked *