Fluent Intents

pablisco
3 min readSep 14, 2017

--

Intents are an integral part of any app. Thanks to Kotlin, we can now simplify the way we can use them. All driven by Extension Functions. We touched on this back in my post about Extending Resources.

Starting an Activity

Let’s start with the most common case. If we are inside a Context such an Activity, we can start another one in java.

Intent intent = new Intent(this, NextActivity.class);
startActivity(intent);

Using Extensions, we can come to something a bit more readable.

startActivity {
component = componentFor(HomeActivity::class)
}

The higher-order function startActivity takes a lambda which is applied to a new Intent and then calls the original startActivity(Intent).

fun Context.startActivity(f: Intent.() -> Unit): Unit =
Intent().apply(f).run(this::startActivity)

The other part of this solution, componentFor, adds a factory method to the Context. Using the provided Class to create a ComponentName.

fun Context.componentFor(targetType: KClass<*>) =
componentFor(targetType.java)
fun Context.componentFor(targetType: Class<*>) =
ComponentName(this, targetType)

The constructor we used earlier with Java, internally, applies the component name to the Intent.

public Intent(Context packageContext, Class<?> cls) {
mComponent = new ComponentName(packageContext, cls);
}

We are doing the same with startActivity. however, we can simplify this further by adding an inline helper for this:

inline fun <reified T: Activity> Context.start(
noinline f: Intent.() -> Unit = {}
) = startActivity {
component = componentFor(T::class.java)
f(this)
}

Now we can start our activity just by mentioning it.

start<HomeActivity>()

We also get type safety and only allow Activity types, avoiding the embarrassment of trying to start a service like this. We can still use the lambda parameter to add more configuration.

start<HomeActivity> {
categories += CATEGORY_INFO
}

If we want to start the activity with an action instead of a type it can be done too

fun Context.start(a: String, f: Intent.() -> Unit = {}) =
startActivity {
action = a
f(this)
}

Something extra

Typically we add extra values to an intent so we can use them on the other side.

Intent intent = new Intent(this, NextActivity.class)
.setExtra("user_id", userId);
startActivity(intent);

This is how we can tell the system to open an email application.

start(ACTION_SENDTO) {
url = "someone@example.com"
subject = "Subject"
text = "Body"
type = "text/plain"
}

A lot of good extension work is going on here. So, how did we get from having to put the extra manually with a key to a Kotlin property?

Property delegation. The solution is composed of two parts. The first one is a sealed hierarchy of ReadWriteProperty types:

sealed class ExtraProperty<T> : ReadWriteProperty<Intent, T> {  companion object {
fun string(key: String) = StringProperty(key)
// other factory methods
}
class StringProperty internal constructor(
private val name: String
) : ExtraProperty<String>() {

override fun getValue(
thisRef: Intent,
property: KProperty<*>): String =
thisRef.getStringExtra(name)

override fun setValue(
thisRef: Intent,
property: KProperty<*>,
value: String) { thisRef.putExtra(name, value) }

}

// Other types ...
}

Then we can declare the title extension property.

var Intent.text by ExtraProperty.string(EXTRA_TEXT)

And that’s it. Now we can use the property without having to deal with messy and error bound keys. The property works both ways; we can retrieve the title on the other side easily.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
subjectView.text = intent.subject
}

Intent Url

One aspect of the previous example we had that was left unexplained is the url property. When providing a url to an intent we normally have to parse it first.

setData(Uri.parse("someone@example.com"))

With a new extension property we can just add it as a plain url.

inline var Intent.url : String
get() = dataString
set(value) { data = Uri.parse(value) }

In this case, we can use inline (which avoids extra methods) because we are not doing this with Delegation, as in our previous example. If we are worried about method counts, we can rewrite the extra properties example in a similar fashion.

inline var Intent.subject
get() = getStringExtra(EXTRA_SUBJECT)
set(value) { putExtra(EXTRA_SUBJECT, value) }

Slightly more verbose when setting it up, but gets the job done too.

Other components

The same pattern we used for Activity can be used for Services and BroadcastReceivers.

launch<SyncService>()
send("com.example.broadcast.MY_NOTIFICATION")

Here we can’t use start for Services, because of JVM erasure. However, in my opinion, makes it more clear to have separate naming there.

The code backing all the examples in this post is here:
https://gist.github.com/pablisco/64775eba5afa982f4cfb2362aa7bd9b4

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

--

--