Kotlin is really easy to learn for Android developers because the syntax is similar to Java and Kotlin often feels like a natural Java evolution. At the beginning, a developer usually writes Kotlin code by having in mind habits from Java, but after a while, it is very easy to move to more idiomatic Kotlin solutions. Let's look at some cool Kotlin features and see where Kotlin may provide benefits by solving common programming tasks in an easier, more concise, and more flexible way. We have tried to keep examples simple and self-explanatory, but they utilize content from various parts of this book, so it's fine if they are not fully understood at this point. The goal of this section is to focus on the possibilities and present what can be achieved by using Kotlin. This section does not necessarily need to fully describe how to achieve it. Let's start with a variable declaration:
var name = "Igor" // Inferred type is String
name = "Marcin"
Notice that Kotlin does not require semicolons. You can still use them, but they are optional. We also don't need to specify a variable type because it's inferred from the context. Each time, the compiler can figure out the type from the context; we don't have to explicitly specify it. Kotlin is a strongly typed language, so each variable has an adequate type:
var name = "Igor"
name = 2 // Error, because name type is String
The variable has an inferred String type, so assigning a different value (integer) will result in a compilation error. Now, let's see how Kotlin improves the way to add multiple strings using string templates:
val name = "Marcin"
println("My name is $name") // Prints: My name is Marcin
We need no more joining strings using the + character. In Kotlin, we can easily incorporate single variable or even whole expressions, into string literals:
val name = "Igor"
println("My name is ${name.toUpperCase()}")
// Prints: My name is IGOR
In Java, any variable can store null values. In Kotlin, strict null safety forces us to explicitly mark each variables, that can store nullable values:
var a: String = "abc"
a = null // compilation error
var b: String? = "abc"
b = null // It is correct
By adding a question mark to a data type (string versus string?), we say that the variable can be nullable (can store null references). If we don't mark the variable as nullable, we will not be able to assign a nullable reference to it. Kotlin also allows us to deal with nullable variables in proper ways. We can use the safe call operator to safely call methods on potentially nullable variables:
savedInstanceState?.doSomething
The method doSomething will be invoked only if savedInstanceState has a non-null value, otherwise the method call will be ignored. This is Kotlin's safe way to avoid null pointer exceptions that are so common in Java.
Kotlin also has several new data types. Let's look at the Range data type that allows us to define end inclusive ranges:
for (i in 1..10) {
print(i)
} // 12345678910
Kotlin introduces the Pair data type that, combined with infix notation, allows us to hold a common pair of values:
val capitol = "England" to "London"
println(capitol.first) // Prints: England
println(capitol.second) // Prints: London
We can deconstruct it into separate variables using destructive declarations:
val (country, city) = capitol
println(country) // Prints: England
println(city) // Prints: London
We can even iterate through a list of pairs:
val capitols = listOf("England" to "London", "Poland" to "Warsaw")
for ((country, city) in capitols) {
println("Capitol of $country is $city")
}
// Prints:
// Capitol of England is London
// Capitol of Poland is Warsaw
Alternatively, we can use the forEach function:
val capitols = listOf("England" to "London", "Poland" to "Warsaw")
capitols.forEach { (country, city) ->
println("Capitol of $country is $city")
}
Note that Kotlin distinguishes between mutable and immutable collections by providing a set of interfaces and helper methods (List versus MutableList, Set versus Set versus MutableSet, Map versus MutableMap, and so on):
val list = listOf(1, 2, 3, 4, 5, 6) // Inferred type is List
val mutableList = mutableListOf(1, 2, 3, 4, 5, 6)
// Inferred type is MutableList
Immutable collection means that the collection state can't change after initialization (we can't add/remove items). Mutable collection (quite obviously) means that the state can change.
With lambda expressions, we can use the Android framework build in a very concise way:
view.setOnClickListener {
println("Click")
}
The Kotlin standard library (stdlib) contains many functions that allow us to perform operations on collections in a simple and concise way. We can easily perform stream processing on lists:
val text = capitols.map { (country, _) -> country.toUpperCase() }
.onEach { println(it) }
.filter { it.startsWith("P") }
.joinToString (prefix = "Countries prefix P:")
// Prints: ENGLAND POLAND
println(text) // Prints: Countries prefix P: POLAND
.joinToString (prefix = "Countries prefix P:")
Notice that we don't have to pass parameters to a lambda. We can also define our own lambdas that will allow us to write code in a completely new way. This lambda will allow us to run a particular piece of code only in Android Marshmallow or newer:
inline fun supportsMarshmallow(code: () -> Unit) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
code()
}
//usage
supportsMarshmallow {
println("This code will only run on Android Nougat and newer")
}
We can make asynchronous requests easily and display responses on the main thread using the doAsync function:
doAsync {
var result = runLongTask() // runs on background thread
uiThread {
toast(result) // run on main thread
}
}
Smart casts allow us to write code without performing redundant casting:
if (x is String) {
print(x.length) // x is automatically casted to String
}
x.length //error, x is not casted to a String outside if block
if (x !is String)
return
x.length // x is automatically casted to String
The Kotlin compiler knows that the variable x is of the type String after performing a check, so it will automatically cast it to the String type, allowing it to call all methods and access all properties of the String class without any explicit casts.
Sometimes, we have a simple function that returns the value of a single expression. In this case, we can use a function with an expression body to shorten the syntax:
fun sum(a: Int, b: Int) = a + b
println (sum(2 + 4)) // Prints: 6
Using default argument syntax, we can define the default value for each function argument and call it in various ways:
fun printMessage(product: String, amount: Int = 0,
name: String = "Anonymous") {
println("$name has $amount $product")
}
printMessage("oranges") // Prints: Anonymous has 0 oranges
printMessage("oranges", 10) // Prints: Anonymous has 10 oranges
printMessage("oranges", 10, "Johny")
// Prints: Johny has 10 oranges
The only limitation is that we need to supply all arguments without default values. We can also use named argument syntax to specify function arguments:
printMessage("oranges", name = "Bill")
This also increases readability when invoking the function with multiple parameters in the function call.
The data classes give a very easy way to define and operate on classes from the data model. To define a proper data class, we will use the data modifier before the class name:
data class Ball(var size:Int, val color:String)
val ball = Ball(12, "Red")
println(ball) // Prints: Ball(size=12, color=Red)
Notice that we have a really nice, human readable string representation of the class instance and we do not need the new keyword to instantiate the class. We can also easily create a custom copy of the class:
val ball = Ball(12, "Red")
println(ball) // prints: Ball(size=12, color=Red)
val smallBall = ball.copy(size = 3)
println(smallBall) // prints: Ball(size=3, color=Red)
smallBall.size++
println(smallBall) // prints: Ball(size=4, color=Red)
println(ball) // prints: Ball(size=12, color=Red)
The preceding constructs make working with immutable objects very easy and convenient.
One of the best features in Kotlin are extensions. They allow us to add new behavior (a method or property) to an existing class without changing its implementation. Sometimes when you work with a library or framework, you would like to have an extra method or property for a certain class. Extensions are a great way to add those missing members. Extensions reduce code verbosity and remove the need to use utility functions known from Java (for example, the StringUtils class). We can easily define extensions for custom classes, third-party libraries, or even Android framework classes. First of all, ImageView does not have the ability to load images from a network, so we can add the loadImage extension method to load images using the Picasso library (an image loading library for Android):
fun ImageView.loadUrl(url: String) {
Picasso.with(context).load(url).into(this)
}
//usage
imageView.loadUrl("www.test.com\\image1.png")
We can also add a simple method displaying toasts to the Activity class:
fun Context.toast(text:String) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
}
//usage (inside Activity class)
toast("Hello")
There are many places where usage of extensions will make our code simpler and more concise. Using Kotlin, we can fully take advantage of lambdas to simplify Kotlin code even more.
Interfaces in Kotlin can have default implementations as long as they don't hold any state:
interface BasicData {
val email:String
val name:String
get() = email.substringBefore("@")
}
In Android, there are many applications where we want to delay object initialization until it is needed (used). To solve this problem, we can use delegates:
val retrofit by lazy {
Retrofit.Builder()
.baseUrl("https://www.github.com")
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
Retrofit (a popular Android networking framework) property initialization will be delayed until the value is accessed for the first time. Lazy initialization may result in faster Android application startup times, since loading is deferred to when the variable is accessed. This is a great way to initialize multiple objects inside a class, especially when not all of them are always needed (for certain class usage scenarios, we may need only specific objects) or when not every one of them is needed instantly after class creation.
All the presented examples are only a glimpse of what can be accomplished with Kotlin. We will learn how to utilize the power of Kotlin throughout this book.