Skip to main content

Working with Kotlin Fundamentals

1. Declaring Variables

Declaration Type Syntax Description Notes
val val name: String = "Android" Immutable variable (read-only) Cannot be reassigned after initialization
var var count: Int = 0 Mutable variable (read-write) Can be reassigned after initialization

Example: Variable Declaration

val appName: String = "MyApp"
var userAge: Int = 25
userAge = 26 // Valid reassignment
Note: val is preferred for immutable values to improve code safety and readability.

2. Understanding Null Safety (?, !!, ?:, ?.let)

Operator Description Usage Example Notes
? Safety operator to prevent null pointer exceptions val length = str?.length Returns null if str is null, otherwise returns the length
!! Non-null assertion operator to force non-null values val length = str!!.length Throws KotlinNullPointerException if str is null
?: Elvis operator to provide a default value when null val name = user?.name ?: "Unknown" If user?.name is null, returns "Unknown"
?.let Safe call with a block of code to execute if the value is not null user?.let { it.name = "New Name" } Executes the block only if user is not null

Example: Safe Navigation with ?

val user: User? = getUser()
val email = user?.email // Returns null if user is null

Example: Using the Elvis Operator ?:

val name = user?.name ?: "Anonymous"
println(name) // Prints "Anonymous" if user.name is null

Example: Using ?.let

user?.let {
    println("User name: ${it.name}")
    it.updateLastLogin()
}
Note: Kotlin's null safety prevents runtime NullPointerExceptions by requiring explicit handling of nullable types.

3. Using Type Inference and Explicit Types

Feature Description Example
Type Inference Kotlin can deduce the type of a variable from its initializer. val name = "Kotlin"
Explicit Type Declaration Manually specify the type of a variable using colon syntax. val version: String = "1.9"

Example: Type Inference in Action

val number = 42 // Inferred as Int
val text = "Hello" // Inferred as String
val isTrue = true // Inferred as Boolean

Example: Explicit Type Declaration

val number: Int = 42
val text: String = "Hello"
val isTrue: Boolean = true
Note: Type inference reduces verbosity but explicit types improve readability in complex scenarios.

4. Working with String Templates ($variable, ${expression})

String templates in Kotlin allow embedding expressions directly into string literals using the $ symbol.

Feature Description Example
Simple Variable Reference a variable directly using $variableName.
val name = "Alice"
val greeting = "Hello, $name!"
println(greeting) // Output: Hello, Alice!
Expression Embed expressions inside ${expression}.
val a = 5
val b = 10
val result = "Sum of $a and $b is ${a + b}"
println(result) // Output: Sum of 5 and 10 is 15
Function Call Call functions inside string templates.
fun getGreeting(name: String) = "Hello, $name!"
val message = "Message: ${getGreeting("Bob")}"
println(message) // Output: Message: Hello, Bob!

Example: Escaping Dollar Sign

val price = 100
val message = "Price is $$price" // Escaping the dollar sign
println(message) // Output: Price is $100

5. Creating Data Classes

Data classes in Kotlin are a concise way to create classes that primarily hold data. They automatically generate common methods like toString(), equals(), hashCode(), and copy().

Feature Description Example
data class declaration Declares a data class using the data keyword. data class Person(val name: String, val age: Int)
primary constructor Must have at least one parameter in the primary constructor. data class User(val id: Int, val username: String)
component functions Generated for each property to enable destructuring. val (name, age) = person
copy() Creates a copy of the instance with optional modifications. val updatedPerson = person.copy(age = 30)

Example: Using Data Classes

data class Product(val id: Int, val name: String, val price: Double)

val product1 = Product(1, "Laptop", 999.99)
val product2 = product1.copy(name = "Gaming Laptop", price = 1299.99)

println(product1) // Output: Product(id=1, name=Laptop, price=999.99)
println(product2) // Output: Product(id=1, name=Gaming Laptop, price=1299.99)

6. Using Extension Functions (fun Type.functionName)

Extension functions in Kotlin allow you to add new functionality to existing classes without modifying their source code or using inheritance.

Feature Description Example
Syntax Defines how to declare an extension function fun Type.functionName() { ... }
Usage Can be called as if it were a method of the type val result = list.size()

Example: Adding a method to List

fun List<String>.joinToStringWithComma(): String {
    return this.joinToString(", ")
}

val fruits = listOf("apple", "banana", "cherry")
val result = fruits.joinToStringWithComma()
// Output: "apple, banana, cherry"

Example: Extending a Class with Properties

val String.isPalindrome: Boolean
    get() = this == this.reversed()

val text = "racecar"
println(text.isPalindrome) // true
Note: Extension functions do not actually modify the original class. They are resolved statically at compile time.

7. Implementing Higher-Order Functions

Higher-order functions in Kotlin are functions that take other functions as parameters or return functions. They enable functional programming paradigms and make code more expressive and reusable.

Function Description Example
map Transforms each element of a collection using a provided function.
val numbers = listOf(1, 2, 3, 4)
val doubled = numbers.map { it * 2 }
// Result: [2, 4, 6, 8]
filter Selects elements from a collection based on a predicate function.
val numbers = listOf(1, 2, 3, 4, 5)
val even = numbers.filter { it % 2 == 0 }
// Result: [2, 4]
forEach Executes a provided function for each element in the collection.
val items = listOf("apple", "banana")
items.forEach { println(it) }
fold Accumulates a value by applying a function to each element and an accumulator.
val numbers = listOf(1, 2, 3, 4)
val sum = numbers.fold(0) { acc, num -> acc + num }
// Result: 10
flatMap Maps each element to a collection and flattens the result into a single list.
val lists = listOf(listOf(1, 2), listOf(3, 4))
val flattened = lists.flatMap { it }
// Result: [1, 2, 3, 4]

Example: Using Higher-Order Functions Together

val numbers = listOf(1, 2, 3, 4, 5)
val result = numbers
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .fold(0) { acc, num -> acc + num }

println(result) // Output: 12

8. Using Scope Functions

Function Description Return Value Example
apply Executes a block of code on the receiver object and returns the object itself The receiver object
val stringBuilder = StringBuilder().apply {
    append("Hello")
    append(" World")
}
println(stringBuilder.toString()) // Output: Hello World
also Executes a block of code on the receiver object and returns the object itself The receiver object
val numbers = listOf(1, 2, 3).also {
    println("Processing list: $it")
}
println(numbers) // Output: [1, 2, 3]
let Executes a block of code on the receiver object and returns the result of the block The result of the block
val result = "Hello".let {
    it.uppercase()
}
println(result) // Output: HELLO
run Executes a block of code and returns the result of the block The result of the block
val result = run {
    val a = 10
    val b = 20
    a + b
}
println(result) // Output: 30
takeIf Executes a block of code if the condition is true, otherwise returns null The receiver object or null
val number = 5.takeIf { it > 0 }
println(number) // Output: 5

val negative = (-3).takeIf { it > 0 }
println(negative) // Output: null
takeUnless Executes a block of code if the condition is false, otherwise returns null The receiver object or null
val number = 5.takeUnless { it < 0 }
println(number) // Output: 5

val positive = (3).takeUnless { it < 0 }
println(positive) // Output: null
Note: Scope functions are useful for improving code readability and reducing boilerplate when working with objects in Kotlin.

9. Working with Sealed Classes and Interfaces

Sealed classes in Kotlin are a special kind of class that restricts the inheritance hierarchy, making it possible to define a closed set of subclasses. They are useful for modeling a fixed set of options or states.

Feature Description Notes
Sealed Class Defines a closed set of subclasses Cannot be instantiated directly, only through subclasses
Sealed Interface Defines a closed set of implementations Can be extended only by classes in the same file or module
Inheritance Subclasses must be declared in the same file or module Allows for exhaustive when expressions

Example: Sealed Class Usage

sealed class Result<T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val message: String) : Result<T>()
}

fun processResult(result: Result<String>) {
    when (result) {
        is Result.Success -> println("Success: ${result.data}")
        is Result.Error -> println("Error: ${result.message}")
    }
}

Example: Sealed Interface Usage

sealed interface NetworkState {
    data class Connected(val host: String) : NetworkState
    data class Disconnected(val reason: String) : NetworkState
}

fun handleNetworkState(state: NetworkState) {
    when (state) {
        is NetworkState.Connected -> println("Connected to $host")
        is NetworkState.Disconnected -> println("Disconnected: $reason")
    }
}
Note: Sealed classes and interfaces are particularly useful in when expressions, as the compiler ensures that all possible subclasses are handled.

Key Points

  • Sealed classes and interfaces restrict inheritance to a known set of subclasses
  • They enable exhaustive when expressions, ensuring all cases are handled
  • Subclasses must be declared in the same file or module
  • They provide a safer alternative to open class hierarchies

10. Understanding Object Declarations and Companion Objects

Feature Description Usage
object declaration Creates a singleton instance of a class object MyObject { ... }
companion object Defines static members inside a class class MyClass { companion object { ... } }

Example: Object Declaration

object Logger {
    fun log(message: String) {
        println("LOG: $message")
    }
}

Logger.log("Application started") // Output: LOG: Application started

Example: Companion Object

class Person {
    companion object {
        const val DEFAULT_AGE = 18
        fun createWithDefaultAge(): Person {
            return Person(DEFAULT_AGE)
        }
    }

    constructor(age: Int) {
        this.age = age
    }

    private var age: Int
}

val person = Person.createWithDefaultAge()
println(person.age) // Output: 18
Note: Companion objects are initialized when the class is first loaded.

Object Declaration vs Companion Object

  • Object Declaration: Singleton instance, no instantiation needed
  • Companion Object: Part of a class, accessible via ClassName.Companion

11. Using Inline Functions

Property Description Notes
inline Marks a function as inline Allows the compiler to inline the function body at the call site
noinline Prevents inlining of a specific parameter Useful for lambda parameters that should not be inlined
crossinline Allows use of return inside a lambda Required when using return in inline functions with lambdas

Example: Basic Inline Function

inline fun<T> repeat(times: Int, body: (Int) -> T): List<T> {
    val result = mutableListOf<T>()
    for (i in 0 until times) {
        result.add(body(i))
    }
    return result
}

val numbers = repeat(3) { i -> i * 2 }
println(numbers) // [0, 2, 4]

Example: Using noinline

inline fun processWithCallback(
    data: String,
    noinline callback: (String) -> Unit
) {
    println("Processing: $data")
    callback(data.uppercase())
}

processWithCallback("hello") { result ->
    println("Result: $result")
}

Example: Using crossinline

inline fun withReturn(
    crossinline block: () -> Unit
) {
    block()
}

fun main() {
    withReturn {
        return@withReturn // This is allowed due to crossinline
    }
}
Note: Inline functions improve performance by reducing function call overhead but increase code size. Use them judiciously.

12. Working with Type Aliases

Type aliases in Kotlin provide a way to give a type a different name, which can improve code readability and maintainability. They are particularly useful when dealing with complex generic types or long type names.

Alias Declaration Usage Notes
typealias TypeName = OriginalType typealias MapOfStrings = Map<String, String> Creates a new name for an existing type

Example: Defining a Type Alias

typealias MapOfStrings = Map<String, String>

fun processMap(map: MapOfStrings) {
    map.forEach { (key, value) -> println("$key: $value") }
}

Example: Using Type Aliases for Complex Types

typealias UserMap = Map<String, List<User>>

data class User(val name: String, val age: Int)

fun processUsers(users: UserMap) {
    users.forEach { (key, userList) -> 
        println("Group: $key")
        userList.forEach { user -> println("  ${user.name}") }
    }
}
Note: Type aliases do not create new types; they are just alternative names for existing types. The compiler treats them as the same type.

Was this article helpful?