Extending Resources

pablisco
AndroidPub
Published in
4 min readAug 31, 2017

As developers, we love shortcuts. They make our life easier and create less code. Less code tends to mean fewer chances for new bugs.

For instance, in Android, there is a shortcut for retrieving strings or colours in Context extending classes (like Activities). Instead of calling getResources().getString(resId) we can call getString(resId) directly.

Earlier this year, Google announced that Android Studio will support Kotlin as a first class language, giving us exciting new tools for solving problems.

With Kotlin, we can override operators and “extend” objects with new functions/methods or properties. Allowing us to write code like:

val height = dimens[R.dimen.title_height]

This would be instead of:

val height = getResources().getDimension(R.dimen.title_height)

The second one is not just longer but it also has more elements. Raising the cognitive complexity of the code. There is not a lot of code here, but when looking at a large codebase, each increment in complexity makes a new coders (and original authors after 3 months) take longer time to wrap their heads around it.

How does this magic work?

It’s all a combination of extension functions and operators. The operator that we are using in this instance is get(Int). When overriding this function in an type we can access it by using the [..] operator as if it was an array or map:

class ResourceMapper<out T>(private val mapRes: (resId: Int) -> T) {
operator fun get(@AnyRes resId: Int) = mapRes(resId)
}

This object call and returns the value provided by the mapRes provided function when requested. We can now create a variable in our context like:

val mapper = ResourceMapper { resources.getDimension(it) }

As a side note, for anybody new to Kotlin, if the last argument of a function or constructor is a lambda, the parenthesis is optional and there is not new keyword.

This is ok, but if we have to write this on each of our classes, we aren’t really solving the problem entirely. This is where extension properties come in useful:

val Context.dimens
get() = ResourceMapper { resources.getDimension(it) }

That’s a read-only property that gets “added” to any/all Context (and sub classes). The downside with this is that, each time dimens is accessed a new object is created. Therefore, it’s not recommended if we are doing time sensitive actions like drawing a view.

However, it’s always recommended to prepare variables before doing something inside the onDraw() method of a view.

Formatting a String

This is good for simple calls like getString(), or getDimension(). Some resources like formatted text requires some extra information. For this, we can have an intermediate object that exposes such functionality:

class FormattedString(
private val resources: Resources,
private val resId: Int
) {
operator fun invoke(vararg values: Any): String =
resources.getString(resId, *values)

operator fun invoke(quantity: Int): String =
resources.getQuantityString(resId, quantity)

operator fun invoke(quantity: Int, vararg values: Any): String =
resources.getQuantityString(resId, quantity, *values)
}
val Context.formattedStrings
get() = ResourceMapper { FormattedString(resources, it) }

The invoke() operator represents (...) and allows the object behave like a function (or more if multiple are present). In this case we have 3 different ways to invoke the object depending of whether we require formatting, plurals or both:

formattedStrings[R.string.hello_world]("Hello", "Kotlin")

And it compares with the traditional way as:

getResources().getString(R.string.hello_world, "Hello", "Kotlin")

Not just for Context

If we want to have these extension properties on another place other than a Context (i.e. View or Fragment) we then have to copy and/or reference all the resources we have extended for all the base types.

However, we can create an interface to provide the context with an interface such as:

interface ContextAware {
fun getContext(): Context
}

And add the property extensions to that interface:

val ContextAware.strings get() = getContext().strings

Because both View and Fragment have the method getContext() we can then make them implement ContextAware and they will get all the properties:

class CustomView(context: Context): View(context), ContextAware {
val text = strings[android.R.string.copy]
}
class CustomFragment(): Fragment(), ContextAware {
val text = strings[android.R.string.copy]
}

But I don’t have a Context

If we are on a type that doesn’t have access to a Context, like a Thread or other third party, non Android type, we can still access the oneContext that is always available and cannot be leaked, the Application.

If we make the Application object extend ContextAware:

class CustomApp: Application(), ContextAware {
companion object {
var _instance: CustomApp? = null
val instance: CustomApp
get() = _instance ?:
throw IllegalStateException("CustomApp not created")
}
override fun getContext(): Context = this
override fun onCreate() {
super.onCreate()
_instance = this
}
}

Then we can use it as a delegate:

class Task: ContextAware by CustomApp.instance {
val text = strings[android.R.string.copy]
}

The only thing to be aware of this is that the application Context doesn’t have a theme, so any values that are overridden in a theme will not be available and it’ll fallback to the default.

The rest of resource extensions code can be found here:

https://gist.github.com/pablisco/da25563d57559dd1d18f165272269b57

Feel free to make suggestions or comments here or on Twitter:

--

--