Kotlin Crash Course
Chapter 1

Basics & Types

Variables, constants, type inference, strings, numbers, and basic operators. Learn how Kotlin's syntax differs from JS and Python.

By the end of this chapter, you will:

  • Understand the difference between val and var
  • Know Kotlin's basic types and how type inference works
  • Use string templates and multi-line strings
  • Work with basic operators and expressions
  • Understand why Kotlin has no primitive types

Variables: val vs var

In Kotlin, every variable is declared with either val or var. This choice is important because Kotlin strongly encourages immutability.

Theory

Kotlin's property system is designed around the principle that read-only data is safer. A val (value) can only be assigned once and behaves like a final variable in Java. Under the hood, the compiler generates a private field with a getter but no setter. A var (variable) generates both a getter and a setter, making it mutable. This distinction exists at the language level — the compiler enforces it, not just a convention.

Immutability by default eliminates entire categories of bugs: race conditions in multi-threaded code, accidental state mutations, and unintended side effects. When you see val in Kotlin code, you know that reference will never point to a different object.

val name = "Alex"      // Immutable — like JS `const` or Python doesn't have this
var age = 25           // Mutable — like JS `let` or Python variable

name = "Sam"           // ❌ ERROR: Val cannot be reassigned
age = 26               // ✅ OK
Best Practice: Always start with val. Only use var when you absolutely need to change the value. This prevents an entire class of bugs.

Type Inference

The Kotlin compiler performs local type inference by examining the initializer expression. This is not dynamic typing — the type is fixed at compile time. Explicit type declarations are required when there is no initializer (e.g., val name: String) or when you want a wider type than the initializer suggests (e.g., val n: Number = 42).

val inferred = "Hello"           // Compiler knows this is String
val explicit: String = "Hello"   // You can specify the type after a colon

val count = 42                   // Int
val price = 19.99                // Double
val isActive = true              // Boolean

Compare with JS / Python

JavaScript
const name = "Alex";
let age = 25;

// No type annotations in JS
// typeof name === "string"
Python
name = "Alex"      # no const
age = 25

# Type hints exist but are optional
name: str = "Alex"

Kotlin difference: val is the default mindset. Type inference is automatic but the type is fixed at compile time — no runtime type changes.

Basic Types

Unlike Java, Kotlin has no primitive types at the language level — Int, Double, Boolean, etc. are all objects. The compiler performs autoboxing optimizations: when possible, it compiles these to JVM primitives (int, double, boolean) for performance, but you write them as objects. This means you can call methods on numbers: (-5).absoluteValue or 42.toString().

// Everything is an object — methods available on literals
val abs = (-10).absoluteValue        // 10
val hex = 255.toString(16)          // "ff"
val bits = 0b1010.countOneBits()    // 2 (Kotlin 1.4+)

// JVM primitives under the hood, but you never think about it
val efficient: Int = 1_000_000      // compiled to primitive int
Kotlin TypeDescriptionExample
Int32-bit integer42
Long64-bit integer42L
Double64-bit floating point3.14
Float32-bit floating point3.14f
Booleantrue or falsetrue
CharSingle character'A'
StringText"Hello"
Note: Kotlin has no "primitive" types at the language level. Int, Double, etc. are all objects. The compiler optimizes them to JVM primitives when possible, but you don't need to think about it.

Number Literals

Kotlin supports multiple number bases and lets you use underscores for readability. This is especially useful for bitmasks, color values, and large constants.

val binary = 0b00001011       // 11 in decimal — useful for flags/bitmasks
val hex = 0x0F                // 15 in decimal — common for color channels
val large = 1_000_000         // One million — underscores for readability

// Real-world: bitmask flags and hex values
val permissionFlags = 0b00001101   // bitwise permission flags
val colorChannel = 0xFF           // 255 — a single RGB channel
Why this matters: In networking and file systems, binary flags are common. Underscores make large numbers readable without changing the value — 1_000_000 is the same as 1000000.
Android color note: Android ARGB colors like 0xFF121212 exceed Int's max value (2,147,483,647) and become Long in Kotlin. Use Color.parseColor("#FF121212") or the Android Color class for full ARGB values instead of raw hex literals.

Strings

Strings in Kotlin are more powerful than in JS or Python, especially with templates and multi-line support.

Theory

Kotlin strings are immutable sequences of UTF-16 characters (backed by java.lang.String on the JVM). The compiler transforms string templates into StringBuilder operations at compile time — there is no runtime interpolation engine. This means "Hello, $name" is as efficient as manual concatenation. Multi-line strings (triple quotes) are called raw strings and do not support \ escaping — what you type is what you get, including line breaks and indentation.

String Templates

val name = "Alex"
val age = 25

// Simple variable
val greeting = "Hello, $name!"           // "Hello, Alex!"

// Expression in braces
val nextYear = "You'll be ${age + 1}"    // "You'll be 26"

// Calling functions in templates
val shout = "HELLO, ${name.uppercase()}!"  // "HELLO, ALEX!"

// Escaping $ when you need a literal dollar sign
val price = "The price is \$50"          // "The price is $50"

// Raw strings (no escaping needed)
val path = "C:\\Users\\Alex"             // regular string — need escaping
val rawPath = """C:\Users\Alex"""       // raw string — no escaping!
Real-world use case: Building log messages or API URLs: "https://api.example.com/users/${userId.trim()}". Escaping $ is essential when displaying prices or currency: "Total: \$${totalAmount}".

Multi-line Strings

Raw strings preserve all whitespace exactly as typed. Use trimIndent() to remove common leading whitespace (useful for aligning with code indentation) or trimMargin("|") to strip everything up to a margin prefix character.

val json = """
    {
        "name": "$name",
        "age": $age
    }
""".trimIndent()   // Removes leading whitespace from each line

Compare with JS / Python

JavaScript
const name = "Alex";
const greeting = `Hello, ${name}!`;  // template literal
const raw = `Line 1
Line 2`;                              // multi-line
Python
name = "Alex"
greeting = f"Hello, {name}!"         # f-string
raw = """Line 1
Line 2"""                             # triple quotes

Kotlin difference: Templates work in regular strings too — no special ` or f prefix needed. Raw strings use triple quotes and preserve formatting.

Type Conversion

Kotlin does not do implicit type conversion. You must be explicit.

Theory

Java allows implicit widening conversions (intlongfloatdouble) which can silently lose precision (e.g., long to float). Kotlin rejects this design — every conversion must be explicit via .toXxx() methods. This prevents subtle precision bugs. The toInt(), toDouble(), etc. methods are defined as member functions on number types, not as external utility functions.

val i = 10
val d = i.toDouble()     // ✅ 10.0
// val bad: Double = i   // ❌ ERROR: Type mismatch

val s = "42"
val num = s.toInt()      // ✅ 42
// val bad2 = "abc".toInt()  // ❌ Runtime exception!
Watch out: Unlike JavaScript, "5" + 3 is not "53" in Kotlin — it's a compile error. Kotlin won't silently convert types for you.

Late Initialization

Sometimes you need a non-nullable variable that can't be initialized immediately — for example, in dependency injection or test setup where the value is assigned after object creation.

Theory

lateinit is a Kotlin compiler feature that delays initialization checking to runtime. The compiler generates a special nullable backing field but exposes it as a non-nullable type. Accessing the property before initialization throws UninitializedPropertyAccessException. You can check initialization status via the property reference syntax ::property.isInitialized. This exists primarily for frameworks (Android, Spring, test frameworks) where initialization happens through lifecycle methods or dependency injection containers outside the class constructor.

class UserRepository {
    lateinit var database: Database  // Will be assigned later

    fun setup(db: Database) {
        database = db
    }

    fun fetchUser(id: String): User {
        // Must check initialization before use
        if (::database.isInitialized) {
            return database.query(id)
        } else {
            throw IllegalStateException("Database not initialized")
        }
    }
}
// Common in Android — views are initialized after layout inflation
class MainActivity {
    lateinit var submitButton: Button

    fun onCreate() {
        submitButton = findViewById(R.id.submit_button)
        submitButton.setOnClickListener { /* ... */ }
    }
}
Important rules: lateinit only works with var (not val) and only for non-primitive, non-nullable types. If you access it before initialization, you get an UninitializedPropertyAccessException. Always check ::name.isInitialized if unsure.
When to use: Use lateinit for dependency injection (Spring, Koin), test setup (JUnit @Before), and Android view binding where initialization happens in a lifecycle method. If you can use a nullable type instead, prefer that for safety.

Type Aliases

Type aliases give a new name to an existing type. They don't create new types — just more readable names for complex or frequently used type signatures.

Theory

A typealias is resolved at compile time — it does not create a new type, just an alternative name. This means typealias UserId = String and String are fully interchangeable at runtime. Type aliases are especially useful for function types ((T) -> R), generic instantiations with many type parameters, and documenting intent. They can also have visibility modifiers (internal typealias) and can reference generic types with their own parameters (typealias MyMap<K, V> = Map<K, V>).

// Simple aliases for domain concepts
typealias UserId = String
typealias Email = String

// Aliases for complex collection types
typealias Users = List
typealias UserMap = Map

// Usage — much clearer intent
fun fetchUsers(): Users { /* ... */ }
fun findUser(id: UserId): User? { /* ... */ }
// Aliases for function types — great for callbacks
typealias OnClickListener = (View) -> Unit
typealias ApiCallback = (Result) -> Unit

class Button {
    fun setOnClickListener(listener: OnClickListener) { /* ... */ }
}

fun fetchData(callback: ApiCallback) { /* ... */ }
Why this matters: Type aliases make your code self-documenting. fun process(users: Users) is clearer than fun process(users: List<User>). They're especially valuable for nested generics and lambda types in Android callbacks.

Duration (Kotlin 1.6+)

Kotlin provides a type-safe Duration API for working with time intervals. It prevents mixing seconds and milliseconds by accident.

import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Duration.Companion.milliseconds

val timeout = 30.seconds          // Duration
val half = 500.milliseconds       // Duration

// Arithmetic and comparison
val total = timeout + half        // 30.5 seconds
val isLong = timeout > 1.seconds  // true

// Convert
val millis = timeout.inWholeMilliseconds  // 30000

// Use with APIs
fun delayFor(duration: Duration) {
    println("Delaying for $duration")
}
Why use Duration: Passing raw Long values for timeouts leads to bugs — was that 500 seconds or 500 milliseconds? Duration makes the unit explicit at the type level. The compiler won't let you accidentally pass milliseconds where seconds are expected.

Comments

// Single line comment

/*
 * Multi-line comment
 * Just like C, Java, JS
 */

/**
 * KDoc — Kotlin's documentation comment
 * Used to generate API docs
 * @param name The user's name
 */

Try It Yourself

Declare a val for your name, a var for your age, and print a sentence using a string template that says:

"My name is [name] and next year I will be [age+1] years old."

// Your code here:




// Run it at play.kotlinlang.org
Stuck? Show hint
Use val name = "Your Name", var age = 25, and println("My name is $name and next year I will be ${age + 1} years old.")

Concept Check

What will happen if you try to reassign a val?

It will compile but crash at runtime
It will not compile — the IDE shows an error immediately
It will work fine, just like reassigning a JS const in non-strict mode