kotlin-workshop

Kotlin Workshop - Chennai

View on GitHub

Program Entry Point


fun main() {
    println("Hello world!")
}

Variables

val a: Int = 1  // immediate assignment
val b = 2   // `Int` type is inferred

var x = 5 // `Int` type is inferred
x += 1 //changed after assignment

/*
Exercise
1. Declare a String constant with initial value "Constant" using explicit type
2. Change the above constant to use type inference
3. Change the above to a variable, update it and print its value.
*/

Types

booleans, numbers, characters, strings, arrays.


val isKotlinFun: Boolean = true
val myInteger = 1
val mCharacter = 'c'
val myString = "string"
val arrayOfIntegers = arrayListOf(1, 2, 3, 4)

Nullability

val x: Int // Error - variable must be initialized
val x: Int = null // Error - null cannot be a value for non-null type Int
val x: Int? = null // OK


//safe call (?)
val name: String? = null
println(name?.length) // prints “null”

//assert (!!) when you know for sure a variable won't be null. if null, causes exception.
var name: String? = null
name = "Treehouse"
println(name!!.length) // prints "9"

department?.head?.name//chaining safe calls

var i: Int? = 10
i?.let { println(it) }//prints i 

i = null
i?.let { println(it) }//doesn't print

//elvis operator
val l = b?.length ?: -1

fun printName(name: String?) {
    val s = name ?: return //if name is null, returns without printing
    println(s)
}

/*
Exercise
1. Create a function that takes two nullable String args and returns a Concatenated String if  
at least one arg is non-null. In all other cases the function returns an empty string.
*/

Functions


//Function without a return type (Unit - equivalent of void in Java)
fun printSum(a: Int, b: Int) {
    println("sum of $a and $b is ${a + b}") //string interpolation
}

//Function with two Int parameters and Int return type:
fun sum(a: Int, b: Int): Int {
    return a + b //no semi-colons are required at the end of statements
}

//Function with an expression body and inferred return type:
fun sum(a: Int, b: Int) = a + b

//Example of a function which returns max of two given ints
fun maxOf(a: Int, b: Int): Int {
    if (a > b) {
        return a
    } else {
        return b
    }
}

// The maxOf function now with an with expression body
fun maxOf(a: Int, b:Int) = if (a > b) a else 

//Function with nullable Int return type. Compiler forces us to handle nulls in consumer code.
fun parseInt(str: String): Int? {
    // ...
}

fun consumer() {
    val invalidInt = parseInt("")
    // compilation fails
    val invalidSum = invalidInt + 2
    // compilation passes
    val validSum = (invalidInt ?: 0) + 2
}

//Default arguments
fun displayGreeting(message: String, name: String = "Guest") {
    println("Hello $name, $message")
}

displayGreeting("Welcome to Kotlin") // Hello Guest, Welcome to Kotlin
displayGreeting("Welcome to Kotlin", "Folks") // Hello Folks, Welcome to Kotlin

//varargs keyword enables us to send an array of a type as a single parameter to a function
fun printNames(vararg names: String): String {
    for(name in names) {
        println(name)
    }
}

/*
Exercise
1. Create a function that takes two Int args and returns 'OE', 'EO', 'OO', EE'  
where O stands for Odd and E for even.

2. Create a function that takes a String arg and a Int args and returns true or false  
based on if the length of given String arg matches the given Int arg

3. Create a function that takes a variable number of Int args and returns the Sum
*/

Type Checks and Casts/SmartCasts: ‘is’ and ‘as’

fun getStringLength(obj: Any): Int? { //Any is a placeholder type for any kind of type. Rarely useful without casting :)
    if (obj !is String) return null

    //What is the compiler is doing smartly here?
    return obj.length
}

//What is the compiler is doing smartly here?
if (x !is String || x.length == 0) return

//"Unsafe" cast operator
val x: String = y as String //if y is null, it causes exception

//"Safe" cast
val x: String? = y as? String

/*
Exercises
1. Create a function that takes an array of mixed types  
(int, boolean, double, char, String) and prints their type

Control flow

//for loop over an array
val items = listOf("apple", "banana", "kiwifruit")
for (item in items) {
    println(item)
}

//for loop over an array with indices
val items = listOf("apple", "banana", "kiwifruit")
for (index in items.indices) {
    println("Item at $index is ${items[index]}")
}

//while loop
val items = listOf("apple", "banana", "kiwifruit")
var index = 0
while (index < items.size) {
    println("item at $index is ${items[index]}")
    index++
}

//Pattern matching with when expression
fun describe(obj: Any): String =
    when (obj) {
        1          -> "One"
        "Hello"    -> "Greeting"
        is Long    -> "Long"
        !is String -> "Not a string"
        else       -> "Unknown"
    }

//when expression with smart casts    
when (x) {
    is Int -> print(x + 1) //smart casts
    is String -> print(x.length + 1)
    is IntArray -> print(x.sum())
}

//range
for (x in 1..5) {
    print(x)
}

//range with step
for (x in 1..10 step 2) {
    print(x)
}

//return: By default returns from the nearest enclosing function or anonymous function.
//break: Terminates the nearest enclosing loop.
//continue: Proceeds to the next step of the nearest enclosing loop.

//break with label
fun foo() {
    loop@ for (i in 1..100) {
    	for (j in 1..100) {
            if (j == 5) break@loop
	    println("i:$i - j:$j")
    	}
    }
}

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return // non-local return directly to the caller of foo()
        print(it)
    }
    println("this point is unreachable")
}

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach loop@{
        if (it == 3) return@loop // local return to the caller of the lambda, i.e. the forEach loop
        print(it)
    }
    print(" done with explicit label")
}

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return@forEach // local return to the caller of the lambda, i.e. the forEach loop
        print(it)
    }
    print(" done with implicit label")
}
/*
Exercise
1. Try to replace if-else-if with when syntax in the following function:
fun foo(name:String, age:Int): Person {
    if(name.length > 10) {
        throw Exception("Really long name!")
    } else if(age > 100) {
        throw Exception("Impossible age!!")
    } else {
        return Person(name, age)
    }
}
*/

Collections

A collection contains multiple items that can be accessed through subscripting.
Kotlin offers three main types of collections: Arrays, Lists, and Maps.


val cardNames: Array = arrayOf("Jack", "Queen", "King")//explicit type
val cardNames = arrayOf("Jack", "Queen", "King")//implicit type

//access element at 0th index
val firstCard = cardNames[0] // firstCard = "Jack"

//replace value at 0th index
cardNames[0] = "Ace"

//list same as array but can't modify items inside a list

//can modify items inside a mutable list
val cards = mutableListOf("Jack", "Queen", "King")

cards.add("Ace") // Jack, Queen, King, Ace
cards.remove("Jack") // Queen, King, Ace
cards.clear() // empty list
cards.addAll("Jack", "Queen", "King", "Ace")

//example
val listWithNulls: List<String?> = listOf("Kotlin", null)
for (item in listWithNulls) {
    item?.let { println(it) } // prints Kotlin and ignores null
}

val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()

//maps
val cards = mapOf("Jack" to 11, "Queen" to 12, "King" to 13)
val jackValue = cards["Jack"] // 11

//to modify values in a map, use mutable map
val mutableCards = mutableMapOf("Jack" to 11, "Queen" to 12, "King" to 13)
cards["Ace"] = 1

//convert map to mutableMap
val mCards = cards.toMutableMap()

//looping a map
val cards = mapOf("Jack" to 11, "Queen" to 12, "King" to 13)
for ((name, value) in cards) {
    println("$name, $value")
}

//use underscore for unused parameter
cards.forEach { _, value -> println("$value!") }

//when
when (cardInt) {
    11 -> println("Jack")
    12 -> println("Queen")
    13 -> println("King")
}

//if returning a value from when expression, it needs to be exhaustive, see the else branch
val cardName = when (cardInt) {
    11 -> "Jack"
    12 -> "Queen"
    13 -> "King"
    else -> "Other"
}

Lambdas

Lambdas are code blocks enclosed in curly braces.


val items = listOf(1, 2, 3, 4, 5)

items.fold(0, { 
    // When a lambda has parameters, they go first, followed by '->'
    acc: Int, i: Int -> 
    val result = acc + i
    // The last expression in a lambda is considered the return value:
    result
})

// Parameter types in a lambda are optional if they can be inferred:
val joinedToString = items.fold(0, { acc, i -> acc + i })

val fruits = listOf("banana", "avocado", "apple", "kiwifruit")
fruits
  .filter { it.startsWith("a") } //it stands for implicit name of a single parameter lambda
  .map { it.toUpperCase() }
  .forEach { println(it) }

//function types
val onClick: () -> Unit = {println("a")}
val onClick: (String) -> Unit = {str -> println(str)}
onClick("AAAA")

//typealias
typealias ClickHandler = (Button, ClickEvent) -> Unit //Unit refers to void/nothing

//a lambda expression
{ a, b -> a + b }

//anonymous function
fun(s: String): Int { return s.toIntOrNull() ?: 0 }

//lambda can access its closure, i.e. variable declared in outer scope
var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
println(sum)

Higher Order Functions

A higher-order function is a function that takes functions as parameters, or returns a function.

val ints = listOf(1, 2, 3, 4)
ints.filter { it % 2 == 0 }.forEach { value ->
    println(value)
}

// Composing functions and return a new function
fun compose(f: (Int) -> String, g: (Double) -> Int): (Double) -> String = { x -> f(g(x)) }