Beginner's Guide to Kotlin

  ·   20 min read

Getting Started with Kotlin #

Setting up your environment #

To start developing Kotlin applications, you’ll need to set up your development environment. The simplest way is using the Kotlin/JVM distribution, which allows you to run Kotlin code on the Java Virtual Machine (JVM). Here’s a breakdown of the process:

  1. Download the Kotlin distribution: Visit the official Kotlin website (https://kotlinlang.org/) and download the latest version of the Kotlin distribution appropriate for your operating system. This will typically include the Kotlin compiler (kotlinc), standard library, and other tools.

  2. Install the JDK (Java Development Kit): Kotlin runs on the JVM, so you’ll need a JDK installed on your system. Download and install a compatible JDK from Oracle (https://www.oracle.com/java/technologies/javase-downloads.html) or other reputable providers like OpenJDK. Ensure you have the JAVA_HOME environment variable correctly set.

  3. (Optional) Use an IDE: While you can use a command-line compiler, using an Integrated Development Environment (IDE) significantly improves the development experience. Popular choices include IntelliJ IDEA (with Kotlin plugin), Android Studio (built-in Kotlin support), and Eclipse (with Kotlin plugin). These IDEs offer features like code completion, debugging, and project management that simplify Kotlin development.

  4. (Optional) Setting up a build system: For larger projects, using a build system like Gradle or Maven is recommended. These tools automate the build process, making it easier to manage dependencies and build artifacts. Your chosen IDE will typically have integration with these build systems.

Your first Kotlin program #

Let’s write a simple “Hello, world!” program to get you started. This program will print the message “Hello, world!” to the console.

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

This code defines a single function, main(), which is the entry point of your Kotlin program. The println() function prints the string “Hello, world!” to the console. Save this code in a file named Main.kt.

Running your Kotlin code #

There are several ways to run your Kotlin code:

1. Using the command-line compiler:

After saving your Main.kt file, open your terminal or command prompt, navigate to the directory containing the file, and use the Kotlin compiler to compile and run your code:

kotlinc Main.kt -include-runtime -d Main.jar
java -jar Main.jar

This compiles your Kotlin code into a JAR file (Main.jar) and then runs the JAR file using the Java Runtime Environment (JRE). The -include-runtime flag includes the Kotlin runtime library in the JAR file, making it self-contained.

2. Using an IDE:

If you’re using an IDE like IntelliJ IDEA or Android Studio, the process is even simpler. After creating a Kotlin project and writing your code, you can typically run the code by clicking a “Run” button or selecting a “Run” option from the menu. The IDE handles the compilation and execution process for you.

Regardless of the method you choose, the output should be:

Hello, world!

This confirms your Kotlin environment is set up correctly and you’ve successfully run your first Kotlin program.

Basic Syntax and Data Types #

Variables and Constants #

Kotlin uses val to declare immutable variables (constants) and var to declare mutable variables. Once a val is assigned a value, it cannot be changed. A var, on the other hand, can be reassigned.

val name: String = "John Doe" // Constant declaration and initialization
var age: Int = 30           // Mutable variable declaration and initialization
age = 31                     // Reassigning the value of a mutable variable

// Type inference:  The compiler can often infer the type
val city = "New York"  // Type is inferred as String
var count = 10         // Type is inferred as Int

It’s good practice to initialize variables when you declare them. If you don’t provide an initial value for a var, you must explicitly assign a value before using it. val must be initialized when it is declared.

Data Types (Int, Double, Boolean, String, etc.) #

Kotlin has several built-in data types:

  • Int: Represents 32-bit integers (whole numbers).
  • Long: Represents 64-bit integers.
  • Short: Represents 16-bit integers.
  • Byte: Represents 8-bit integers.
  • Double: Represents 64-bit floating-point numbers (numbers with decimal points).
  • Float: Represents 32-bit floating-point numbers.
  • Boolean: Represents boolean values (true or false).
  • Char: Represents single characters (e.g., 'A', 'b').
  • String: Represents sequences of characters (text).
  • Any: Represents the supertype of all other types.
  • Unit: Represents the absence of a meaningful value (similar to void in other languages).
val age: Int = 30
val price: Double = 99.99
val isAdult: Boolean = true
val initial: Char = 'J'
val message: String = "Hello, Kotlin!"

Type Inference #

Kotlin’s type inference system allows you to omit the explicit type declaration in many cases. The compiler will infer the type based on the value assigned.

val name = "Alice" // Type inferred as String
val quantity = 10  // Type inferred as Int

While type inference is convenient, explicitly declaring types can improve code readability and maintainability, especially in larger projects.

Operators #

Kotlin supports standard arithmetic, comparison, logical, and bitwise operators. Here are some examples:

  • Arithmetic Operators: +, -, *, /, % (modulo)
  • Comparison Operators: == (equals), != (not equals), >, <, >=, <=
  • Logical Operators: && (AND), || (OR), ! (NOT)
  • Assignment Operators: =, +=, -=, *=, /=, %=
val sum = 10 + 5
val difference = 10 - 5
val product = 10 * 5
val quotient = 10 / 5
val remainder = 10 % 3

val isEqual = 10 == 10
val isGreater = 10 > 5

val isTrue = true && false
val isFalse = !true

Kotlin also supports other operators like the Elvis operator (?:), range operators (.., until), and more, which you’ll encounter as you progress in your learning.

Control Flow #

if-else statements #

Kotlin’s if-else statements work similarly to those in other languages. The else block is optional. The condition in an if statement must be a Boolean expression.

val age = 25

if (age >= 18) {
    println("Adult")
} else {
    println("Minor")
}

val temperature = 20

if (temperature > 25) {
    println("Hot")
} else if (temperature > 15) {
    println("Warm")
} else {
    println("Cool")
}

if expressions can also be used as expressions, returning a value:

val max = if (a > b) a else b

when expressions #

when expressions provide a more concise way to handle multiple conditions. They are similar to switch statements in other languages but offer more flexibility.

val dayOfWeek = 3

when (dayOfWeek) {
    1 -> println("Monday")
    2 -> println("Tuesday")
    3 -> println("Wednesday")
    4 -> println("Thursday")
    5 -> println("Friday")
    6, 7 -> println("Weekend") // Multiple values can be handled together
    else -> println("Invalid day")
}


val score = 95

when {
    score >= 90 -> println("A")
    score >= 80 -> println("B")
    score >= 70 -> println("C")
    else -> println("F")
}

In the second example, when acts as a general conditional expression, without checking a specific variable.

for loops #

Kotlin provides several ways to iterate using for loops.

1. Iterating over ranges:

for (i in 1..5) { // Closed range (inclusive)
    println(i) // Prints 1, 2, 3, 4, 5
}

for (i in 1 until 5) { // Half-open range (exclusive of 5)
    println(i) // Prints 1, 2, 3, 4
}

for (i in 5 downTo 1) { // Descending range
    println(i) // Prints 5, 4, 3, 2, 1
}

for (i in 1..5 step 2) { // Stepping with a specific increment
    println(i) // Prints 1, 3, 5
}

2. Iterating over arrays or collections:

val names = arrayOf("Alice", "Bob", "Charlie")
for (name in names) {
    println(name)
}

// Using indices:
for ((index, name) in names.withIndex()) {
    println("Name at index $index: $name")
}

while loops #

Kotlin supports both while and do-while loops:

var i = 0
while (i < 5) {
    println(i)
    i++
}

var j = 0
do {
    println(j)
    j++
} while (j < 5)

The do-while loop executes the block of code at least once, before checking the condition. The while loop checks the condition before executing the block.

Functions #

Defining functions #

Functions in Kotlin are defined using the fun keyword, followed by the function name, parameter list in parentheses (), and the return type (if any) after a colon :.

fun greet(name: String): String {
    return "Hello, $name!"
}

fun add(a: Int, b: Int): Int {
    return a + b
}

fun printMessage(message: String) { //Unit return type is implicit
    println(message)
}

The Unit return type is implicitly returned if there’s no explicit return type and the function doesn’t explicitly return a value.

Function parameters and return types #

Function parameters are specified by their type and name. You can provide default values for parameters.

fun greet(name: String, greeting: String = "Hello"): String {
    return "$greeting, $name!"
}

fun calculateArea(width: Double, height: Double = width): Double { //Default value for height
    return width * height
}

fun main(){
    println(greet("Alice")) //Uses default greeting
    println(greet("Bob", "Hi")) //Provides custom greeting
    println(calculateArea(5.0)) //Uses default height (same as width)
    println(calculateArea(5.0, 10.0)) //Provides custom height

}

Function overloading #

Kotlin allows function overloading, meaning you can have multiple functions with the same name but different parameter lists. The compiler differentiates them based on the number, type, or order of parameters.

fun add(a: Int, b: Int): Int = a + b
fun add(a: Double, b: Double): Double = a + b
fun add(a: String, b: String): String = a + b

fun main(){
    println(add(5, 3))       // Calls Int version
    println(add(5.5, 2.3))   // Calls Double version
    println(add("Hello", "World")) // Calls String version
}

Lambda expressions #

Lambda expressions are anonymous functions that can be passed as arguments to other functions or assigned to variables. They are particularly useful when working with higher-order functions (functions that take other functions as parameters or return functions).

val sum: (Int, Int) -> Int = { a, b -> a + b } //Lambda expression defining a function that takes two integers and returns an integer.

fun operate(a:Int, b:Int, operation: (Int, Int) -> Int): Int{
    return operation(a,b)
}

fun main(){
    println(sum(5,3)) //Calling the lambda expression directly
    println(operate(10,5, sum)) //Passing lambda expression as an argument
    println(operate(7,2){a,b -> a - b}) //Lambda expression as argument directly in function call
}

Lambda expressions can be simplified when they have a single parameter or when the parameter types can be inferred. For example: { a -> a * 2 } or { it * 2 } (if only one parameter is used).

Object-Oriented Programming (OOP) in Kotlin #

Classes and objects #

Classes are blueprints for creating objects. Objects are instances of classes. Classes are defined using the class keyword.

class Dog(val name: String, var age: Int) {  // Primary constructor

    fun bark() {
        println("Woof!")
    }

    fun description():String{
        return "My name is $name and I am $age years old."
    }
}


fun main() {
    val myDog = Dog("Buddy", 3)  //Creating an object (instance) of the Dog class
    println(myDog.name)         //Accessing properties
    myDog.age = 4               //Modifying a mutable property
    myDog.bark()                //Calling a method
    println(myDog.description())
}

Constructors #

Constructors initialize objects when they are created. Kotlin has primary and secondary constructors. The primary constructor is part of the class header.

class Person(val name: String, val age: Int) { // Primary constructor

    // Secondary constructor
    constructor(name: String, age: Int, city: String) : this(name, age) {
        println("Person lives in $city")
    }

    fun greet(){
        println("Hi, my name is $name")
    }
}

fun main(){
    val person1 = Person("Alice", 30)
    val person2 = Person("Bob", 25, "New York")
    person1.greet()
    person2.greet()
}

In this example, the secondary constructor delegates to the primary constructor using this(name, age).

Inheritance #

Inheritance allows a class (subclass or derived class) to inherit properties and methods from another class (superclass or base class). Kotlin uses the : symbol to indicate inheritance.

open class Animal(val name: String) { //open keyword makes the class inheritable
    open fun makeSound() {
        println("Generic animal sound")
    }
}

class Dog(name: String) : Animal(name) { //Inheriting from Animal
    override fun makeSound() {
        println("Woof!")
    }
}

class Cat(name: String) : Animal(name) {
    override fun makeSound() {
        println("Meow!")
    }
}

fun main() {
    val myDog = Dog("Buddy")
    myDog.makeSound()
    val myCat = Cat("Whiskers")
    myCat.makeSound()
}

The open keyword is required for a class to be inheritable. The override keyword is used to override methods from the superclass.

Interfaces #

Interfaces define a contract that classes can implement. They specify methods that implementing classes must provide.

interface Flyable {
    fun fly()
}

class Bird(val name: String) : Flyable {
    override fun fly() {
        println("$name is flying")
    }
}

class Plane(val model:String) : Flyable{
    override fun fly() {
        println("$model is taking off")
    }
}

fun main(){
    val bird = Bird("Eagle")
    bird.fly()
    val plane = Plane("Boeing 747")
    plane.fly()
}

Data classes #

Data classes are concisely defined classes that automatically generate methods like equals(), hashCode(), toString(), copy(), and componentN() for convenient data representation.

data class Book(val title: String, val author: String, val year: Int)

fun main() {
    val book1 = Book("The Lord of the Rings", "J.R.R. Tolkien", 1954)
    println(book1) // Automatically generated toString() method
    val book2 = book1.copy(year = 1965) // Using the copy() method
    println(book2)
    println(book1 == book2) // Using the equals() method
}

Data classes simplify the creation of classes primarily focused on holding data.

Null Safety #

The problem of null pointer exceptions #

Null pointer exceptions (NullPointerExceptions or NPEs) are a common source of errors in many programming languages, including Java. They occur when a program attempts to access a member (method or property) of an object that is currently null (does not refer to any object). This often leads to program crashes. Kotlin’s null safety features aim to mitigate this issue.

Nullable types #

Kotlin distinguishes between non-nullable and nullable types. A non-nullable type cannot hold a null value. A nullable type can hold either a value or null. Nullable types are indicated by a question mark ? after the type.

val name: String = "Alice"  // Non-nullable String
val age: Int? = null        // Nullable Int

// val message:String = null //This will cause a compilation error because String is not nullable
val message: String? = null //This is allowed because String? is nullable

Trying to access a member of a non-nullable variable that is null will result in a compile-time error. This prevents NullPointerExceptions at runtime.

Safe call operator (?.) #

The safe call operator ?. allows you to access members of a nullable type without causing a NullPointerException. If the object on the left side of ?. is null, the expression short-circuits and returns null; otherwise, it accesses the member.

val person: Person? = null
val city: String? = person?.address?.city //If person or address is null, city will be null

fun main() {
    val person: Person? = Person("Bob", "123 Main St")
    val city = person?.address?.city //If person is not null and address is not null then it'll access the city
    println(city)
}


data class Address(val city:String)
data class Person(val name:String, val address:Address?)

Elvis operator (?:) #

The Elvis operator ?: provides a concise way to handle nullable values. It returns the value on the left side if it’s not null; otherwise, it returns the value on the right side.

val name: String? = null
val displayName = name ?: "Anonymous" // displayName will be "Anonymous" because name is null

val age: Int? = 25
val displayAge = age ?: 0 // displayAge will be 25

The Elvis operator is frequently used to provide default values for nullable variables.

The !! operator #

The not-null assertion operator !! forces a nullable type to be treated as non-nullable. It’s generally discouraged unless you’re absolutely certain the value is not null, because using it can still cause a NullPointerException at runtime if the value is indeed null. It should be used with caution.

val name: String? = null
val displayName = name!!.uppercase() //Throws a NullPointerException at runtime if name is null

fun main(){
    val name:String? = "Alice"
    println(name!!.uppercase()) // This is safe because name is not null
}

The !! operator is a last resort – prefer using safe calls and the Elvis operator whenever possible to avoid runtime crashes. It is safer to handle potential null values explicitly using the safe call and Elvis operator rather than relying on the !! operator.

Collections #

Kotlin provides several built-in collection types for storing and manipulating data. These are immutable by default unless explicitly declared as mutable.

Lists #

Lists are ordered collections that can contain duplicate elements. Kotlin offers both mutable and immutable lists.

val numbers: List<Int> = listOf(1, 2, 3, 3, 4, 5) // Immutable list
val mutableNumbers: MutableList<Int> = mutableListOf(1, 2, 3, 4, 5) //Mutable List

fun main(){
    println(numbers) //Prints the immutable list
    mutableNumbers.add(6) //Adding element to a mutable list
    println(mutableNumbers) //Prints the modified mutable list

    //Accessing elements
    println(mutableNumbers[0]) //Accessing element at index 0
    println(mutableNumbers.get(1)) //Another way to access elements

    //Checking for an element
    println(mutableNumbers.contains(3)) //true

    //Iterating (see Iterating through collections section)
}

listOf() creates an immutable list, while mutableListOf() creates a mutable list. Mutable lists allow adding, removing, or modifying elements.

Sets #

Sets are unordered collections that do not allow duplicate elements. Like lists, they can be mutable or immutable.

val names: Set<String> = setOf("Alice", "Bob", "Charlie", "Alice") // Immutable set (duplicates are removed)
val mutableNames: MutableSet<String> = mutableSetOf("Alice", "Bob", "Charlie") //Mutable Set

fun main(){
    println(names)  //Prints the immutable set without duplicates. Order is not guaranteed
    mutableNames.add("David")
    println(mutableNames) //Prints the modified mutable set

    //Checking for an element
    println(mutableNames.contains("Bob")) //True

    //Iterating (see Iterating through collections section)

}

setOf() creates an immutable set, while mutableSetOf() creates a mutable set.

Maps #

Maps are collections of key-value pairs. Keys must be unique, while values can be duplicated. Maps can also be mutable or immutable.

val ages: Map<String, Int> = mapOf("Alice" to 30, "Bob" to 25, "Charlie" to 35) // Immutable map
val mutableAges: MutableMap<String, Int> = mutableMapOf("Alice" to 30, "Bob" to 25) //Mutable map

fun main(){
    println(ages) //Prints the immutable map
    mutableAges["Charlie"] = 35 //Adding or modifying an entry
    println(mutableAges) //Prints the modified mutable map

    //Accessing a value using the key
    println(mutableAges["Alice"]) //Prints 30

    //Checking for a key
    println(mutableAges.containsKey("Bob")) //True

    //Iterating (see Iterating through collections section)
}

mapOf() creates an immutable map, while mutableMapOf() creates a mutable map.

Iterating through collections #

There are several ways to iterate through collections:

1. Using for loops:

val numbers = listOf(1, 2, 3, 4, 5)
for (number in numbers) {
    println(number)
}

val names = mapOf("Alice" to 30, "Bob" to 25)
for ((name, age) in names) {
    println("$name is $age years old")
}

2. Using iterators:

val numbers = listOf(1, 2, 3, 4, 5)
val iterator = numbers.iterator()
while (iterator.hasNext()) {
    println(iterator.next())
}

3. Using higher-order functions (more advanced):

val numbers = listOf(1, 2, 3, 4, 5)
numbers.forEach { println(it) } //forEach is a higher-order function

Higher-order functions like forEach, map, filter, etc., provide more functional ways to process collections. These will be covered in more advanced sections of this manual.

Extensions #

What are extensions? #

Extensions in Kotlin allow you to add new functionality to existing classes without modifying their source code or creating subclasses. This is achieved by defining functions outside of a class but specifying the class they extend as a receiver. Extensions don’t change the original class; they simply add new methods that can be called as if they were part of the class.

How to create extensions #

Extensions are defined using the fun keyword, followed by the receiver type, a dot ., the extension function name, and the parameter list.

fun String.addExclamationMark(): String { //Extension function for String class
    return this + "!"
}

fun Int.isEven(): Boolean { //Extension function for Int class
    return this % 2 == 0
}

fun main() {
    val message = "Hello"
    println(message.addExclamationMark()) // Calling the extension function

    val number = 10
    println(number.isEven()) // Calling the extension function
}

In these examples, String.addExclamationMark() adds an exclamation mark to a string, and Int.isEven() checks if an integer is even. These functions can be called directly on String and Int objects, respectively, even though they are defined outside the String and Int classes.

Benefits of using extensions #

  • Improved code readability: Extensions can make code more concise and readable by adding methods directly to existing types where they logically belong. You avoid creating helper classes or utility functions.

  • Increased code reusability: Extensions are reusable across different parts of your codebase.

  • Avoidance of subclassing: Extensions provide a way to extend functionality without the need to create new subclasses, which can be beneficial when you can’t modify the original class or when creating subclasses would be overly complex.

  • Improved organization: Well-designed extensions can contribute to better code organization, keeping related functionality grouped together logically.

  • Adding functionality to external libraries: Extensions are particularly useful for adding extra functions to classes defined in external libraries that you cannot modify directly.

However, keep in mind that extensions do not modify the original class. They simply add new functionality that can be called on instances of that class. If multiple extension functions with the same name exist for a particular type, resolution is determined by the context in which the extension function is called.

Working with Strings #

String templates #

String templates in Kotlin provide a concise way to embed expressions within strings. They are defined using a dollar sign $ followed by either a variable name or an expression enclosed in curly braces {}.

val name = "Alice"
val age = 30

val message = "My name is $name and I am $age years old." //Simple variable interpolation
println(message)

val complexMessage = "The result of 10 + 5 is ${10 + 5}" //Expression interpolation
println(complexMessage)

//Using raw strings for avoiding escaping special characters

val rawString = """This is a raw string.
It can contain multiple lines
and special characters like \n without escaping."""
println(rawString)

String templates make it easy to create dynamic strings by incorporating values of variables and the result of expressions directly into the string literal.

String manipulation functions #

Kotlin provides a rich set of built-in functions for manipulating strings. Some commonly used functions include:

  • length: Returns the length of the string.

  • uppercase(): Converts the string to uppercase.

  • lowercase(): Converts the string to lowercase.

  • substring(startIndex, endIndex): Extracts a substring.

  • replace(oldValue, newValue): Replaces occurrences of a substring.

  • startsWith(prefix): Checks if the string starts with a given prefix.

  • endsWith(suffix): Checks if the string ends with a given suffix.

  • contains(substring): Checks if the string contains a given substring.

  • split(delimiter): Splits the string into a list of substrings based on a delimiter.

  • trim(): Removes leading and trailing whitespace.

val message = "Hello, World!"

println(message.length)         // Output: 13
println(message.uppercase())     // Output: HELLO, WORLD!
println(message.lowercase())     // Output: hello, world!
println(message.substring(7, 12)) // Output: World
println(message.replace("World", "Kotlin")) // Output: Hello, Kotlin!
println(message.startsWith("Hello")) // Output: true
println(message.endsWith("!"))     // Output: true
println(message.contains("World"))   // Output: true
println(message.split(", "))      // Output: [Hello, World!]
println(message.trim())          // Output: Hello, World!

These functions are readily available and provide a comprehensive set of tools for string manipulation, improving code efficiency and readability when working with text data. In addition, Kotlin’s standard library offers many more specialized string functions for more advanced scenarios.

Next Steps #

Learning Resources #

This Beginner’s Guide provides a foundational understanding of Kotlin. To further enhance your Kotlin skills and explore more advanced topics, consider these resources:

  • Official Kotlin Documentation: The official Kotlin website (https://kotlinlang.org/) offers comprehensive documentation, tutorials, and guides covering all aspects of the language. This is your primary resource for detailed information and up-to-date details.

  • Kotlin Koans: Kotlin Koans (https://kotlinlang.org/docs/learning-kotlin.html#try-kotlin-koans) are a series of interactive exercises that guide you through various Kotlin concepts. They provide a hands-on approach to learning.

  • Online Courses: Numerous online platforms (Coursera, Udemy, Udacity, etc.) offer Kotlin courses ranging from beginner to advanced levels. These courses provide structured learning paths and often include practice projects.

  • Books: Several books are dedicated to Kotlin programming, providing in-depth coverage of the language and its features. Search for “Kotlin programming books” to find options suitable for your experience level.

  • Kotlin Blog and Articles: Stay updated with the latest developments and best practices by following the Kotlin blog and reading articles on various programming websites and publications.

Community and Support #

The Kotlin community is active and supportive. If you encounter problems or have questions, several avenues are available for assistance:

  • Kotlin Slack: The official Kotlin Slack channel (https://surveys.jetbrains.com/s3/kotlin-slack) (you may need to request an invite) is a great place to ask questions, share knowledge, and connect with other Kotlin developers.

  • Kotlin Forums: Explore the Kotlin forums (https://discuss.kotlinlang.org/) for discussions, troubleshooting, and answers to common questions.

  • Stack Overflow: Stack Overflow is a valuable resource for finding solutions to coding problems. Search for Kotlin-related questions, and if you can’t find an answer, ask your own question, ensuring you provide a clear and concise description of the issue.

  • GitHub: Kotlin’s GitHub repository (https://github.com/JetBrains/kotlin) is a valuable resource for finding code samples, libraries, and contributing to the Kotlin project. You can also find many open-source Kotlin projects on GitHub that you can study and learn from.

Remember to clearly describe your problem when seeking help, including relevant code snippets and the steps you’ve already taken to troubleshoot the issue. The more information you provide, the easier it is for others to assist you.