Kotlin for Dummies: Learn Kotlin in 30 minutes

Azmi Rutkay Biyik
14 min readNov 15, 2021

--

Extended Introduction to Kotlin Programming Language

About the Notes

I have been developing mobile apps for more than 6 years and I’ve been developing apps by using cross-platform frameworks for more than 2 years. Now I am getting familiar with Kotlin along with the Android Programming with Kotlin.

I’ve prepared these notes for myself when I first started learning the Kotlin programming language. You can think of these notes as an extended introduction to Kotlin.

I prepared the notes in Notion — I strongly suggest you use Notion to take your notes in a structured way, it is amazing — for my private usage and now sharing with you here.

You will find these topics in the notes:

  • Data Types: Mutuable and Read-Only Variables, Type Casting, Null-safety, Numbers, Characters, Strings, Booleans, Arrays
  • Operators: Arithmetic, Relational, Assignment, Unary, Logical, Bitwise
  • Functions: Local Functions, Lambdas, Inline Functions
  • Conditional Statements: if-else, when
  • Loops
  • Object-Oriented Programming: Classes, Objects, Constructors, Encapsulation, Access Modifiers, Inheritance, Generics
  • Asynchronous Programming: Threading, Callbacks, Future/Promise, Rx, Coroutines

Introduction

This is a strongly statically typed general-purpose programming language that runs on JVM. Kotlin mainly targets the Java Virtual Machine (JVM), but also transpiles to JavaScript. Kotlin/JS provides the ability to transpile your Kotlin code, the Kotlin standard library, and any compatible dependencies to JavaScript. The recommended way to use Kotlin/JS is via the kotlin.js and kotlin.multiplatform Gradle plugins.

Variables

Mutable Variables

Mutable means that the variable can be reassigned to a different value after initial assignment.

var a:Int = 1
var b = 1
var c:Int
c = 1

Read Only/Const variables

val a:Int = 1
val b = 1
val c:Int // Type is required when no initializer is provided
c = 1

Data Types

Kotlin built-in data type can be categorized as follows:

  • Number
  • Character
  • String
  • Boolean
  • Array

Numbers

val a: Int = 10000
val d: Double = 100.00
val f: Float = 100.00f
val l: Long = 1000000004
val s: Short = 10
val b: Byte = 1

Characters

Characters are represented by the type Char. Character literals go in single quotes: '1'

If a value of character variable is a digit, you can explicitly convert it to an Int number using the digitToInt() function

Strings

Strings in Kotlin are represented by the type String. Generally, a string value is a sequence of characters in double quotes (")

Elements of a string are characters that you can access via the indexing operation: s[i]. You can iterate over these characters with a for loop:

for (c in str) {
println(c)
}

Strings are immutable. Once you initialize a string, you can’t change its value or assign a new value to it. All operations that transform strings return their results in a new String object, leaving the original string unchanged.

String Templates

val s = "abc"
println("$s.length is ${s.length}") // prints "abc.length is 3"

Booleans

The type Boolean represents boolean objects that can have two values: true and false.

Boolean has a nullable counterpart Boolean? that also has the null value.

Built-in operations on booleans include:

  • || – disjunction (logical OR)
  • && – conjunction (logical AND)
  • !negation (logical NOT)

|| and && work lazily.

val myTrue: Boolean = true
val myFalse: Boolean = false
val boolNull: Boolean? = null

Arrays

Arrays in Kotlin are represented by the Array class. It has get and set functions that turn into [] by operator overloading conventions, and the size property, along with other useful member functions:

class Array<T> private constructor() {
val size: Int
operator fun get(index: Int): T
operator fun set(index: Int, value: T): Unit
operator fun iterator(): Iterator<T>
// ...
}

Type Checks and Casts

Use the is operator or its negated form !is to perform a runtime check that identifies whether an object conforms to a given type:

if (obj is String) {
print(obj.length)
}
if (obj !is String) { // same as !(obj is String)
print("Not a String")
} else {
print(obj.length)
}

“Safe” (nullable) cast operator

To avoid exceptions, use the safe cast operator as?, which returns null on failure.

val x: String? = y as? String

Null-Safety

var a: String = "abc" // Regular initialization means non-null by default
a = null // compilation error
var b: String? = "abc" // can be set to null
b = null // ok

Safe Calls

Your second option for accessing a property on a nullable variable is using the safe call operator ?.

val a = "Kotlin"
val b: String? = null
println(b?.length) // This returns b.length if b is not null, and null otherwise.
println(a?.length) // Unnecessary safe call

Operators

  • Arithmetic Operators
  • Relational Operators
  • Assignment Operators
  • Unary Operators
  • Logical Operators
  • Bitwise Operations

Arithmetic Operators

Relational Operators

Assignment Operators

Unary Operators

The unary operators require only one operand; they perform various operations such as incrementing/decrementing a value by one, negating an expression, or inverting the value of a boolean.

fun main(args: Array<String>) {
var x: Int = 40
var b:Boolean = true
println("+x = " + (+x))
println("-x = " + (-x))
println("++x = " + (++x))
println("--x = " + (--x))
println("!b = " + (!b))
}
/*Prints:+x = 40
-x = -40
++x = 41
--x = 40
!b = false
*/

Logical Operators

Bitwise Operators

Kotlin does not have any bitwise operators but Kotlin provides a list of helper functions to perform bitwise operations.

Functions

Kotlin functions are declared using the fun keyword:

fun double(x: Int): Int {
return 2 * x
}

Kotlin functions are first-class, which means they can be stored in variables and data structures, and can be passed as arguments to and returned from other higher-order functions. You can perform any operations on functions that are possible for other non-function values.

Default Arguments

fun read(
b: ByteArray,
off: Int = 0,
len: Int = b.size,
) { /*...*/ }

Named Arguments

When calling a function, you can name one or more of its arguments. This can be helpful when a function has many arguments and it’s difficult to associate a value with an argument, especially if it’s a boolean or null value

fun myFun(
str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ',
) { /*...*/ }
// calling the function with named variables
myFun(
"String!",
false,
upperCaseFirstLetter = false,
divideByCamelHumps = true,
'_'
)

On the JVM: You can’t use the named argument syntax when calling Java functions because Java bytecode does not always preserve the names of function parameters.

Local Functions

Kotlin supports local functions, which are functions inside other functions:

fun dfs(graph: Graph) {
fun dfs(current: Vertex, visited: MutableSet<Vertex>) {
if (!visited.add(current)) returnfor (v in current.neighbors)
dfs(v, visited)
}
dfs(graph.vertices[0], HashSet())
}

Lambdas

Syntax:

{variable with type -> body of the function}

val upperCase = { str: String -> str.toUpperCase() }  
println( upperCase("hello, world!") )
//prints HELLO, WORLD!

Inline Functions

An inline function is declared with inline keyword. The use of inline function enhances the performance of higher order function. The inline function tells the compiler to copy parameters and functions to the call site.

Higher order functions mean functions that take another function as an argument such as:

fun nonInlined(block: () -> Unit) {
println("before")
block()
println("after")
}

No Function instance will be created, instead, the code around the invocation of block inside the inlined function will be copied to the call site, so you'll get something like this in the bytecode.

Control Flow

if-else

if (condition1) {
// code block A to be executed if condition1 is true
} else if (condition2) {
// code block B to be executed if condition2 is true
} else {
// code block C to be executed if condition1 and condition2 are false
}

Ternary Operator

There is no ternary operators supported in Kotlin -unfortunately-

Equivalently, the following syntax is valid:

var v = if (a) b else c

When

When is similar to switch-case in Java

val day = 2val result = when (day) {
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
7 -> "Sunday"
else -> "Invalid day."
}
when (day) {
1, 2, 3, 4, 5 -> println("Weekday")
else -> println("Weekend")
}
when (day) {
in 1..5 -> println("Weekday")
else -> println("Weekend")
}

Loops

Kotlin for loop iterates through anything that provides an iterator

for (item in collection) {
// body of loop
}
for (item in 1..5) {
println(item)
}

Kotlin while loop is similar to Java while loop.

while (condition) {
// body of the loop
}
var i = 5;
while (i > 0) {
println(i)
i--
}

and do-while loop

do{
// body of the loop
} while( condition )
var i = 5;
do{
println(i)
i--
}while(i > 0)

Break & Continue

Kotlin break statement is used to come out of a loop once a certain condition is met. This loop could be a for, while or do…while loop.

var i = 0;
while (i++ < 100) {
println(i)
if( i == 3 ){
break
}
}

The Kotlin continue statement breaks the loop iteration in between (skips the part next to the continue statement till end of the loop) and continues with the next iteration in the loop.

var i = 0;
while (i++ < 6) {
if( i == 3 ){
continue
}
println(i)
}

Object-Oriented Programming

Classes

Basic syntax:

class myClass {
// property (data member)
private var name: String = "You"

// member function
fun printMe() {
print("You rock! Yes, " + name)
}
}
fun main(args: Array<String>) {
val obj = myClass() // create obj object of myClass class
obj.printMe()
}

Kotlin does not have a new keyword.

Nested Classes

By definition, when a class has been created inside another class, then it is called as a nested class. In Kotlin, nested class is by default static, hence, it can be accessed without creating any object of that class.

fun main(args: Array<String>) {
val demo = Outer.Nested()
print(demo.foo())
}
class Outer {
class Nested {
fun foo() = "I am hiding inside"
}
}

Constructors

A class in Kotlin can have a primary constructor and one or more secondary constructors. The primary constructor is a part of the class header, and it goes after the class name and optional type parameters.

class Person public @Inject constructor(firstName: String) { /*...*/ }

If the primary constructor does not have any annotations or visibility modifiers, the constructor keyword can be omitted:

class Person(firstName: String) { 
constructor(i: Int) {
println("Constructor $i")
}
/*...*/
}

Encapsulation & Access Modifiers

If you do not specify any visibility modifier, public is used by default, which means that your declarations will be visible everywhere

  • private means that the member is visible inside this class only (including all its members).
  • protected means that the member has the same visibility as one marked as private, but that it is also visible in subclasses.
  • internal means that any client inside this module who sees the declaring class sees its internal members.
  • public means that any client who sees the declaring class sees its public members.

Inheritance

open class Base(p: Int)class Derived(p: Int) : Base(p)

Overriding Methods

Kotlin requires explicit modifiers for overridable members and overrides:

open class Shape {
open fun draw() { /*...*/ }
fun fill() { /*...*/ }
}
class Circle() : Shape() {
override fun draw() { /*...*/ }
}

Overriding Properties

The overriding mechanism works on properties in the same way that it does on methods.

open class Shape {
open val vertexCount: Int = 0
}
class Rectangle : Shape() {
override val vertexCount = 4
}
interface Shape {
val vertexCount: Int
}
class Rectangle(override val vertexCount: Int = 4) : Shape // Always has 4 verticesclass Polygon : Shape {
override var vertexCount: Int = 0 // Can be set to any number later
}

Abstract Class

A class may be declared abstract, along with some or all of its members. An abstract member does not have an implementation in its class. You don't need to annotate abstract classes or functions with open.

abstract class Polygon {
abstract fun draw()
}
class Rectangle : Polygon() {
override fun draw() {
// draw the rectangle
}
}

The open keyword means “open for extension“. The open annotation on a class is the opposite of Java’s final: it allows others to inherit from this class. By default, all classes in Kotlin are final, which corresponds to Effective Java, Item 17: Design and document for inheritance or else prohibit it.

You also need to be explicit about methods you want to make overridable, also marked with open:

open class Base {
open fun v() {}
fun nv() {}
}

Generics

Generic Classes

fun main(args: Array<String>) {
var objject = genericsExample<String>("KOTLIN")
var objject1 = genericsExample<Int>(10)
}
class genericsExample<T>(input:T) {
init {
println("I am getting called with the value "+input)
}
}

Generic Functions

Functions can have generic parameters, which are specified using angle brackets before the function name:

fun <T> singletonList(item: T): List<T> { /*...*/ }

Generics: in, out, where

  • List<out T> in Kotlin is equivalent to List<? extends T> in Java.
  • List<in T> in Kotlin is equivalent to List<? super T> in Java
  • where is a little bit different. The passed type must satisfy all conditions of the where clause simultaneously. In the above example, the T type must implement both CharSequence and Comparable.
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}

Async Programming

Multi-threading is a feature that allows concurrent execution of two or more parts of a program for maximum utilization of CPU. In Kotlin we have multiple ways of performing multi-threading just like java.

  • Threading
  • Callbacks
  • Futures, promises, and others
  • Reactive Extensions
  • Coroutines

Threading

fun postItem(item: Item) {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
fun preparePost(): Token {
// makes a request and consequently blocks the main thread
return token
}
  • Threads aren’t cheap. Threads require context switches which are costly.
  • Threads aren’t infinite. The number of threads that can be launched is limited by the underlying operating system. In server-side applications, this could cause a major bottleneck.
  • Threads aren’t always available. Some platforms, such as JavaScript do not even support threads.
  • Threads aren’t easy. Debugging threads, avoiding race conditions are common problems we suffer in multi-threaded programming.

Callbacks

With callbacks, the idea is to pass one function as a parameter to another function, and have this one invoked once the process has completed.

fun postItem(item: Item) {
preparePostAsync { token ->
submitPostAsync(token, item) { post ->
processPost(post)
}
}
}
fun preparePostAsync(callback: (Token) -> Unit) {
// make request and return immediately
// arrange callback to be invoked later
}
  • Difficulty of nested callbacks. Usually a function that is used as a callback, often ends up needing its own callback. This leads to a series of nested callbacks which lead to incomprehensible code. The pattern is often referred to as the titled christmas tree (braces represent branches of the tree).
  • Error handling is complicated. The nesting model makes error handling and propagation of these somewhat more complicated.

Futures, Promises

fun postItem(item: Item) {
preparePostAsync()
.thenCompose { token ->
submitPostAsync(token, item)
}
.thenAccept { post ->
processPost(post)
}
}fun preparePostAsync(): Promise<Token> {
// makes request and returns a promise that is completed later
return promise
}
  • Different programming model. Similar to callbacks, the programming model moves away from a top-down imperative approach to a compositional model with chained calls. Traditional program structures such as loops, exception handling, etc. usually are no longer valid in this model.
  • Different APIs. Usually there’s a need to learn a completely new API such as thenCompose or thenAccept, which can also vary across platforms.
  • Specific return type. The return type moves away from the actual data that we need and instead returns a new type Promise which has to be introspected.
  • Error handling can be complicated. The propagation and chaining of errors aren’t always straightforward.

Reactive Extensions (Rx)

The idea behind Rx is to move towards what’s called observable streams whereby we now think of data as streams (infinite amounts of data) and these streams can be observed. In practical terms, Rx is simply the Observer Pattern with a series of extensions which allow us to operate on the data.

Kotlin Coroutines

Kotlin’s approach to working with asynchronous code is using coroutines, which is the idea of suspendable computations, i.e. the idea that a function can suspend its execution at some point and resume later on.

One of the benefits however of coroutines is that when it comes to the developer, writing non-blocking code is essentially the same as writing blocking code. The programming model in itself doesn’t really change.

fun postItem(item: Item) {
launch {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
}
suspend fun preparePost(): Token {
// makes a request and suspends the coroutine
return suspendCoroutine { /* ... */ }
}

This code will launch a long-running operation without blocking the main thread. The preparePost is what's called a suspendable function, thus the keyword suspend prefixing it. What this means as stated above, is that the function will execute, pause execution and resume at some point in time.

  • The function signature remains exactly the same. The only difference is suspend being added to it. The return type however is the type we want to be returned.
  • The code is still written as if we were writing synchronous code, top-down, without the need of any special syntax, beyond the use of a function called launch which essentially kicks off the coroutine (covered in other tutorials).
  • The programming model and APIs remain the same. We can continue to use loops, exception handling, etc. and there’s no need to learn a complete set of new APIs.
  • It is platform independent. Whether we’re targeting JVM, JavaScript or any other platform, the code we write is the same. Under the covers the compiler takes care of adapting it to each platform.
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello") // main coroutine continues while a previous one is delayed
}
  1. launch is a coroutine builder. It launches a new coroutine concurrently with the rest of the code, which continues to work independently. That's why Hello has been printed first.
  2. delay is a special suspending function. It suspends the coroutine for a specific time. Suspending a coroutine does not block the underlying thread, but allows other coroutines to run and use the underlying thread for their code.
  3. runBlocking is also a coroutine builder that bridges the non-coroutine world of a regular fun main() and the code with coroutines inside of runBlocking { ... } curly braces. This is highlighted in an IDE by this: CoroutineScope hint right after the runBlocking opening curly brace. If you remove or forget runBlocking in this code, you'll get an error on the launch call, since launch is declared only in the CoroutineScope.
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}
// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}

Hope these notes help your progress! Don’t forget to check further topics such as Annotations, Reflection, more about Coroutines, and Effective Kotlin guide to write cleaner Kotlin code!

Clap, follow, and leave your comments below!

You can also follow me on Twitter and Linkedin for more growth, gamification, and indie developer tips & tricks.

--

--

Azmi Rutkay Biyik

Blogger, Software Engineer, Growth & Gamification Enthusiast