Kotlin Android development - 6 months into it
We've been developing Android applications for a while now here in Karumi. With the announcement of official support for Kotlin in this platform last year, we decided to push forward in this direction. Kotlin has a lot to offer, from tons of syntactic sugar to a more expressive type system than Java. We are bringing you some insights we gained in the process of creating safer applications and how to be more productive writing them.
The following text is our opinion on the state of the art and we are probably going to find better ways to write our software the more we code, but that's the point, isn't it? Let's get started then!
Prerequisites: We are assuming you worked with Android, Kotlin, and some Clean Architecture because that's our context. If you don't know any of those topics, you can still try and read it at your own risk (we are linking most of the new concepts just in case), but you are most probably going to miss some essential ideas in the way.
Safe when statements
One of the first things we noticed and wanted to test were Kotlin sealed classes. More specifically, we wanted to see if the compiler was able to verify if we were handling all cases or there were missing branches in our code. We found out that when using when expressions to deal with all scenarios, we sometimes would get a compiler error when one of them was left out and other times we wouldn't. What a strange behavior!
After some reading, we discovered this happens depending on how when
was being used. If your when
use is returning a value, then it's considered an expression. For the type checker to verify that your program is correct, it expects you are going to consider all different possibilities (in the end, it has to verify that all the types returned in every branch of your when
expression matches the type of the consuming part). What about when
statements then? Well, the Kotlin team decided not to check for exhaustiveness in that scenario, they didn't want to force developers to write an empty else
branch.
If you aren't comfortable leaving those potential branches unhandled, just like us, we ended up using a powerful extension that transforms every when statement into a when expression. How so? By just "using" the value returned by when
.
val Any?.exhaustive get() = Unit
Sure, it looks somehow wrong, one of those things you try not to think about it too much, more on that later. The benefits being that thanks to this extension, we feel more confident of our code. Here is an example of how we use it, imagine a view listening to changes on a view model:
private fun onStatusChanged(status: Status) {
when (status) {
is Status.Idle -> // ...
is Status.Fetching -> // ...
is Status.Fetched -> // ...
}.exhaustive
}
Thanks to exhaustive
we can be sure we won't forget about handling a new Status.FetchError
scenario in case we create it afterward and that's well worth the ugliness of such extension.
Edit: Some people suggested me to just use return when
or change the method signature to return Unit
. There are scenarios where that's just not possible, though and we wanted a generic solution to cover cases such as:
private fun onStatusChanged(status: Status): Boolean {
when (status) {
is Status.Idle -> {
loadingView.visibility = GONE
hideItems()
}
is Status.Fetching -> loadingView.visibility = VISIBLE
is Status.Fetched -> {
loadingView.visibility = GONE
showItems()
}
}.exhaustive
return doSomethingElse()
}
In this new example, we can't really return the when
expression for two reasons:
- We may want to do some further computation after the
when
statement. - We need to return a value.
The exhaustive
function just simplifies all those scenarios and work in all cases. As I've been pointed out there is already a proposal to add this feature! https://youtrack.jetbrains.com/issue/KT-12380
Limited extensions
We just presented you a global extension. We call it global in two different ways:
- It's an extension of the
Any?
type, that means any Kotlin class will be able to use it (it won't do much, though). - It is accessible from any point in your code as long as you import it.
We can't do anything about the former: we can't know what a when
expression will return and the lack of meta-programming (and more specifically, macros), leave us with little room for improvement here. About the second point, this is something we often find annoying, why would I have methods to treat String
types like URI
? Not every String value is meant to represent a URI, and the same applies to many other extensions. The problem is, now all Strings will have all those functions to use. What's the limit here? We've seen people abusing it to no end, creating users with their id by creating String
extensions, crazy, isn't it?
We now follow one single rule: if the extension is not applicable to all possible values of a type, we create that extension inside an interface, and it's the calling class responsibility to implement that interface (more like a trait, actually) to declare what are its intentions explicitly. Let's see an example to make it more clear.
There are some cases where we want to reference Android string resources from our presentation layer, where we don't have a context available. For those scenarios, we created a data class that holds all the information we might need to transform the resources into a translated string later on. This is a possible implementation of such class:
data class StringResource(
@StringRes val resource: Int,
private val arguments: Array<out String> = emptyArray()
) {
fun resolve(context: Context): String =
context.getString(resource, *arguments)
}
Now imagine we want to be able to create StringResource
values directly from its Int value, our first approach would be just to declare:
val Int.stringResource: StringResource
get() = StringResource(this)
fun Int.toStringResource(vararg arguments: String): StringResource =
StringResource(this, arguments)
But that would mean we would potentially make ANY integer value a string resource, doesn't it sound wrong? By following our rule, we end up creating that very same extension, but this time, inside an interface:
interface StringResourceCreator {
val Int.stringResource: StringResource
get() = StringResource(this)
fun Int.toStringResource(vararg arguments: String): StringResource =
StringResource(this, arguments)
}
Now, not everyone will be able to see or use the new extensions, only those implementing StringResourceCreator
will, and that's great because we are not polluting the global Int
namespace anymore!
object SomeMapper : StringResourceCreator {
fun toWelcomeMessage(numberOfFriends: Int): StringResource = when (numberOfFriends) {
0 -> R.string.welcoming_lonely_user
1 -> R.string.welcoming_not_so_lonely_user
else -> R.string.welcoming_friendly_user
}.stringResource
}
Operators
Kotlin operator overloading feature is one of the most pleasant things we've been playing with lately. Just like every other language feature, there are places where it will make your life easier and others where it just won't. Operator overloading is hard to get right, the common-sense rule here is to use them only when there is an already established operator for it, and everyone in the team agrees that it's easy to understand. If you don't follow that rule (which is entirely subjective anyway) things will go out of hand pretty quickly. We won't tell you when to use an operator or a regular function but here is a list of places where we found it useful.
Use cases
According to Clean Architecture, use cases (or interactors if you prefer) are just a declaration of what your domain can do; they are usually named using verbs such as GetUserInformation
or AddItemToCart
. In our case, use cases are just an implementation of an old design pattern called the command pattern. Following this pattern, commands are just classes with a single method called execute
. It almost looks like Kotlin developers were thinking about this specific case for the invoke
operator, at least a broader concept they already addressed in Java with functional interfaces and in Scala with the apply
function. This is how our use cases look like:
class GetCurrentUser(private val currentUser: CurrentUser) {
operator fun invoke(): CurrentUser? = currentUser.get()
}
class Presenter(private val getCurrentUser: GetCurrentUser) {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
val currentUser = getCurrentUser()
// ...
}
}
The change is minimal, but it's somewhat satisfying to use.
Subscriptions
There is a particular category of use cases we often use in Karumi; it's subscription use cases where some presenter registers itself to changes in the domain to update its view. The classic scenario is to deal with those cases where the user updates some information in a detail page, and the previous items list screen wants to update its data as well.
In those cases, the regular invoke
operator might be an acceptable solution but would force us to create two different use cases, one to subscribe to events and a different one to unsubscribing. The thing is, those use cases will always be together, there is no compelling reason for a client class to subscribe to domain events and not cleaning its reference up when it's done (besides neverending subscriptions that are not so common, from our experience). For those cases, we are using the plusAssign
and minusAssign
operators instead. Honestly, we didn't invent it, .NET (more specifically, C#) developers have been using it for some time.
Here is how it looks in our Kotlin applications:
class CartItemSubscription(private val notificationCenter: CartItemsNotificationCenter) {
operator fun plusAssign(subscriber: CartItemNotificationCenter.Subscriber) {
notificationCenter.subscribers.add(subscriber)
}
operator fun minusAssign(subscriber: CartItemNotificationCenter.Subscriber) {
notificationCenter.subscribers.remove(subscriber)
}
}
class CartItemsPresenter(private val cartItemSubscription: CartItemSubscription) {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
cartItemSubscription += this
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
cartItemSubscription -= this
}
}
Repositories
The repository pattern is one of the many patterns we end up using more frequently in our projects when implementing Clean Architecture. I won't go into details on how it should be implemented nor if its responsibilities lie on the domain or data layer.
This is one of the first uses of patterns we came upon. Given the very nature of repositories as containers of things, the get
and set
operators are logical. Do you need to get an item by its id? Do you want to use more parameters? No worries, get
and set
to the rescue, here is an example.
class SuperHeroesRepository {
operator fun get(id: String): SuperHero? { /*...*/ }
operator fun get(faction: Faction): List<SuperHero> { /*...*/ }
operator fun set(id: String, superHero: SuperHero) { /*...*/ }
}
val repository = SuperHeroesRepository()
val spiderman = repository["SpiderMan"]
val avengers = repository[Faction.Avengers]
repository["IronMan"] = avengers.first(::hasTooMuchEgo)
Object extensions
Our last example of the usage of operator overloading is something we generally wouldn't recommend you to do because its usage is counter-intuitive, especially if you are starting to read and write some Kotlin. Remember the invoke
operator we discussed before? Well, operator overloading is not limited to instance methods, we can overload operators in objects as well! What can we do with such powerful feature? Nothing good, that's for sure.
The thing is, we are using a popular dependency injector for our projects called Kodein. As every other DI library, it lets you define dependency modules to better separate your dependencies according to your criteria. The problem is that the Kodein.Module
class is not open, and therefore we can't create subclasses and take advantage of polymorphism. We are then forced to create methods returning the different modules of our app:
object ComicModule {
fun get(): Kodein.Module { /*...*/ }
}
object ComicStoreModule {
fun get(): Kodein.Module { /*...*/ }
}
val myModules: List<Kodein.Module> = listOf(ComicModule.get(), ComicStoreModule.get())
But we always need to do that weird call to get()
... If that doesn't irk you in the least, then you are good to go, skip to the next topic. However, if you are like us, we found that you can use the invoke
operator on objects as well!
object ComicModule {
operator fun invoke(): Kodein.Module { /*...*/ }
}
object ComicStoreModule {
operator fun invoke(): Kodein.Module { /*...*/ }
}
val myModules: List<Kodein.Module> = listOf(ComicModule(), ComicStoreModule())
If you are wondering if this is like cheating on Kotlin, then you are probably right. Do not use this technique ever, unless you are 100% positive there won't be confusions about what is that code doing, for anyone. It's really hard for any sane developer to understand that what they are executing there is not the ComicModule
constructor but a custom invoke
method. It's cool though.
Asynchronous execution
We are very excited about Kotlin coroutines. We've been thinking for a while where is the best place to use them. The caveat here is that when you create a suspend
function you are forcing the caller to either call you in a coroutine context or to be a suspend
ed function as well.
With that in mind, we want to deal with asynchronicity as soon as possible, that is, as close to user interaction. If you do it in use cases you'd breaking the command pattern; commands are not expected to know anything about the context where they are going to be executed, that includes if the use case itself is asynchronous or not. There are two other layers we might want to do the context switch, that is in the UI or the presentation layer. We ended up doing the change in our presenters because we are used to implementing MVP with a passive view approach. That means that none of the public methods of the presenter are returning anything. That sounds about right for our suspended functions! This approach also lets the responsibility of how to execute use cases up to the presenter: All our use cases are synchronous, and it's the presenter's responsibility to deal with them in a different context, execute them in parallel, sequentially or do whatever they want to do with them.
Testing asynchronous execution
To test asynchronous calls, we require our presenters to always ask for a CoroutineContext
so that we can change its implementation by something that runs everything in the main thread (something Espresso will thank you for). That's all good for regular showListOfFriends
and showErrorSnackbar
tests but what about UI tests that are meant to verify what happens while something is executing in the background such as loading views? We are going to need to hold the execution of those asynchronous operations... indefinitely.
Luckily for us, there is a HandlerContext
that is (transitively) a subclass of CoroutineContext
, yay! We can create our own Handler
implementation that sends all "asynchronous calls" to the main thread (for Espresso reasons) and holds others forever so that we can test transition states. Here is a basic implementation of such handler:
class BlockingHandler : Handler(Looper.getMainLooper()) {
private var callIndex: Int = 0
var shouldBlockCall: (index: Int) -> Boolean = { false }
override fun dispatchMessage(msg: Message?) {
if (!shouldBlockCall(callIndex)) {
super.dispatchMessage(msg)
}
callIndex += 1
}
}
If we want to block the execution of the nth background call we have to hold a reference to the handler and modify it's blocking predicate. Or if we don't want such fine-grained control we can just ignore any asynchronous call:
blockingHandler.shouldBlockCall = { true } // Ignore all asynchronous calls
blockingHandler.shouldBlockCall = { it == 3 } // Ignore only call number 3
Farewell
We reviewed several techniques we've been working in Karumi when dealing with Kotlin + Android projects. Keep in mind that we will continue learning and evolving our way of working in this environment. Therefore we may find some of these topics outdated at some point in time, we understand it, and we embrace it. We only hope you can find some useful tips here and if you didn't, at least find some inspiration to find your own.