Lecture 18 Special Topics: Kotlin
This lecture introduces Kotlin, an open source programming language that runs on the Java Virtual Machine and is fully interoperable with Java Code. It is designed to be a more concise, more flexible, and faster alternative to Java while offering language-level support to features such as null
-checking and functional programming techniques. As of May 2017, Google offers first-class support for using Kotlin when developing Android applications.
- Kotlin was developed by JetBrains, the creators of the IntelliJ IDE which forms the basis for Android Studio.
This lecture references code found at https://github.com/info448-s17/lecture16-kotlin. Kotlin demo code can be found in the kotlin
folder; build and run this code through gradle with the command gradle -q
.
- Note that the provided gradle build script includes the Kotlin standard library for Java 8, making (simplied) versions of Java classes available.
18.1 Kotlin Syntax
The Kotlin language draws many syntactical structures from popular modern languages, including JavaScript and Swift (used for iOS development); I find it has a lot in common with TypeScript.
The full documentation for the Kotlin language can be found at https://kotlinlang.org/docs/reference/; practice and examples are available in the form of Kotlin Koans. This lecture does not aim to be a complete reference but rather to highlight some of the most noticable or interesting aspects of the langauge.
18.1.1 Variables
Kotlin variables are declared by using the keyword var
(for mutable variables) or val
(for read-only variables); this is similar to let
and const
in JavaScript ES5. Like Java, Kotlin is a statically-typed language—the variable type is written after the variablen name, separated by a colon (:
). For example:
val x:Int = 448 //read-only
var stepCount:Int = 9000 //mutable
stepCount++ //can change the variable
val message:String = "Hello" + " " + "world" //supports normal operators
Notice that each of these statements lacks semicolons! Similar to many scripting languages, statements can separated either by a colon or by a newline.
In Kotlin, primitive types (e.g., int
, double
, boolean
) and implemented using classes, allowing for all values to support instance methods and variables (called properties). These classes are named after their Java equivalents (though of course start with a capital letter, since they are classes!)
- The compiler optimizes the types to avoid extraneous overhead.
Additionally, variable type can often be inferred from the assigned value, so may be omitted:
var x = 448 //`Int` type is inferred
x-40 //valid mathematical operation
- Note even without the explicit type declaration, the variable is strongly typed—it’s just that the variable type is determined by the compiler. I encourage you to include the explicit type declaration, particularly when it helps clarify the semantic meaning of the variable.
One of Kotlin’s major features is null safety, or the ability to catch null-pointer errors at compile time. In Kotlin, we need to explicitly declare that a variable can be assigned null
as a value. We do this by including a question mark ?
immediately after the type declaration:
var message:String = "Hello"
message = null //compilation error!
var possiblyNull:String? = "Hello"
possiblyNull = null //no problems!
- Kotlin provides a number of other structures using the
?
to check for and handlenull
values; see Null Safety for details.
Basic Kotlin types work pretty much the same as basic Java types. A few notable differences and features:
Warning: Because of the classes Kotlin uses for numeric types, it is unable to automatically convert to a “larger” type (e.g., from
Int
toDouble
). This casting needs to occur explicitly:val integer:Int = 5 var root:Double = Math.sqrt(integer) //compile type error! //sqrt() takes in a double root = Math.sqrt(integer.toDouble()) //no problems!
Fun feature: in Kotlin, numeric literals can be written with underscores (
_
) as readability separators; the underscores are removed during compile time.val oneMillion:Int = 1_000_000
Fun feature: Kotlin also supports string templating, in which variables used within the String are automatically evaluated (avoiding the need for complex String concatenation). Variables to evaluate are prepended with a dollar sign
$
, while more complex expressions can be evaluated from inside${}
val quantity = 5 val price = 2.95 val cost:String = "$quantity items at $price costs ${quantity*price}"
Note: Arrays in Kotlin are represented by the
Array<T>
class. TheArray<T>
class has a private constructor though; in order to create an array, use thearrayOf()
factory method (to create an array out of the given parameters), orarrayOfNulls()
to create an array of nnull
elements. Note that arrays still support using bracket notation ([]
) for accessing values.var numbers:Array<Int> = arrayOf(1,2,3,4,5) //create an array println(numbers.size) //kotline uses `size` property for array length println(numbers[3]) //4 numbers[4] = 10 //assign value as normal
- Kotlin also provides specialized classes for arrays of basic types, e.g.,
IntArray
- Kotlin also provides specialized classes for arrays of basic types, e.g.,
18.1.2 Functions
Kotlin functions are in some ways closer to JavaScript functions that to Java methods. For one, Kotlin supports package-level functions that do not exist as members of a particular class, as an alternative to static
functions in Java. Thus we can talk about Kotlin functions independent of classes (which are discussed below).
Functions in Kotlin are declared with the fun
keyword (not because they are fun, though they can be). This is folowed by the name of the function and the parameter list (where the parameters are given explicit types). The return type is declared after the parameter list, following a colon (:
). For example:
fun makeFullName(first:String, last:String): String {
val full = first + " " + last
reurn full
}
val fullName = makeFullName("Ada", "Jones")
Java’s void
return type is in Kotlin represented by the singular value Unit
. If a function returns Unit
, it can and should be omitted:
fun sayHello() { //returns Unit, but type omitted
println("Hello world");
}
Kotlin functions also support named default argments: you can provide a default value for an argument in the function declaration. If that (positional) argument is omitted, then the default value is used instead. Additionally, arguments can be specified by name when calling the function, allowing you to include them out of order.
fun greet(personTo:String = "world", personFrom:String = "Me") {
println("Hello $personTo, from $personFrom")
}
greet() //use defaults for arguments
greet("y'all") //use default for second argument
greet(personFrom="Myself", personTo="You") //named arguments (out of order)
Similar to JavaScript arrow functions, Kotlin functions whose body is just a single expression can leave off the block entirely, instead writing the expression to return after an equals sign (=
)—as though that expression were assigned to the function:
//a function square() that takes in an Int and returns that number squared
fun square(n:Int):Int = n*n
- Note that you can omit the return type from single expression functions, as it can be inferred.
This provides a very concise way of writing simple functions!
Similar to JavaScript (and drawing from Java 8), Kotlin supports higher order anonymous functions and lambda functions. These are functions that can be assigned to variables (including function parameter), just like any other object! These are highly effective when using function programming paradigms (e.g., map()
and reudce()
), or when specifying event callback functions.
Anonymous functions are normal functions (whether with a block body or just a single expression), but written without a name. These functions can be assigned to variables, or passed directly to functions that take in appropriate callbacks:
val square = fun(n:Int):Int {
return n*n
}
val numbers:IntArray = intArrayOf(1,2,3,4,5)
println(numbers.contentToString()) //[1, 2, 3, 4, 5]
val squares:List<Int> = numbers.map(square) //transform each element with square()
println(squares.joinToString()) //1, 4, 9, 16, 25
Note that the
square
variable here has an inferred type that is a function which takes in anInt
and returns anInt
. We can explicitly declare this type using the following syntax:val square:(Int) -> Int
The argument types are listed in parentheses, followed by an arrow
->
, followed by the return type.
Anonymous functions that are a single expression can also be written as lambda expressions. Lambda expressions are placed inside curly braces {}
, with the parameter list coming first (not in parentheses). This is followed by a arrow ->
, followed by the expression that should be evaluated.
val add = { x:Int, y:Int -> x+y }
//with an explicit typed (so inferred in the expression)
val add:(Int, Int) -> Int
add = { x,y -> x+y }
In fact, is Kotlin is able to figure out the signature for a lambda function with only a single parameter (e.g., because the lambda is being anonymous passed in as a defined callback function, so must meet the required interface), it will allow us to omit the signature entirely. In this case, the single parameter is automatically assigned to the variable it
:
val numbers:IntArray = intArrayOf(3,1,4,2,5)
//filter() takes a callback with a single parameter that returns boolean,
//so the signature can be inferred
val filtered:List<Int> = numbers.filter( {it > 2} )
println(filtered.joinToString()) //3, 4, 5
This allows us to write somewhat readable code using functional programming approaches. As an example from the documentation:
fruits
.filter { it.startsWith("a") }
.sortedBy { it }
.map { it.toUpperCase() }
.forEach { println(it) }
18.1.3 Classes
Kotlin is an object-oriented language, just like Java. Thus despite being able to support package-level functions, Kotlin is primarily written using classes with attributes and methods.
As in Java, Kotlin classes are declared using the class
keyword. Additionally, most Kotlin classes have a primary constructor, the parameters of which can be included as part of the class declaration.
//declares a class `Dog` whose constructor takes two parameters
class Dog(name:String, breed:String) {
var name = name //assign to properties
val breed = breed
}
- Of course constructor parameters can support default named arguments as well!
- Kotlin properties (instance variables) default to being
public
, but can be declaredprivate
if desired. See Visibility Modifiers. - Note that methods in Kotlin are simply functions (written as above) declared inside the class body.
The primary constructor parameters can directly declare the variables by including either var
or val
as appropriate. Thus a shortcut for the above constructor and initialization is:
//declares instance variables
class Dog(var name:String, val breed:String) {
}
- Additional constructors are specified using the
constructor
keyword as the method name. See the documentation for details.
Objects in Kotlin are instantiated by simply calling the constructor as a normal function, without the new
keyword.
val myDog = Dog("Fido","mutt") //no new keyword!
myDog.name //"Fido"
myDog.breed //"mutt"
As with any other object-oriented language, Kotlin also supports inheritance, with all classes existing within an inheritance hierarchy. Note that in Kotlin, all classes by default inherit from the class Any
, rather than from java.lang.Object
.
We can specify a classes inheritance by putting the parent type after a colon (:
) in the declaration. If the child class has a primary constructor, then we also include the parameters which should be passed to the parent’s constructor.
class Dog(var name:String, val breed:String) : Animal() {
}
- Interfaces are implemented the same way, as part of a comma-separated list of inherited classes. That is, there is no distinction between extending a class and implementing an interface.
Importantly, Kotlin requires us to explicitly mark classes as “subclassable” (they are otherwise compiled to final
classes). We do this by using the open
keyword as an annotation in the class declaration:
//declare class as open
open class Animal(var name:String) { }
//constructor parameters can be passed to parent constructor
class Dog(var name:String) : Animal(name) { }
The open
keyword is also needed if we want to allow a method to be overridden. Additionally, any methods that override a parent version need to be annotated as such with the override
keyword.
open class Animal() {
//function can be overridden
open fun speak() {
println("The animal speaks")
}
}
class Dog() : Animal() {
//overrides parent function
override fun speak() {
println("The dog barks")
}
//overriding toString() using an expression function
override fun toString() = "A dog"
}
Note that Kotlin does not have static
class methods; instead, just use package-elevel functions!
Kotlin also supports a few other special kinds of classes:
Data Classes are declared using the
data
keyword as an annotation in the class declaration. These are classes that do nothing but hold data: they have only attributes, no methods (similar to astruct
in C). The compiler will automatically provide implementations oftoString()
andequals()
for these classes.data class Ball(var x:Int, var y:Int, val color:String) val myBall = Ball(30,30,"red") print(myBall) //Ball(x=30, y=30, color=red)
Nested classes can be made into inner classes (e.g., “non-static” classes with access to the containing class’s instance variables) using the
inner
annotation in the class declaration.Anonymous classes (called Object expressions) can be created with the
object
keyword in place of aclass
and the class name:button.addActionListener(object : ActionListener() { //class definition (including methods to override) goes in here! override fun actionPerformed(ActionEvent event) { //... } });
This is pretty similar to Java, but using object
instead of new
to instantiate the anonymous class.
18.2 Kotlin for Android
Kotlin is built into Android Studio 3.0 by default, but is also available via a plugin for earlier versions. You can install this plugins through the Preferences > Plugins
menu: click the Install JetBrains plugin...
button at the bottom and select Kotlin
. Note that you will need to restart Android Studio.
After you’ve installed the plugin, you can use Android Studio to convert an existing Java file to a Kotlin source code file (.kt
) by selecting Code > Convert Java File to Kotlin File
from the menu. Once you start editing the file, you will be prompted to configure the project to use Kotlin. This will modify the Gradle build scripts to include Kotlin support, allowing you to build and run your app as usual.
For example, we can convert the provided MainActivity
, and consider the changes that have been made:
- The
adapter
instance variable can be null, so its type includes a?
. - Notice how Intents are addressed, particularly the
::class.java
syntax. - For loops use
for ... in
syntax, and Kotlin supports ranges. - You can fix the syntax error with the adapter by explicitly casting the ListView
as
aListView
(this is an example of the converting misunderstanding the code; it happens!) - Static variables are wrapped in a companion object.
This section of the lecture is incomplete.
18.2.1 The Android Extension
https://kotlinlang.org/docs/tutorials/android-plugin.html
To include:
apply plugin: 'kotlin-android-extensions'
To avoid findViewById()
import kotlinx.android.synthetic.main.<layout>.*
18.2.2 Anko
https://github.com/Kotlin/anko
dependencies {
compile "org.jetbrains.anko:anko-commons:$anko_version"
}
Logging: https://github.com/Kotlin/anko/wiki/Anko-Commons-%E2%80%93-Logging
- Avoids the explicit call to
Log
, and uses the class name as the TAG. There are other Java libraries that support easier logging as well.
info("London is the capital of Great Britain")
debug(5) // .toString() method will be executed
warn(null) // "null" will be printed
Intents: https://github.com/Kotlin/anko/wiki/Anko-Commons-%E2%80%93-Intents
startActivity(intentFor<SomeOtherActivity>("id" to 5).singleTop())
//or
startActivity<SomeOtherActivity>("id" to 5)
Toast and Alerts: https://github.com/Kotlin/anko/wiki/Anko-Commons-%E2%80%93-Dialogs
//toasts
toast("Hi there!")
longToast("Wow, such a duration")
//alert
alert("Hi, I'm Roy", "Have you tried turning it off and on again?") {
yesButton { toast("Oh…") }
noButton {}
}.show()