Introduction
Kotlin may be a relatively new language but it's gaining traction fast and unlike some other languages (e.g. Swift) it feels mature with new major versions introducing features whilst always maintaining backwards compatibility. Perhaps this is because the language was built primarily to boost JetBrains' internal teams' productivity or maybe JetBrains are just really good at what they do.
Kotlin offers a lot of improvements over Java such as more concise syntax, null safety, smart casts, operator overloading, extension methods, data classes, etc. all while maintaining seamless interoperability with Java. This rich feature set combined with incredibly simple integration into new or existing projects targeting the JVM means it's really easy to start using Kotlin in your projects.
In the following sections, we're going to focus on just a few of the features of Kotlin which make it our language of choice for Android development and how it makes our applications more robust.
Target Bytecode Version
When incorporated into an Android project, the Kotlin compiler will generate Java 6 compatible bytecode meaning we can target virtually any version of Android. This allows us to use all of the features and APIs of a modern language while still being able to maintain compatibility and support for our users with older devices.
At the time of writing it's possible to use most Java 8 language features (lambdas, method references, try-with-resources, etc.) on any version of Android but it is not possible to use Java 8 APIs such as java.util.stream
or java.util.function
as these require a minimum of API level 24 meaning we lose out on some functional goodness when using the default toolchain.
Extension Methods
One of our favourite features in languages like Objective-C, Swift, C#, etc. is support for extension methods. Extension methods allow us to extend the platform and third party APIs to maximise the readability of our code and abstract away boilerplate. One downside of extension methods in Kotlin is that we can only add instance methods and not class methods. The following example demonstrates how we can add a method to android.content.Context
instances to make conversion from dp units to pixels much simpler anywhere we need to do this programmatically.
fun Context.dpToPixels(dp: Float): Float =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics)
The next example highlights how we can use generics in extension methods to restrict how our extensions can be called. Here we're creating a validate
method which is available on any List
of EditText
objects which simply ensures that all views in the list are non empty. Note we would usually use type inference rather than specify the return type for similar functions but here it's been left in for clarity.
fun List<EditText>.validateAllFieldsAreNotEmpty(): Boolean = all { it.text.isNotEmpty() }
In the Java world, it's common practice to create classes containing static methods to partially emulate extension methods. To ease Java interoperability the Kotlin compiler does this for us when declaring extension methods meaning we can still use our extensions in Java code. Assuming the previous example was in a file named ListExtensions.kt
, the Kotlin compiler creates a Java method with a signature similar to the following.
public class ListExtensionsKt {
public static boolean validate(List<EditText> list) {
...
}
}
Kotlin Android Extensions
Most Android developers will be familiar with findViewById
as a way to inspect and manipulate the view hierarchy and Kotlin provides a helpful set of extensions which allows us to reference the view hierarchy with compile time safety. To enable the Kotlin Android Extensions, it's as simple as enabling the plugin in build.gradle.
apply plugin: "kotlin-android-extensions"
Now, let's assume we have a layout named hello_world_activity.xml which contains the following text view somewhere in the view hierarchy.
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello"
/>
From the corresponding activity we can then reference our text view and its properties like below. The important part here is the kotlinx.android.synthentic
import which allows us to reference the view with ID textView
from our layout hello_world_activity.xml
.
import kotlinx.android.synthetic.main.hello_world_activity.textView
class HelloWorldActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
textView.text = "Hello Kotlin!"
}
}
This may not remove much boilerplate when compared with findViewById
but being able to catch invalid references to view hierarchy objects and verify type references at compile time eliminates a whole class of runtime errors from your Android applications.
Coroutines
A coroutine is basically a block within which we can suspend execution at certain points without blocking the thread where the coroutine is executing. This allows us to write code which handles the result of one or more asynchronous operations sequentially similar to async/await in C#. Suspending a coroutine is almost free with no context switching or OS involvement required and can almost be thought of as a lightweight thread. The following example shows how we can update the UI at a fixed rate delay without blocking the UI thread - compared with an AsyncTask
, this is certainly much simpler. Here we pass a lambda to the coroutine builder function launch
which schedules the block for execution on the UI thread. The delay
function is a suspending function and the point at which the coroutine execution is suspended and the UI thread is released until the delay has passed.
fun countdown(textView: TextView) {
launch(UI) {
for (i in 10 downTo 1) {
textView.text = "Countdown $i ..."
delay(500)
}
textView.text = "Done!"
}
}
The coroutines library also provides support for a variety of APIs for working with asynchronous operations/computations (reactive streams, futures, etc.). One of the supported libraries is RxJava and pairing Rx with coroutines allows for much more flexible combining of observables whilst making conditional logic on the stream of values much simpler. The example below demonstrates how we could use coroutines and Rx to return a Completable
once we've fetched and cached a list of user accounts asynchronously.
fun fetchUserAccounts(): Completable {
return rxCompletable(CommonPool) {
val accounts = api.fetchAllAccounts().await()
cache.addAll(accounts)
}
}
interface Api {
@GET("/accounts")
fun fetchAllAccounts(): Single<List<Account>>
}
There's not a load of code here but there's quite a lot going on, a few interesting points to note:
- The
rxCompletable(CommonPool)
function takes a lambda argument which is the coroutine body and returns an RxCompletable
. If execution of the lambda completes without error, theonComplete
event will be called on the returned completable, if any exception is thrown theonError
event will be called. api
is an instance of a simple Retrofit interface which fetches a list of user accounts asynchronously and returns an RxSingle
with the result. The coroutine library uses an extension method onSingle
here to provide anawait()
function which performs the Rx subscription and allows us to wait for completion inside the coroutine body without blocking a thread. Once the call to fetch accounts has completed, we can continue execution in a sequential manner without the need for any callbacks making our code much easier to reason about.
Kotlin Cross-Platform
While still in its infancy, the Kotlin Native project allows us to build native binaries which do not require a JVM to run. Given that iOS is one of the target platforms, this presents some exciting opportunities for cross-platform mobile development similar to Xamarin. Whilst only a preview at the minute, the KotlinConf iOS application was built using this technology so hopefully it's just a matter of time before a stable release is available.
Kotlin 1.2 also introduces support for multi platform projects which goes even further and allows creation of cross-platform projects with support for shared and platform specific modules. Right now platform support is limited to the JVM, JS and Android but once Native/iOS support is available, the feature set should be on a par with Xamarin.
Conclusion
Kotlin is very quickly becoming our language of choice for server side and Android development - the functional toolkit, seamless Java interoperability, fast build times and increased productivity make it our first choice over Java.
If you'd like to learn more, you can view all our Kotlin related blog posts here. We also run several Kotlin training courses for developer teams, some of which are certified by JetBrains.