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: