When writing code, we should always have a place for improvement. Improvement can present itself in different ways. A proof of it, is the attention to CI (Continuous Integration) and CD (Continuous Delivery) has significantly grown in recent years. Same goes for new languages and technologies.
Nevertheless, this doesn't involve changing to a new language, technology, nor tool. Some of these improvements are so simple as changing how something works, i.e. adding tests/abstractions, changing patterns, etc. This is how I have proceeded in several projects I have worked in.
Disclaimer: Whether you have talked with me in person, or have attended to one of my talks at meetups, it's likely, you already have heard me talking about it, even on YouTube with the GDG Santo Domingo (in spanish) or Kotlin/Everywhere Guadalajara and Mexico City.
Another disclaimer: This article is not about architecture –even though it's a hot topic in the Android community –, best practices or even about how to organize an Android Application. It's just about a set of changes that showed me some Kotlin concepts. Also, I started to implement this aforementioned strategies almost 3 years ago, therefor by the time this article is published, the libraries from Google –like core-ktx
– will probably being updated or improved. And just to be clear: It's good to mention that the approaches mentioned in the following lines don't apply to all scenarios nor cases; the solutions mentioned in this article depend on scenarios or needs themselves, besides individual reasons to choose them over the others.
But first… some Context
If you're an Android Developer or have worked with Android, you probably caught the pun intended by this title. If not, let me explain it to you quickly.
In Android development, everything happening on your application has a Context
and to make some stuff work, you need to deal with a Context
as well. This is no more than a class that Android uses to provide information and access to the app.
One specific case arises at the time it's required to start a new Activity
on your app. This means -succinctly- launching a whole new screen or an external application. In order to accomplish it, firstly you should create an Intent
based on your Context
and pass the Activity
you would like to launch:
val myIntent = Intent(context, MyNextActivity::class.java)
Then you can start MyNextActivity
from another Activity
simply using this intent: startActivity(myIntent)
If your application relies on activities for showing varied sections or views of your application, then probably you will end up with a lot of these calls distributed around. All of them with the same structure yet with several variants, such as:
val someIntent = Intent(context, SomeActivity::class.java)
//Do stuff with the intent
startActivity(someIntent)
//And if you don't do things with the intent and you are in an activity
startActivity(
Intent(this, MyNextActivity::class.java)
)
My first problem
To be honest, there is no problem with this code. Not at all. But I have seen this happening in a lot of applications, firstly you have these calls distributed around without messed around, then you suddenly have 2 or 3 repetitions of the same work in different places, setting the same Intent
with a different variable name but same parameters and then doing the same call. To solve these, most people usually do one of two things:
- Wrap it around a single method for each case
- Put all under a single class/object and just pass needed parameters for repeated cases
This means, for case 1:
/** Inside your class that will have the calls **/
fun startNext() {
startActivity(
Intent(this, MyNextActivity::class.java)
)
}
fun startSome() {
val someIntent = Intent(this, SomeActivity::class.java)
//Do things with intent
startActivity(someIntent)
}
And for case 2:
object AppRouter {
fun startNext(context: Context) {
startActivity(
Intent(context, MyNextActivity::class.java)
)
}
fun startSome(context: Context) {
val someIntent = Intent(context, SomeActivity::class.java)
//Do things with intent
startActivity(someIntent)
}
}
You can add several layers of complexity, but this is the base for most solutions: have a single point for launching/starting new activities. In my case, I always ended up having case 1 all around. Having functions here and there seemed like a good solution, until I ended with 120 functions doing almost the same all around, which is not bad, but seeing that ::class.java
even inside of different methods really bothered me. Plus, seeing 3 or 4 functions inside the same class with almost the same body also feels wrong.
I wanted a way to remove that, to make that ::class.java
disappear from everywhere and have something more explicit or at least more readable without the 123193 wrapper functions all around.
Generics
Coming from Java Land and seeing that all my function looked the same with one single parameter changed made me thing: "I can do better than this". So I did:
fun <T> launchActivity(context: Context, clazz: Class<T>) {
val intent = Intent(context, clazz)
startActivity(intent)
}
First problem solved: We have a single function to start our activities and we can leave it as a global function. As long as someone has access to a Context
it can make use of this function. But…
- Uncle Bob says that the less parameters, the better the function –and it's true, is easier to use it–
- The
::class.java
will still be present all around - If we want to hide the
::class.java
and provide a more "business" related abstraction we still have a lot of functions
fun startNext(context: Context) {
launchActivity(context, MyNextActivity::class.java)
}
fun startSome(context: Context) {
launchActivity(context, SomeActivity::class.java)
}
And seeing this… well, the initial stuff that triggered me was the whole ::class.java
and is still present, this is not a great improvement…
Inheritance
Well, having this global function also feels kind of wrong, and almost all the places using the function will be some kind of Activity
. Maybe we can make the function available through inheritance…
open class BaseLaunchActivity: Activity() {
fun <T> launchActivity(clazz: Class<T>) {
val intent = Intent(this, clazz)
startActivity(intent)
}
}
//And then…
class MyActivity : BaseLaunchActivity() {
fun onSomeClick() {
launchActivity(SomeActivity::class.java)
}
}
Cool! We got rid of one parameter, we hide where the magic is happening and made the function available to most of the parts using it.
But wait… most? We want all, not most… also this has other issues:
- It is not always possible to inherit
- Not all the classes in need of the code will be a kind of
Activity
- When some class has access to
Activity
it will need to be cast in an ugly way -
::class.java
is still present –ugh, it's ugly!
For the casting part we will have:
(getActivity() as BaseLaunchActivity).launchActivity(SomeActivity::class.java)
Which is TURBO-SUPER-ULTRA DANGEROUS –yes, I'm over reacting– because you can't be sure that from now and all the time, this cast will be successful. Why? Well, your class/component is depending on a container and the class itself does not have a mechanism to check all the time which type the container has –and even if it has it, it shouldn't be designed in that way– also if your functionality depends on the container being of some type… well, that's bad design.
Extension functions to the rescue
Depending on inheritance for this simply functionality seems like producing more problems. Other approach will be using an interface:
interface ActivityLauncher {
fun <T> launchActivity(context: Context, clazz: Class<T>) {
val intent = Intent(context, clazz)
startActivity(intent)
}
}
But we got back to having 2 parameters… BUT on interfaces we can enforce properties:
interface ActivityLauncher {
val launcherContext: Context
fun <T> launchActivity(clazz: Class<T>) {
val intent = Intent(launcherContext, clazz)
startActivity(intent)
}
}
But now every class that needs this function will need to both:
- Implement the interface
- Give a value to the
launcherContext
property
That's boring!!
What about a single extension function to remove the dependency over the property?
fun <T> Context.launchActivity(clazz: Class<T>) {
val intent = Intent(this, clazz)
startActivity(intent)
}
That's actually cool!
Now whatever Context
existing around can simply invoke this function without issues, we no longer need wrapper functions but… the ::class.java
has not moved. Arghhh… that thing is making our code look ugly…
And we have a bigger problem… turns out that for this small function, the Kotlin compiler will generate some bytecode that once existing in the JVM will look approximately –on java– like this:
public final class ContextExtensions {
public static final void launchActivity(@NotNull Context context, @NotNull Class clazz) {
Intent intent = new Intent(context, clazz);
startActivity(intent);
}
}
This is not bad but is a caveat of having Kotlin extension functions and… well, if it is only to make things readable, we are not making too much progress: we only removed 1 line of code from our original block of code. What can save us from actually having this on the bytecode and still adding readability to our codebase?
INLINE
Adding a single reserved word to our function will make everything cool on the bytecode:
inline fun <T> Context.launchActivity(clazz: Class<T>) {
val intent = Intent(this, clazz)
startActivity(intent)
}
But if you are writing this on AndroidStudio or IntelliJ probably you'll notice a warning explaining that there's no actual optimization from inlining this function. And that's true, inline
works as an SMART-COPY-PASTE which mean it will do magic on compile time to paste the body of your function where is being called with actual params and stuff.
Talking about params… what about the Intent
? We usually pass information between activities using the Intent
right? With the current approach is not possible to pass any parameter… let's fix that first and we then can check the warning from the inline
.
Lambda with receiver
Because we don't want to increment the size of our parameter list, because the types of parameters can grow and because we don't want to have multiple wrapper functions just for the parameters, we need to find a way to "configure" our Intent
on demand.
The lambda with receiver is one way to fix this. Adding this parameter to our function we can allow each caller to configure during execution of our function the Intent
and still keep things readable:
inline fun <T> Context.launchActivity(clazz: Class<T>, confBlock: Intent.() -> Unit) {
val intent = Intent(this, clazz)
intent.confBlock()
startActivity(intent)
}
Adding this last parameter we are saying that our function can take a lambda that will execute in the scope of an Intent
and can use properties and methods from the Intent
object. This lambda is later used to set up our Intent
and finally we start the activity with the already configured Intent
.
This change allows the execution to be something like:
fun onSomeInteraction() {
launchActivity(SomeActivity::class.java) {
putExtra(MY_KEY, myValue)
putExtra(SOME_KEY, someValue)
}
}
Also, the warning about inline
disappears with these changes as the optimization now makes sense. In compile time, the compiler will take the call to our function and replace it with the actual body of the function and replacing the lambda with a local variable. This will increase a little bit the size of our bytecode but gave us more readability for usages.
The bytecode for this call on Java will look something like:
public void onSomeInteraction() {
Intent intent = new Intent(this, SomeActivity.class);
intent.putExtra(MY_KEY, myValue);
intent.putExtra(SOME_KEY, someValue);
startActivity(intent);
}
As you can see, no reference to external classes, no calls to external functions, all living there. This is a good thing, as the code stays simple while coding and has the real meaning when executing.
But still… while coding, the ::class.java
is present. Why have I been complaining about this if it is something present in Kotlin? Well, the main reason is because this operator and class is not nice, it's quite ugly and also can be confusing. There is more than one way to obtain a Class<T>
from a class and this one is the easier one. There's a chance for error using the other ones and they are verbose, not what we are trying to avoid here.
Reified for your types
We are currently having a generic – <T>
– function that is inlined, these two elements can be combined to use a third element on our function: reified
:
inline fun <reified Ty> Context.launchActivity(clazz: Class<T>, confBlock: Intent.() -> Unit) {
val intent = Intent(this, clazz)
intent.confBlock()
startActivity(intent)
}
Adding this word will means that for every single call of our function, when inlining, the compiler will use the actual type instead of a generic or a super type in the caller –I'll go deeper on reified
on next articles. This small change allows us to use the generic inside of our function as a real type, then we can remove the first parameter and replace with the real type:
inline fun <reified T> Context.launchActivity(confBlock: Intent.() -> Unit) {
val intent = Intent(this, T::class.java)
intent.confBlock()
startActivity(intent)
}
Now all the calls to this function can be as clean as:
launchActivity<SomeActivity> {
putExtra(MY_KEY, myValue)
}
launchActivity<MyNextActivity> {}
Some final touches to the signature will make the function even safer and even cooler when being called:
- Make
T
have a constraint toActivity
in a way only classes that extend fromActivity
are allowed - Add a default value to the lambda with receiver to allow calls with an empty configuration
inline fun <reified T: Activity> Context.launchActivity(confBlock: Intent.() -> Unit = {}) {
...
}
Now our function is cooler than ever, won't add too much to the final bytecode and also will make our code more readable. Wow! We traveled a long way doing a lot of changes to a single piece of code to make things cooler, but not just that, now other people developing along with us can benefit from this improvement as they can simply call our function without having to worry how it works. I have seen even people adding animations, making a way to hold results and transforming data using these kind of functions/approaches. Writing idiomatic Kotlin actually makes you think and develop different and cooler!
Conclusion
We can apply this kind of approach by steps or go directly to other functions on our codebase, one example is the usage of SharedPreferences
and making the edition do an auto-apply "magically" using the lambda with receiver. We can create ways to configure more stuff in less steps and this will make our writing of code improve as we will be adding tools to our already-vast set of functions and tools on Kotlin. Thinking about what is repeated or can be improved allows us –the developers– to discover new worlds and make things cooler and sharper. Don't be scared of change.
Special thanks to my friends @annaelizleal, @jhoon and Alejandro Tellez, they help me a lot with grammar, typos and fixing styles. And special thanks to you for reading.
Top comments (0)