Quality of Life Improvements for Android Development (Kotlin)

Back to Blog Home

Quality of Life Improvements for Android Development (Kotlin)

TLDR; Skip to the end of the article if you’re just looking for code.

As you may have already heard, Kotlin language support for Android development is out of beta since October, 2017. It is a huge productivity improvement for teams to use Kotlin as it reduces the boilerplate more than any other improvement introduces in the previous years. The syntax being quite similar to Swift, cross platform collaboration among developers of different skill sets is now a reality.

What’s new in Kotlin?

  • A cleaner syntax with fewer punctuation
  • First class support for function objects, free functions and anonymous closures, also called lambdas
  • Ability to add functionality to existing types without using inheritance
  • Swift like null check operators and optional types
  • Seamless inter-op with existing Java code
  • Many other functional programming utilities

This is a huge win for the ecosystem. If you are still not convinced, keep reading to see how simple it is to utilize them.

Extensions

Before we get into coding, it is best to explain what extensions are. Imagine you have a class with five different methods and you want to add the sixth. The Java way of doing it is to inherit from the class and implement your method. This may work in some cases but if you keep doing this for every method you need, you may end up with lots of derived classes with single methods. As you create more and more derivations, the cognitive load demanded from the developer as well as the ambiguity of type naming schemes becomes huge issues. Another equivalent solution would be to implement a static function outside of the class which accept the class instance as first argument. This solution is cleaner and has been the status quo for many years.

Languages like C# and Swift solve this problem by disguising static methods of this kind as real instance methods. As long as the dispatch is static (as in method override is ignored), allowing such methods to be called as instance methods causes more good than harm.

Let’s demonstrate a case below with a copied example from Kotlin’s documentation.

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}

As you can see, we added an element wise swap functionality to MutableList<Int> without disturbing the type.

Extensions open many possibilities for utility libraries but they also pollute their types’ namespaces. Before we get into some really useful examples, please make sure that when you use extensions, choose your method names CAREFULLY to avoid name clashes. One good approach is to add a unique identifier indicating your project as prefix to the method names.

Free Functions

Java, being a first class object oriented language, does not allow you to write a free roaming function to outside of a class. Each function has to be owned by a class, thus they are called methods of those classes.

Kotlin loosens this requirement and allows top level functions that do not belong to any type. When a utility applies more than one type, using free functions instead of static methods of some class named after investigating a commonality among otherwise unrelated classes would be much efficient.

Extensions for type: Any

Let’s ask ourselves. What is the most repeated developer task during Android development? One would argue it is null checks, some other would point to logging for debugging purposes. Both are perfect candidates to improve.

fun notNull(vararg os: Any?): Boolean {
    var result = true

    for (o in os) {
        result = result && o != null
        if (!result) break
    }

    return result
}

Above is a free function to null check multiple variables. It can be called with as many arguments as necessary and will return true if at least one of them is null.

// Combined null check of multiple variables
if (notNull(this, aNotNull, aNull))
{
    // All is not null.
} else {
    // Some nulls creep there.
}

What about logging? Can we make LogD(), LogE() etc parts of any objects’ interfaces? Sure we can.

fun Any?.LogTaggedV(tag: String) {
    if (this != null) {
        Log.v(tag, this.toString())
    } else {
        Log.d("Unexpected Null " + tag, "error: ", NullPointerException())
    }
}

fun Any?.LogTaggedW(tag: String) {
    if (this != null) {
        Log.w(tag, this.toString())
    } else {
        Log.d("Unexpected Null " + tag, "error: ", NullPointerException())
    }
}

fun Any?.LogTaggedE(tag: String) {
    if (this != null) {
        Log.e(tag, this.toString())
    } else {
        Log.d("Unexpected Nul l" + tag, "error: ", NullPointerException())
    }
}

fun Any?.LogTaggedD(tag: String) {
    if (this != null) {
        Log.d(tag, this.toString())
    } else {
        Log.d("Unexpected Null " + tag, "error: ", NullPointerException())
    }
}

fun Any?.LogTaggedI(tag: String) {
    if (this != null) {
        Log.i(tag, this.toString())
    } else {
        Log.d("Unexpected Null " + tag, "error: ", NullPointerException())
    }
}


fun Any?.LogV() {
    if (this != null) {
        Log.v(this.javaClass.simpleName, this.toString())
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}

fun Any?.LogW() {
    if (this != null) {
        Log.w(this.javaClass.simpleName, this.toString())
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}

fun Any?.LogE() {
    if (this != null) {
        Log.e(this.javaClass.simpleName, this.toString())
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}

fun Any?.LogD() {
    if (this != null) {
        Log.d(this.javaClass.simpleName, this.toString())
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}

fun Any?.LogI() {
    if (this != null) {
        Log.i(this.javaClass.simpleName, this.toString())
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}


fun Any?.LogV(msg: String) {
    if (this != null) {
        Log.v(this.javaClass.simpleName, msg)
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}

fun Any?.LogW(msg: String) {
    if (this != null) {
        Log.w(this.javaClass.simpleName, msg)
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}

fun Any?.LogE(msg: String) {
    if (this != null) {
        Log.e(this.javaClass.simpleName, msg)
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}

fun Any?.LogD(msg: String) {
    if (this != null) {
        Log.d(this.javaClass.simpleName, msg)
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}

fun Any?.LogI(msg: String) {
    if (this != null) {
        Log.i(this.javaClass.simpleName, msg)
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}


fun Any?.LogV(msg: String, e: Throwable) {
    if (this != null) {
        Log.v(this.javaClass.simpleName, msg, e)
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}

fun Any?.LogW(msg: String, e: Throwable) {
    if (this != null) {
        Log.w(this.javaClass.simpleName, msg, e)
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}

fun Any?.LogE(msg: String, e: Throwable) {
    if (this != null) {
        Log.e(this.javaClass.simpleName, msg, e)
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}

fun Any?.LogD(msg: String, e: Throwable) {
    if (this != null) {
        Log.d(this.javaClass.simpleName, msg, e)
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}

fun Any?.LogI(msg: String, e: Throwable) {
    if (this != null) {
        Log.i(this.javaClass.simpleName, msg, e)
    } else {
        Log.d("Unexpected Null", "error: ", NullPointerException())
    }
}

This may look to much too consume but there are actually four families of functions there. Replace * with any debug levels like V for verbose, D for debug etc.

  1. LogTagged*(tag: String) : Log the value of the object, give custom tag to find easily in Logcat.
  2. Log*() : Log the value of the object, use object’s class name as tag in Logcat.
  3. Log*(msg: String) : Log the message, use object’s class name as tag.
  4. Log*(msg: String, e: Throwable) : Log the message, use object’s class name as tag, print the stack trace.

The best part is, these functions can be called on null objects without throwing since we extended the type Any? instead of plain Any.

Here is the usage.

// Regular logging of a variable
val aNotNull = "Hello"
aNotNull.LogD()

// Nulls can be logged too
val aNull:Object? = null
aNull?.LogV()

// Logs can be tagged
1.LogTaggedI("A one")

// Logs can include custom messages
this.LogW("I'm logging my Activity")

// Logs can include stack traces
this.LogE("OMG, an exception", RuntimeException("Huge error"))

Extensions for type: Number

Do you ever forget how to format floating point numbers for printing? We do but at least now, we don’t need to remember.

fun Float.format(digits: Int) = java.lang.String.format("%.${digits}f", this)
fun Double.format(digits: Int) = java.lang.String.format("%.${digits}f", this)

Use it like this.

// Float format
123.456f.format(1).LogTaggedD("Formatted float")

// Double format
123.456.format(1).LogTaggedD("Formatted double")

Extensions for Resources

Resource management in Android can be sometimes troubling. One functionality being natively available, other put into compatibility classes or support libraries can make developers’ lives a little annoying sometimes. With the help of extensions, retrieving color and drawable resources are now methods of objects (integers) R.color.* or R.drawable.* where * can be any asset in your project.

fun Int?.drawableByResourceId(context: Context): Drawable? {
    this?.let {
        return@drawableByResourceId ContextCompat.getDrawable(context, it)
    }

    return null
}

fun String?.drawableByName(context: Context): Drawable? {
    this?.let {
        val resourceId = context.resources.getIdentifier(it, "drawable", context.packageName)
        return@drawableByName resourceId.drawableByResourceId(context)
    }

    return null
}

@ColorInt
fun Int?.colorById(context: Context): Int? {
    this?.let {
        return@colorById ContextCompat.getColor(context, it)
    }

    return null
}

fun Int?.colorDrawableByResourceId(context: Context): Drawable? {
    this?.let {
        it.colorById(context)?.let {
            return@colorDrawableByResourceId ColorDrawable(it)
        }
    }

    return null
}

Use them directly with R.

// Drawable id -> Drawable
R.drawable.ic_launcher_background.drawableByResourceId(this).LogTaggedD("A drawable by id")

// Drawable name -> Drawable
"ic_launcher_background".drawableByName(this).LogTaggedD("A drawable by name")

// Color id -> Int (as in RGBA 32 bit integer)
R.color.colorPrimary.colorById(this).LogTaggedD("A color Int by id")

// Color id -> Drawable
R.color.colorPrimary.colorDrawableByResourceId(this).LogTaggedD("A color Drawable by id")

Extensions for type: View

At this point we should be careful. View is a highly infectious type such that when extended, your whole codebase will be under its influence. Although you may still find it useful to extend View, always be careful with your naming conventions in order not to confuse your team members.

Let’s add basic visibility functionalities to View.

// All Views
fun View?.hide() {
    this?.let {
        alpha = 0f
        visibility = View.GONE
    }
}

fun View?.show() {
    this?.let {
        alpha = 1f
        visibility = View.VISIBLE
    }
}

fun View?.updateWidthHeight(w: Int? = null, h: Int? = null) {
    this?.let {
        val lp = it.layoutParams
        if (lp != null) {
            lp.width = if (w != null) w else lp.width
            lp.height = if (h != null) h else lp.height
            it.layoutParams = lp
            it.requestLayout()
            it.invalidate()
        }
    }
}

fun View?.removeFromParent() {
    this?.let {
        val parent: ViewGroup? = it.parent as ViewGroup?
        parent?.removeView(it)
    }
}

And use them like this.

// Show / Hide
helloWorldView.hide()
helloWorldView.show()

// Update layout params
helloWorldView.updateWidthHeight(100, 100)

// Remove from View tree
helloWorldView.removeFromParent()

If you are using Glide as your image loader, you may like the extension below to add its functionality directly to ImageView instances.

// Image View
@JvmOverloads
fun ImageView?.loadImage(picturePath: String?, placeholder: Drawable? = null, error: Drawable? = null) {
    this?.let {
        Glide.with(context)
                .load(picturePath)
                .apply {
                    var options = RequestOptions()
                            .centerCrop()

                    placeholder?.let { options.placeholder(placeholder) }
                    error?.let { options.error(error) }
                }
                .into(this)
    }
}

fun ImageView?.setImageDrawable(@DrawableRes resource: Int) {
    this?.setImageDrawable(resource.drawableByResourceId(context))
}

Loading images into ImageViews is now so easy that you will forget it isn’t a native functionality.

// Load image from URL
helloImageView.loadImage("http://placekitten.com/200/300")

// Load image from assets
helloImageView.setImageDrawable(R.drawable.ic_launcher_background.drawableByResourceId(this))

Let’s not forget to shorten Android’s one of the longest call signatures, namely TextView.setCompoundDrawablesRelativeWithIntrinsicBounds().

// Text View
fun TextView?.setDrawableLeft(@DrawableRes resource: Int) {
    this?.setCompoundDrawablesRelativeWithIntrinsicBounds(resource.drawableByResourceId(context), null, null, null)
}

fun TextView?.setDrawableRight(@DrawableRes resource: Int) {
    this?.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, resource.drawableByResourceId(context), null)
}

fun TextView?.setDrawableTop(@DrawableRes resource: Int) {
    this?.setCompoundDrawablesRelativeWithIntrinsicBounds(null, resource.drawableByResourceId(context), null, null)
}

fun TextView?.setDrawableBottom(@DrawableRes resource: Int) {
    this?.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, resource.drawableByResourceId(context))
}

You will never have to worry about remembering which one of the positional arguments belong to the side you want to put your drawable into.

// Set surrounding drawables
helloWorldView.setDrawableLeft(R.drawable.abc_btn_check_material)
helloWorldView.setDrawableTop(R.drawable.abc_btn_check_material)
helloWorldView.setDrawableRight(R.drawable.abc_btn_check_material)
helloWorldView.setDrawableBottom(R.drawable.abc_btn_check_material)

What’s next?

This is the first part of a series of posts aiming to improve everyday lives of Android developers. The next post will extend Kotlin’s lambda type to allow scheduling jobs with one liners. Check the repository for a sneak peek.

Give me the code!

All the code in this article and an example project: https://github.com/testfairy-blog/KotlinQOL

Credits

  1. Photo by Luke Bender on Unsplash.

References

  1. https://kotlinlang.org/docs/reference/extensions.html