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
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
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
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. |
|
| Expression | Embed expressions inside ${expression}. |
|
| Function Call | Call functions inside string templates. | |
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
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. | |
| filter | Selects elements from a collection based on a predicate function. | |
| forEach | Executes a provided function for each element in the collection. | |
| fold | Accumulates a value by applying a function to each element and an accumulator. | |
| flatMap | Maps each element to a collection and flattens the result into a single list. | |
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 | |
| also | Executes a block of code on the receiver object and returns the object itself | The receiver object | |
| let | Executes a block of code on the receiver object and returns the result of the block | The result of the block | |
| run | Executes a block of code and returns the result of the block | The result of the block | |
| takeIf | Executes a block of code if the condition is true, otherwise returns null | The receiver object or null | |
| takeUnless | Executes a block of code if the condition is false, otherwise returns null | The receiver object or null | |
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")
}
}
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
whenexpressions, 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
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
}
}
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}") }
}
}