Skip to content

Kotlin Folha de referência

Linguagem JVM moderna, concisa e totalmente interoperável com Java.

01

Noções Básicas

Variáveis e Tipos

Prefira val (imutável) em vez de var (mutável). Kotlin infere tipos, mas permite anotações explícitas. Use 'is' para verificações de tipo (smart-casts automaticamente). Tudo é um objeto—sem primitivos na sintaxe Kotlin.

kotlin
val name = "Alice"   // immutable (preferred)
var age = 30          // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List<Int> = listOf(1, 2, 3)
println(name::class)  // class kotlin.String
println(age is Int)   // true

String Templates

$var para variáveis simples, ${expr} para expressões. Strings com aspas triplas preservam novas linhas—use trimIndent() para formatação limpa. String templates tornam a construção de strings concisa e legível sem format strings.

kotlin
val name = "Alice"
val age = 30
println("Name: $name, Age: $age")  // simple var
println("Length: ${name.length}")  // expression
println("Upper: ${name.uppercase()}")
val multiline = """
  Hello, $name
  Age: $age
""".trimIndent()

Null Safety

Null safety do Kotlin: ? marca tipos nullable, ?. safe call (retorna null se null), !! afirma non-null (lança NPE se null), ?: elvis (padrão se null). Isso elimina NullPointerException em tempo de compilação—recurso central do Kotlin.

kotlin
var name: String? = null  // nullable type
println(name?.length)     // null (safe call)
name = "Alice"
println(name!!.length)    // 5 (non-null assertion)
val len: Int = name?.length ?: 0  // elvis operator
// Non-null types can never be null
val s: String = "hi"  // not nullable

Conversão de Tipos

Kotlin exige conversão explícita de tipos (sem widening implícito como Java). Use toInt()/toDouble()/toString() para conversão. toIntOrNull() retorna null em falha—mais seguro que toInt() que lança. Sempre trate null de parsing.

kotlin
val n: Int = 42
val d: Double = n.toDouble()
val s: String = n.toString()
val fromStr: Int? = "100".toIntOrNull()
val invalid: Int? = "abc".toIntOrNull()  // null
println(d, s, fromStr, invalid)

Verificações de Tipo e Smart Cast

is verifica o tipo e smart-casts a variável naquela ramificação—sem cast explícito necessário. !is é a negação. Use as para casts inseguros (lança ClassCastException), as? para casts seguros (retorna null em falha). Smart cast é uma grande conveniência do Kotlin.

kotlin
fun describe(x: Any): String =
    when (x) {
        is Int -> "Int: ${x + 1}"  // x smart-cast to Int
        is String -> "String of length ${x.length}"
        is List<*> -> "List with ${x.size} items"
        else -> "Unknown"
    }
println(describe(42))      // Int: 43
println(describe("hello")) // String of length 5
02

Strings

Métodos Comuns de String

Strings Kotlin têm funções de extensão ricas da biblioteca padrão. A maioria retorna novas strings (imutáveis). Use-as em vez de loops manuais. Funções como uppercase() são locale-aware—use uppercase(Locale.ROOT) para resultados consistentes.

kotlin
val s = "Hello, World"
println(s.length)          // 12
println(s.uppercase())     // HELLO, WORLD
println(s.lowercase())     // hello, world
println(s.split(", "))     // [Hello, World]
println(s.replace("o", "0"))  // Hell0, W0rld
println(s.reversed())      // dlroW ,olleH
println(s.startsWith("Hello"))  // true

Strings Multilinha e Raw

Strings com aspas triplas são raw—sem sequências de escape necessárias (exceto aspas triplas duplas). trimMargin('|') mantém apenas texto após | para indentação limpa. trimIndent() remove whitespace comum. Ideal para SQL, JSON, HTML em código.

kotlin
val text = """
    |Hello,
    |World!
""".trimMargin()  // | marks line start
println(text)
val json = """{"name": "Alice", "age": 30}"""
println(json)  // no escape needed for quotes
// trimIndent() removes common leading whitespace

Construção de Strings

joinToString é a maneira idiomática de juntar coleções com separadores, prefix, postfix. buildString fornece um escopo StringBuilder para construção concisa. Evite + repetido em loops—use joinToString ou StringBuilder para eficiência.

kotlin
val parts = listOf("apple", "banana", "cherry")
println(parts.joinToString(", "))  // apple, banana, cherry
println(parts.joinToString(prefix="[", postfix="]"))
val sb = StringBuilder()
for (p in parts) sb.append(p).append(" ")
println(sb.toString().trim())
// buildString for concise building
val result = buildString {
    parts.forEach { append(it).append(";") }
}

String para Número

toInt()/toDouble() lançam NumberFormatException em entrada inválida. toIntOrNull() retorna null—combine com ?: para padrões seguros. toBooleanStrict() aceita apenas 'true'/'false'. Use variantes OrNull para entrada de usuário ou dados não confiáveis.

kotlin
val n = "42".toInt()           // 42 (throws if invalid)
val d = "3.14".toDouble()      // 3.14
val safe = "abc".toIntOrNull() // null
val withDefault = "x".toIntOrNull() ?: 0  // 0
val bool = "true".toBooleanStrict()  // true
println(n, d, safe, withDefault)

Regex

A classe Regex envolve o Pattern do Java. find retorna MatchResult? (primeira correspondência), findAll retorna todas. matches verifica correspondência completa, containsMatchIn verifica parcial. Use strings com aspas triplas para padrões para evitar double-escaping de barras invertidas.

kotlin
val email = Regex("[\w.]+@[\w]+\.[a-z]+")
val text = "Contact: [email protected]"
val match = email.find(text)
println(match?.value)  // [email protected]
println(email.matches("[email protected]"))  // true
val replaced = text.replace(Regex("\d+"), "#")
println(email.containsMatchIn(text))  // true
03

Estruturas de Dados

List

listOf cria listas imutáveis, mutableListOf para mutáveis. Listas são indexadas a partir de zero. Use first()/last() para extremidades (lançam em vazio), firstOrNull() para segurança. contains verifica pertinência. Prefira listas imutáveis a menos que precise modificar.

kotlin
val nums = listOf(1, 2, 3)  // immutable
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)
mutable[0] = 0
println(nums.size)         // 3
println(nums.first())      // 1
println(nums.last())       // 3
println(nums.contains(2))  // true
println(nums.indexOf(2))   // 1

Map

mapOf cria maps imutáveis, mutableMapOf para mutáveis. 'to' infix cria pairs. Acesso [] retorna nullable V? (null se ausente). Use getOrDefault ou getValue (lança se ausente). Itere com destructuring (k, v).

kotlin
val ages = mapOf("Alice" to 30, "Bob" to 25)
println(ages["Alice"])          // 30
println(ages.getOrDefault("Eve", 0))  // 0
println(ages.containsKey("Alice"))    // true
for ((k, v) in ages) println("$k: $v")
val mutable = mutableMapOf("a" to 1)
mutable["b"] = 2  // add/update

Set

setOf cria sets imutáveis, mutableSetOf para mutáveis. union/intersect/subtract retornam novos sets. Sets impõem unicidade—add retorna false se o elemento existe. Use para desduplicação e operações de conjunto. LinkedHashSet preserva ordem de inserção.

kotlin
val a = setOf(1, 2, 3)
val b = setOf(3, 4, 5)
println(a union b)        // [1,2,3,4,5]
println(a intersect b)    // [3]
println(a subtract b)     // [1,2]
println(a.contains(2))    // true
val mutable = mutableSetOf(1, 2)
mutable.add(3)  // true if added

Array e Primitive Arrays

arrayOf cria Array<T> (boxed para primitivos). Use intArrayOf/doubleArrayOf para arrays primitivos unboxed (melhor desempenho). Array(size) { init } cria com uma função init. Arrays são mutáveis; prefira List para a maioria dos casos de uso.

kotlin
val arr = arrayOf(1, 2, 3)  // Array<Int>
arr[0] = 10
println(arr.size)  // 3
// Specialized arrays for primitives (no boxing)
val ints = intArrayOf(1, 2, 3)
val doubles = doubleArrayOf(1.0, 2.0)
// Array constructor
val squares = Array(5) { it * it }  // [0,1,4,9,16]
println(squares.toList())

Pair e Triple

Pair e Triple agrupam 2-3 valores. 'to' infix cria Pairs (comum para maps). Destruture com val (a, b) = pair. Para mais de 3 valores ou campos nomeados, use data classes—são mais claras e sustentáveis.

kotlin
val pair = "Alice" to 30  // Pair<String, Int>
println(pair.first)   // Alice
println(pair.second)  // 30
val (name, age) = pair  // destructuring
val triple = Triple(1, "a", 2.0)
println(triple.first, triple.third)
// Useful for returning two values
fun minMax(list: List<Int>): Pair<Int, Int> =
    Pair(list.min(), list.max())
04

Fluxo de Controle

If / Else (Expressão)

Em Kotlin, if/else é uma expressão que retorna um valor. Isso substitui o operador ternário. Ambas as ramificações devem retornar tipos compatíveis. Use para atribuição condicional concisa. Como statement (sem else), retorna Unit.

kotlin
val score = 85
val grade = if (score >= 90) "A"
            else if (score >= 80) "B"
            else if (score >= 70) "C"
            else "F"
println(grade)  // B
// if returns a value—no ternary needed
// Both branches must have compatible types

When (Switch)

when é o poderoso switch do Kotlin—suporta literais, ranges (in), negação (!in), múltiplos valores (vírgula) e is-checks. Deve ser exaustivo quando usado como expressão com tipos sealed. Sem um subject, é uma cadeia if/else mais limpa.

kotlin
val n = 2
val label = when (n) {
    0 -> "zero"
    1, 2, 3 -> "small"
    in 4..10 -> "medium"
    !in 1..100 -> "out of range"
    else -> "large"
}
println(label)  // small
// When without subject = multi-condition if
when {
    n > 0 -> println("positive")
    n < 0 -> println("negative")
    else -> println("zero")
}

For Loops e Ranges

.. é range inclusivo, until é exclusivo, downTo conta regressivamente, step define incremento. withIndex() dá iteração indexada. for-in funciona com qualquer Iterable. Esses ranges são concisos e legíveis para loops de contagem.

kotlin
for (i in 0 until 5) print(i)  // 01234 (excludes 5)
for (i in 1..5) print(i)      // 12345 (includes 5)
for (i in 5 downTo 1) print(i)  // 54321
for (i in 1..10 step 2) print(i)  // 13579
val list = listOf("a", "b", "c")
for ((index, value) in list.withIndex()) {
    println("$index: $value")
}

While e Do-While

while verifica antes, do-while verifica depois (executa pelo menos uma vez). Ambos requerem estado mutável. Prefira loops for ou operações funcionais (map, filter) para imutabilidade. Use while para loops dirigidos por condição onde a contagem não é conhecida.

kotlin
var count = 0
while (count < 3) {
    println(count)
    count++
}
var j = 0
do {
    println(j)
    j++
} while (j < 3)
// do-while runs at least once
// Prefer for loops when possible

Break e Continue (Labels)

continue pula para a próxima iteração, break sai do loop. Labels (@outer) habilitam sair de loops aninhados—defina com label@ antes do loop, break/continue com @label. Use com moderação; refatorar para funções é frequentemente mais limpo.

kotlin
for (i in 1..5) {
    if (i == 3) continue  // skip 3
    if (i == 5) break     // stop at 5
    println(i)  // 1, 2, 4
}
// Labeled breaks for nested loops
outer@ for (i in 1..3) {
    for (j in 1..3) {
        if (i == 2 && j == 2) break@outer
        println("$i,$j")
    }
}
05

Funções e Lambdas

Definição de Função

Funções usam fun name(params): ReturnType. Corpo de expressão (= expr) para expressões únicas. Parâmetros padrão e argumentos nomeados suportados. Tipo de retorno Unit = apenas efeito colateral. Parâmetros padrão reduzem a necessidade de overloading.

kotlin
fun add(a: Int, b: Int): Int = a + b  // expression body
fun greet(name: String, greeting: String = "Hello"): String {
    return "$greeting, $name!"
}
println(add(3, 4))           // 7
println(greet("Alice"))      // Hello, Alice!
println(greet("Bob", greeting = "Hi"))  // named arg
fun log(msg: String): Unit = println(msg)  // Unit = void

Lambdas e Ordem Superior

Lambdas: { params -> body }. 'it' é abreviação para um único parâmetro. Passe lambdas para funções de ordem superior como map/filter/reduce. Sintaxe trailing lambda: se o último parâmetro é uma função, pode ir fora de (). Isso é Kotlin idiomático.

kotlin
val square: (Int) -> Int = { x -> x * x }
println(square(5))  // 25
val nums = listOf(1, 2, 3)
println(nums.map { it * 2 })      // [2, 4, 6]
println(nums.filter { it > 1 })   // [2, 3]
println(nums.reduce { a, b -> a + b })  // 6
// 'it' is the implicit single parameter

Extension Functions

Extension functions adicionam métodos a classes existentes (mesmo do Java) sem herança ou modificação. São syntactic sugar para funções estáticas. 'this' refere-se ao receptor. Use para tornar APIs fluentes. Resolvidas em tempo de compilação (sem dispatch dinâmico).

kotlin
fun String.shout(): String = this.uppercase() + "!"
fun Int.isEven(): Boolean = this % 2 == 0
println("hello".shout())  // HELLO!
println(4.isEven())       // true
// Extensions don't modify the class—they're resolved statically
// Use to add utility methods to any type

Inline e Infix

inline copia o corpo da função para call sites (elimina overhead de lambda). Use para funções de ordem superior em caminhos quentes. infix habilita chamadas de função sem ponto/parênteses (a op b)—use para DSLs e operações legíveis como 'to', 'in', 'and'.

kotlin
inline fun measure(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    println("Took ${System.currentTimeMillis() - start}ms")
}
measure { println("working...") }
// Infix functions for readable calls
infix fun Int.times(str: String): String = str.repeat(this)
println(3 times "ab")  // ababab

Vararg e Spread

vararg aceita argumentos variáveis (coletados em um array). * espalha um array em vararg. Parâmetros nomeados podem seguir vararg. Use para APIs flexíveis como listOf(), funções estilo printf. O operador spread é o equivalente Kotlin do spread do JS.

kotlin
fun sum(vararg nums: Int): Int = nums.sum()
println(sum(1, 2, 3, 4))  // 10
// Spread operator to pass array
val arr = intArrayOf(1, 2, 3)
println(sum(*arr))  // 6
// Named/default params can follow vararg
fun greet(vararg names: String, greeting: String = "Hi") =
    names.joinToString { "$greeting, $it!" }
println(greet("A", "B"))
06

Classes e POO

Classe e Construtor

O construtor primário está no header da classe. Parâmetros val/var tornam-se propriedades (com getters/setters). Sem val/var, são apenas parâmetros de construtor (não acessíveis). O bloco init é executado durante a construção. Kotlin gera getters/setters automaticamente.

kotlin
class Person(val name: String, val age: Int) {
    fun greet(): String = "Hi, I'm $name"
    fun isAdult(): Boolean = age >= 18
}
val p = Person("Alice", 30)
println(p.greet())   // Hi, I'm Alice
println(p.name)      // Alice (val = property)
println(p.isAdult()) // true
// val/var in constructor = auto property

Data Class

data class auto-gera equals, hashCode, toString, copy e componentN (para destructuring). Use para holders de dados. Deve ter pelo menos um parâmetro val/var. copy() cria um clone modificado—ótimo para atualizações imutáveis. Destruture com val (a, b) = point.

kotlin
data class Point(val x: Int, val y: Int)
val p1 = Point(3, 4)
val p2 = Point(3, 4)
println(p1 == p2)        // true (value equality)
println(p1.copy(x = 5))  // Point(x=5, y=4)
println(p1)              // Point(x=3, y=4)
// Auto: equals, hashCode, toString, copy, componentN
val (x, y) = p1  // destructuring via componentN()

Sealed Class

Sealed classes restringem subtipos ao mesmo arquivo/módulo kotlin. Expressões when são exaustivas (compilador verifica todos os cases). Use para ADTs (Algebraic Data Types) representando estados finitos. Combinado com when, isso habilita pattern matching seguro e verificado em compilação.

kotlin
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
object Loading : Result()
fun handle(r: Result): String = when (r) {
    is Success -> "Got ${r.value}"
    is Failure -> "Error: ${r.error}"
    Loading -> "Loading..."
}
// when is exhaustive—compiler enforces all cases

Object e Companion

object declara um singleton (uma instância, inicializada lazy). companion object dentro de uma classe contém membros 'static' (acessados via ClassName.member). Use object para singletons, companion para factory methods e constantes. Companion pode implementar interfaces.

kotlin
object Config {  // singleton
    val version = "1.0"
    fun load() = mapOf("key" to "value")
}
println(Config.version)  // 1.0
class User(val name: String) {
    companion object {
        fun create(name: String) = User(name)
        const val MAX = 100
    }
}
val u = User.create("Alice")  // like static method

Herança e Interface

Classes são final por padrão—use 'open' para permitir herança. 'override' é obrigatório. Interfaces podem ter implementações padrão. Uma classe estende uma classe, mas implementa múltiplas interfaces. Use abstract para implementação parcial. Prefira composição sobre herança.

kotlin
open class Animal(val name: String) {
    open fun speak() = "..."
}
class Dog(name: String) : Animal(name) {
    override fun speak() = "Woof"
}
interface Greetable {
    val name: String
    fun greet(): String  // can have default impl
}
class Person(override val name: String) : Greetable {
    override fun greet() = "Hi, $name"
}
07

Null Safety e Scope Functions

Safe Calls e Elvis

?. safe call (retorna null se o receptor é null), ?: elvis (fornece padrão). Encadeie ?. para acesso profundo (user?.address?.city). Use ?: return/throw para saídas antecipadas. Esses tornam o tratamento de null conciso e seguro—sem verificações explícitas de null necessárias.

kotlin
var name: String? = "Alice"
println(name?.length)      // 5
name = null
println(name?.length)      // null
println(name?.length ?: 0) // 0 (elvis: default if null)
println(name?.length ?: return)  // early return
// Safe call chain
val city: String? = user?.address?.city

let (Verificação de Null)

let executa um bloco apenas se o valor for non-null. 'it' é o valor non-null dentro do bloco. Use para operações com guarda de null em vez de verificações if-null. Padrão comum: value?.let { ... } para processamento seguro. Retorna o resultado do bloco.

kotlin
var name: String? = "Alice"
name?.let {
    println("Name is $it")  // only runs if not null
    println("Length: ${it.length}")  // it is non-null
}
// Common for null-guarded blocks
val result = name?.let {
    process(it)  // it: String (non-null)
} ?: "default"

apply e also

apply configura um objeto (retorna o objeto, receptor 'this')—ideal para builders. also realiza efeitos colaterais (retorna o objeto, parâmetro 'it')—bom para logging/depuração em cadeias. Ambos retornam o objeto original, habilitando cadeias fluentes.

kotlin
val list = mutableListOf<Int>().apply {
    add(1); add(2); add(3)  // 'this' = the list
    println("Built $size items")
}
val config = Config().also {
    it.timeout = 30  // 'it' = config
    println("Configured")
}
// apply: configure and return object (this)
// also: side effects, return object (it)

run e with

run executa um bloco em um objeto (retorna resultado do bloco, receptor 'this')—use para transformar um objeto. with é como run, mas recebe o objeto como parâmetro (não encadeável em nullables). Use para agrupar operações no mesmo objeto.

kotlin
val result = "Hello".run {
    length  // 'this' = string, returns last expr
}  // 5
val r2 = with(StringBuilder()) {
    append("a"); append("b")
    toString()  // returns this
}
// run: object.run { } - returns block result
// with: with(obj) { } - returns block result (not chainable)

TakeIf e TakeUnless

takeIf retorna o objeto se o predicado for verdadeiro, null caso contrário. takeUnless é o oposto. Use para filtragem condicional em cadeias—evita verificações if separadas. Combine com ?: para padrões. Elegante para pipelines de validação.

kotlin
val age = 25
val valid = age.takeIf { it >= 18 }  // 25 (if true)
val invalid = age.takeIf { it < 18 }  // null (if false)
val adult = age.takeUnless { it < 18 }  // 25
// Useful for filtering in chains
val email = getUser()?.email?.takeIf { it.contains("@") }
println(valid, invalid, adult)
08

Coleções e Funcional

Map / Filter / Fold

map transforma, filter seleciona, reduce/fold agrega. 'it' é o elemento implícito. fold recebe uma seed; reduce não (lança em vazio). Esses são o núcleo do processamento funcional de coleções—use em vez de loops para clareza.

kotlin
val nums = listOf(1, 2, 3, 4, 5)
println(nums.map { it * 2 })          // [2,4,6,8,10]
println(nums.filter { it % 2 == 0 })  // [2,4]
println(nums.reduce { a, b -> a + b }) // 15
println(nums.fold(0) { a, b -> a + b }) // 15
println(nums.sum())                   // 15
println(nums.joinToString(", "))      // 1, 2, 3, 4, 5

FlatMap e GroupBy

flatten remove um nível de aninhamento. flatMap mapeia e aplaina em um passo—essencial para transformações aninhadas. groupBy particiona por uma chave em um Map. Esses são poderosos para processamento de dados e pipelines de análise.

kotlin
val nested = listOf(listOf(1, 2), listOf(3, 4))
println(nested.flatten())         // [1,2,3,4]
println(nested.flatMap { it.map { n -> n * 2 } })  // [2,4,6,8]
val words = listOf("apple", "bat", "ant")
val byFirst = words.groupBy { it.first() }
// {a=[apple, ant], b=[bat]}
println(byFirst)

Ordenação

sorted/sortedDescending ordenam naturalmente. sortedBy/sortedByDescending ordenam por um seletor de chave. Esses retornam novas listas (imutáveis). Para listas mutáveis, use sort/sortBy (in-place). Use seletores de chave para ordenar por um campo específico.

kotlin
val nums = listOf(3, 1, 4, 1, 5)
println(nums.sorted())           // [1,1,3,4,5]
println(nums.sortedDescending()) // [5,4,3,1,1]
val people = listOf("Alice" to 30, "Bob" to 25)
val byAge = people.sortedBy { it.second }  // by age
println(byAge)
val byNameDesc = people.sortedByDescending { it.first }
println(byNameDesc)

Sequences (Lazy)

Sequences são lazy—operações são adiadas até uma operação terminal (toList, sum, count). Evita coleções intermediárias para melhor desempenho em dados grandes. Use asSequence() para pipelines de múltiplos passos em coleções grandes. Como Java Streams.

kotlin
val nums = (1..1000000).toList()
// Eager: creates intermediate lists
val eager = nums.filter { it % 2 == 0 }.map { it * 2 }.take(5)
// Lazy: no intermediate collections
val lazy = nums.asSequence()
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .take(5)
    .toList()  // forces evaluation
println(lazy)  // [4, 8, 12, 16, 20]

Partition e Chunked

partition divide em duas listas por um predicado (retorna Pair). chunked divide em listas de tamanho fixo. windowed cria janelas deslizantes. Esses são úteis para batching, paginação e algoritmos de janela deslizante. Todos retornam novas coleções.

kotlin
val nums = listOf(1, 2, 3, 4, 5)
val (evens, odds) = nums.partition { it % 2 == 0 }
println(evens)  // [2, 4]
println(odds)   // [1, 3, 5]
val chunked = nums.chunked(2)
println(chunked)  // [[1, 2], [3, 4], [5]]
val windowed = nums.windowed(3)
println(windowed)  // [[1,2,3], [2,3,4], [3,4,5]]
09

Coroutines e Async

Launch (Fire e Forget)

launch inicia uma coroutine que não retorna um resultado (fire-and-forget). delay é non-blocking (diferente de Thread.sleep). runBlocking faz ponte entre código sync/async (use em main/testes). Coroutines são leves—milhares podem executar em poucas threads.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    launch {
        delay(1000)
        println("World!")  // after 1s
    }
    println("Hello")  // immediately
    // launch doesn't return a result
    // Use for side-effect coroutines
}
// Output: Hello, then World! after 1s

Async e Await

async inicia uma coroutine que retorna um Deferred<T>. await() suspende até o resultado estar pronto. Inicie múltiplos asyncs antes de aguardar para paralelismo. Use para computações concorrentes que produzem resultados. Como Promise/Future em outras linguagens.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val deferred = async {
        delay(1000)
        42  // return value
    }
    val result = deferred.await()
    println(result)  // 42
    // Parallel execution
    val a = async { computeA() }
    val b = async { computeB() }
    println(a.await() + b.await())  // concurrent
}

Suspend Functions

suspend functions podem pausar e retomar sem bloquear threads. Elas só podem ser chamadas de coroutines ou outras suspend functions. coroutineScope fornece um escopo estruturado (espera por todos os filhos). Use para APIs async—faz código async parecer síncrono.

kotlin
import kotlinx.coroutines.*
suspend fun fetchUser(id: Int): String {
    delay(500)  // simulates network
    return "User $id"
}
suspend fun fetchAll(): List<String> = coroutineScope {
    val a = async { fetchUser(1) }
    val b = async { fetchUser(2) }
    listOf(a.await(), b.await())
}
// suspend functions can only be called from coroutines

Flow (Cold Stream)

Flow é o cold async stream do Kotlin (como RxJava Observable). Valores são produzidos na coleta. Use operadores map/filter/reduce. emit produz, collect consome. Ideal para streaming de dados, eventos ou APIs paginadas. Hot streams usam SharedFlow/StateFlow.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun numbers(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(100)
        emit(i)  // produce value
    }
}
fun main() = runBlocking {
    numbers().collect { println(it) }  // 1,2,3,4,5
    // Transform
    numbers().map { it * 2 }.filter { it > 4 }.collect {
        println(it)  // 6, 8, 10
    }
}

Dispatchers e Context

Dispatchers escolhem o thread pool: Main para UI, IO para rede/arquivo (pool grande), Default para trabalho CPU (contagem de núcleos). withContext troca o contexto dentro de uma coroutine. Use o dispatcher certo para evitar bloquear UI ou esfaimar thread pools.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    launch(Dispatchers.Main) { /* UI thread */ }
    launch(Dispatchers.IO) {
        // Network/file I/O (thread pool)
        val data = fetchData()
    }
    launch(Dispatchers.Default) {
        // CPU-intensive work
        val result = heavyCompute()
    }
    // withContext switches dispatcher
    val data = withContext(Dispatchers.IO) { readFile() }
}
10

Tratamento de Erros e E/S

Try / Catch / Finally

try/catch/finally é como Java, mas try é uma expressão retornando um valor. Sem checked exceptions—todas são unchecked. Capture exceções específicas para tratamento direcionado. Use finally para limpeza. Prefira Result ou retornos nullable para falhas esperadas.

kotlin
val result = try {
    "abc".toInt()
} catch (e: NumberFormatException) {
    0  // fallback
} finally {
    println("cleanup")
}
println(result)  // 0
// try is an expression—returns a value
// Kotlin doesn't have checked exceptions

Tipo Result

Result<T> envolve sucesso ou falha (como Try em Scala). runCatching converte exceções em Result. getOrNull/getOrElse para acesso seguro. onSuccess/onFailure para callbacks. Use para erros esperados em vez de exceções—tratamento de erros funcional mais limpo.

kotlin
fun parse(s: String): Result<Int> =
    runCatching { s.toInt() }
val r = parse("42")
println(r.getOrNull())    // 42
println(r.getOrElse { 0 }) // 42
r.onSuccess { println("OK: $it") }
    .onFailure { println("Err: ${it.message}") }
val r2 = parse("abc")
println(r2.getOrNull())   // null

Exceções Customizadas

Exceções customizadas estendem Exception (ou uma subclasse). Passe uma message para depuração. Capture exceções específicas antes de genéricas (a ordem importa). Use exceções para casos verdadeiramente excepcionais; para falhas esperadas, prefira Result ou retornos nullable.

kotlin
class InvalidAgeException(message: String) : Exception(message)
fun setAge(age: Int) {
    if (age < 0) throw InvalidAgeException("Age cannot be negative: $age")
}
try {
    setAge(-5)
} catch (e: InvalidAgeException) {
    println("Caught: ${e.message}")
} catch (e: Exception) {
    println("Other: ${e.message}")
}

E/S de Arquivo

Kotlin usa File do Java com funções de extensão. writeText/readText para casos simples. useLines faz streaming de linhas (auto-fecha, eficiente em memória). Para arquivos grandes, use bufferedReader(). Sempre feche recursos—use bloco use { } para auto-fechamento.

kotlin
import java.io.File
// Write
File("test.txt").writeText("Hello, File!")
// Read
val content = File("test.txt").readText()
println(content)  // Hello, File!
// Append
File("log.txt").appendText("new line\n")
// Line by line
File("test.txt").useLines { lines ->
    lines.forEach { println(it) }
}

JSON (kotlinx.serialization)

kotlinx.serialization é a biblioteca JSON oficial do Kotlin—segura em tempo de compilação, sem reflection. Anote data classes com @Serializable. encodeToString/decodeFromString para JSON. Use @SerialName para nomes de chave customizados, @Optional para padrões. Alternativa: Gson/Moshi.

kotlin
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class User(val name: String, val age: Int)
val user = User("Alice", 30)
val json = Json.encodeToString(user)
println(json)  // {"name":"Alice","age":30}
val decoded = Json.decodeFromString<User>(json)
println(decoded.name)  // Alice
// Requires @Serializable annotation
11

Coroutines Profundo

Concorrência Estruturada

Concorrência estruturada vincula coroutines filhas a um escopo pai—o pai não conclui até que todos os filhos o façam, e uma falha filha cancela irmãos. Prefira coroutineScope em vez de GlobalScope. Isso previne vazamentos de coroutine e torna o cancelamento previsível.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    // coroutineScope waits for all children
    coroutineScope {
        launch { delay(1000); println("A") }
        launch { delay(500); println("B") }
    }
    println("Done")  // after both complete
    // If a child fails, parent cancels siblings
    // Never use GlobalScope unless necessary
}

Cancelamento e Cancelamento Cooperativo

Cancelamento é cooperativo—coroutines devem verificá-lo em pontos de suspensão (delay, yield, await). Funções suspending cancellable lançam CancellationException. Use ensureActive() ou isActive para verificar. Loops CPU non-suspending não respondem ao cancelamento a menos que você chame yield() ou verifique isActive.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val job = launch {
        repeat(10) { i ->
            delay(300)
            yield()  // explicit suspension point
            println("Working $i")
        }
    }
    delay(700)
    job.cancelAndJoin()  // cancels and waits
    println("Cancelled")
}
// delay/yield check for cancellation
// isActive checks cancellation state

Tratamento de Exceções

Exceções não tratadas em launch propagam para o pai (cancelando irmãos) a menos que um CoroutineExceptionHandler esteja instalado. async adia exceções para await(). Use try/catch ao redor de await. O handler funciona apenas para exceções não capturadas em launch—use SupervisorJob para isolar falhas.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, e ->
        println("Caught: $e")
    }
    val job = launch(handler) {
        throw RuntimeException("Boom!")
    }
    job.join()
    // async exceptions surface on await()
    val deferred = async { throw IllegalStateException() }
    try { deferred.await() } catch (e: Exception) {
        println("Async failed: $e")
    }
}

Supervision (supervisorScope)

supervisorScope (e SupervisorJob) cria um escopo onde a falha de um filho NÃO cancela seus irmãos—cada filho falha independentemente. Use para operações independentes (ex.: múltiplas chamadas de API independentes). coroutineScope regular cancela irmãos na primeira falha (fail-fast).

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    // supervisorScope: child failures don't cancel siblings
    supervisorScope {
        launch { delay(100); throw RuntimeException("A fails") }
        launch { delay(200); println("B still runs") }
    }
    // vs coroutineScope: A's failure cancels B
    // Use SupervisorJob for long-lived services
    val scope = CoroutineScope(SupervisorJob())
}

Channels (estilo CSP)

Channels passam valores entre coroutines (como Go channels). Rendezvous (capacidade 0) sincroniza sender e receiver; buffered permite enfileiramento. send suspende quando cheio, receive quando vazio. Sempre close() produtores. Para fan-out, use BroadcastChannel ou SharedFlow. Prefira Flow para a maioria das necessidades de streaming.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
    val channel = Channel<Int>(3)  // buffered capacity 3
    launch {
        for (i in 1..5) {
            channel.send(i)
            println("Sent $i")
        }
        channel.close()
    }
    // receive() blocks until available
    for (x in channel) println("Got $x")
    // Produce-consume pattern
    // Rendezvous (cap 0) syncs sender/receiver
}
12

Flow Profundo

Operadores de Flow

Operadores de Flow são cold—executam apenas quando coletados. map/filter/take são como Stream/Sequence. transform é o mais flexível (pode emitir múltiplos valores por entrada). Variantes flatMap lidam com flows aninhados: concat (sequencial), merge (concorrente), latest (cancela anterior). Escolha com base em necessidades de ordenação/concorrência.

kotlin
import kotlinx.coroutines.flow.*
fun nums() = flow {
    for (i in 1..5) emit(i)
}
suspend fun main() {
    nums().map { it * it }           // 1,4,9,16,25
        .filter { it > 5 }          // 9,16,25
        .take(2)                    // 9,16
        .collect { println(it) }
    // transform: emit multiple values
    nums().transform { x ->
        emit(x)
        emit(x * 10)
    }.collect { println(it) }
    // flatMapConcat / flatMapMerge / flatMapLatest
}

Buffer e Concorrência

buffer desacopa produtor e consumidor com uma fila de capacidade fixa—útil quando velocidades de produção e consumo diferem. conflate mantém apenas o valor mais recente (descarta intermediários) para atualizações de UI/estado. collectLatest cancela o collector anterior quando um novo valor chega—ideal para search-as-you-type.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun events() = flow {
    for (i in 1..3) {
        delay(100); emit(i)
    }
}
fun main() = runBlocking {
    // Without buffer: producer & consumer alternate (slow)
    events().buffer()  // decouple producer/consumer
        .collect { delay(200); println(it) }
    // conflate: drop intermediate values
    events().conflate().collect { println(it) }
    // collectLatest: cancel previous collection
    events().collectLatest { delay(200); println(it) }
}

StateFlow e SharedFlow (Hot Streams)

StateFlow é um hot stream mantendo um único valor atualizado—use para estado de UI (substitui LiveData). Sempre tem um valor e conflate-s. SharedFlow é um hot broadcast stream (múltiplos collectors, sem valor inicial exigido)—use para eventos. SharedFlow com replay=1 comporta-se como um event bus em cache.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    // StateFlow: holds one value, stateful
    val state = MutableStateFlow(0)
    launch {
        state.collect { println("State: $it") }
    }
    state.value = 1  // update synchronously
    state.value = 2
    // SharedFlow: broadcasts to multiple collectors
    val events = MutableSharedFlow<String>()
    launch { events.collect { println("A: $it") } }
    launch { events.collect { println("B: $it") } }
    events.emit("Hello")  // both receive
}

Tratamento de Exceções de Flow

catch trata exceções upstream e pode emitir valores de fallback. Ele apenas captura exceções emitidas antes dele na cadeia. retry/retryWhen re-coletam o flow em falha (útil para chamadas de rede). Para exceções downstream, envolva collect em try/catch. Nunca capture dentro do builder do flow—deixe propagar.

kotlin
import kotlinx.coroutines.flow.*
fun numbers() = flow {
    emit(1)
    throw RuntimeException("fail")
    emit(2)  // never reached
}
suspend fun main() {
    // catch operator (upstream only)
    numbers().catch { e -> emit(-1) }
        .collect { println(it) }  // 1, -1
    // retry on failure
    numbers().retry(3) { e ->
        println("Retry: $e"); true
    }.collect { println(it) }
    // retryWhen for custom logic
}

flowOn e Context

flowOn troca o dispatcher para o flow upstream (produtor + operadores acima dele). Isso é essencial quando o flow faz E/S bloqueante—envolva-o com flowOn(Dispatchers.IO). O downstream (collect) executa no contexto do chamador. Múltiplas chamadas flowOn criam contextos separados para cada segmento.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun diskFlow() = flow {
    for (i in 1..3) {
        Thread.sleep(100)  // blocking I/O
        emit(i)
    }
}
suspend fun main() {
    // flowOn changes upstream context
    diskFlow().flowOn(Dispatchers.IO)
        .collect { println(it) }
    // Without flowOn, runs on collector's dispatcher
    // flowOn applies to all operators above it
    diskFlow().map { it * 2 }
        .flowOn(Dispatchers.IO)
        .filter { it > 2 }
        .collect { println(it) }
}
13

Sealed Classes e ADTs

Noções Básicas de Sealed Classes

Sealed classes restringem subclasses a um conjunto conhecido (mesmo arquivo/módulo). O compilador conhece todos os tipos possíveis, habilitando expressões when exaustivas sem else. Ideal para modelar estados finitos (Result, UiState, respostas de rede). Combinadas com data classes, elas formam Algebraic Data Types.

kotlin
sealed class Result<out T> {
    data class Success<T>(val value: T) : Result<T>()
    data class Failure(val error: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}
fun handle(r: Result<Int>) = when (r) {
    is Result.Success -> println("Got ${r.value}")
    is Result.Failure -> println("Error: ${r.error}")
    Result.Loading -> println("Loading...")
}
// All subclasses defined in same file/module

Sealed Interfaces (Kotlin 1.5+)

Sealed interfaces (Kotlin 1.5+) estendem sealed para interfaces, permitindo que uma classe implemente múltiplos tipos sealed—mais flexível que sealed classes. A restrição de mesmo arquivo/mesmo módulo se aplica. Útil para modelagem de domínio onde um tipo pertence a múltiplas categorias.

kotlin
sealed interface Shape {
    fun area(): Double
}
data class Circle(val r: Double) : Shape {
    override fun area() = Math.PI * r * r
}
data class Square(val side: Double) : Shape {
    override fun area() = side * side
}
// Sealed interfaces allow multiple inheritance
sealed interface Clickable { fun click() }
class Button : Shape, Clickable {
    override fun area() = 0.0
    override fun click() = println("Clicked")
}

When Exaustivo

When usado como expressão (não statement), o compilador exige todos os cases sealed—sem else necessário. Adicionar uma nova subclasse produz erros de compilação em sites when não tratados, tornando a refatoração segura. Este é o recurso matador de sealed classes para state machines e renderização de UI.

kotlin
sealed class NetworkState {
    object Loading : NetworkState()
    data class Success(val data: String) : NetworkState()
    data class Error(val message: String) : NetworkState()
}
fun render(state: NetworkState): String = when (state) {
    NetworkState.Loading -> "Spinner"
    is NetworkState.Success -> "Data: ${state.data}"
    is NetworkState.Error -> "Error: ${state.message}"
    // No else needed—compiler enforces all cases
}
// Adding a new subclass causes compile error
// until you handle it

Modelagem de ADT (Result/Either)

Sealed classes modelam sum types (Either/Result) para programação orientada a railway. fold trata ambas as ramificações. Diferente de exceções, erros são explícitos na assinatura de tipo. Arrow-kt fornece Either/Validated mais rico. Use para tratamento de erros previsível e composável sem try/catch.

kotlin
sealed class Either<out L, out R> {
    data class Left<out L>(val value: L) : Either<L, Nothing>()
    data class Right<out R>(val value: R) : Either<Nothing, R>()
}
fun <L, R> Either<L, R>.fold(
    ifLeft: (L) -> Unit,
    ifRight: (R) -> Unit
) = when (this) {
    is Either.Left -> ifLeft(value)
    is Either.Right -> ifRight(value)
}
fun divide(a: Int, b: Int): Either<String, Int> =
    if (b == 0) Either.Left("Divide by zero")
    else Either.Right(a / b)

Sealed Classes com Recursão

Sealed classes podem ser recursivas, modelando estruturas de árvore (ASTs, JSON, expressões). Pattern matching com when + recursão as avalia elegantemente. Isso é uso clássico de ADT funcional—type-safe, exaustivo e amigável a refatoração. Usado em compiladores, parsers e DSLs de config.

kotlin
sealed class Expr {
    data class Num(val value: Int) : Expr()
    data class Add(val left: Expr, val right: Expr) : Expr()
    data class Mul(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int = when (e) {
    is Expr.Num -> e.value
    is Expr.Add -> eval(e.left) + eval(e.right)
    is Expr.Mul -> eval(e.left) * eval(e.right)
}
val expr = Expr.Add(Expr.Num(2), Expr.Mul(Expr.Num(3), Expr.Num(4)))
fun main() = println(eval(expr))  // 14
14

Delegated Properties

Delegate lazy

lazy adia a inicialização até o primeiro acesso e faz cache do resultado. O padrão é thread-safe (double-checked locking). Use para recursos caros (configs, conexões de DB, singletons) que podem não ser necessários. Passe o modo NONE para contextos single-threaded para evitar overhead de sincronização.

kotlin
val heavyConfig: Config by lazy {
    println("Initializing...")
    loadConfigFromFile()  // runs once, first access
}
fun main() {
    println("Before access")
    println(heavyConfig)  // initializes here
    println(heavyConfig)  // cached, no re-init
}
// lazy is thread-safe by default (LazyThreadSafetyMode.SYNCHRONIZED)
// Use LazyThreadSafetyMode.NONE for single-threaded

observable e vetoable

observable dispara um callback após cada mudança (logging, efeitos colaterais). vetoable pode rejeitar mudanças retornando false (validação). Ambos recebem um valor inicial e uma lambda. Use para estado reativo, validação ou disparar atualizações de UI. Para cenários mais complexos, use delegates customizados.

kotlin
import kotlin.properties.Delegates
var count: Int by Delegates.observable(0) { _, old, new ->
    println("Changed: $old -> $new")
}
var age: Int by Delegates.vetoable(0) { _, old, new ->
    new >= 0  // reject negative values
}
fun main() {
    count = 1   // prints "Changed: 0 -> 1"
    count = 2   // prints "Changed: 1 -> 2"
    age = 25    // accepted
    age = -5    // rejected, stays 25
}

Custom Property Delegate

Delegates customizados implementam getValue (e setValue para vars) via ReadOnlyProperty/ReadWriteProperty. Eles encapsulam comportamento reutilizável de propriedade (validação, formatação, cache, mapeamento de DB). O thisRef é o owner, prop é o metadata da propriedade. Poderoso para frameworks ORM/serialization.

kotlin
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class TrimmedString : ReadWriteProperty<Any?, String> {
    private var value: String = ""
    override fun getValue(thisRef: Any?, prop: KProperty<*>) = value
    override fun setValue(thisRef: Any?, prop: KProperty<*>, value: String) {
        this.value = value.trim()
    }
}
class User {
    var name: String by TrimmedString()
}
fun main() {
    val u = User()
    u.name = "  Alice  "
    println(u.name)  // "Alice"
}

Propriedades backed por Map

Delegar para um Map permite vincular propriedades a chaves dinâmicas—útil para parsing de JSON, configs ou linhas de ORM sem boilerplate. O nome da propriedade deve corresponder à chave do map. Use MutableMap para propriedades graváveis. É assim que kotlinx.serialization e muitos ORMs funcionam por baixo dos panos.

kotlin
class User(map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
    val email: String? by map
}
fun main() {
    val u = User(mapOf(
        "name" to "Alice",
        "age" to 30,
        "email" to "[email protected]"
    ))
    println(u.name)  // Alice
}
// Mutable version: MutableMap
class MutableUser(map: MutableMap<String, Any?>) {
    var name: String by map
}

Delegate notNull e Singleton

notNull() é como lateinit, mas para qualquer tipo (incluindo primitivos) e funciona com val/var. Lança se acessado antes de definido. Para singletons, prefira object (eager) ou by lazy (lazy). lateinit é para vars em classes apenas; o delegate notNull é mais flexível, mas tem leve overhead.

kotlin
import kotlin.properties.Delegates
class Service {
    // Late-init for non-null, set once
    var config: String by Delegates.notNull()
    // Throws before initialization
}
// Singleton via object (no delegate needed)
object Database {
    val connection = connect()
}
// Or lazy singleton
val db: Database by lazy { Database() }
fun main() {
    val s = Service()
    // println(s.config)  // IllegalStateException
    s.config = "prod"
    println(s.config)  // prod
}
15

Construção de DSL

Type-Safe Builders

Type-safe builders usam function types com receiver (lambda com receiver) para criar DSLs aninhadas. A lambda executa no contexto do receptor (this), então você chama seus métodos diretamente. É assim que o HTML DSL do Kotlin, scripts de build do Gradle e kotlinx.html funcionam—declarativos e verificados em tempo de compilação.

kotlin
class Table {
    private val rows = mutableListOf<Row>()
    fun row(init: Row.() -> Unit) {
        val r = Row(); r.init(); rows.add(r)
    }
    fun build() = rows
}
class Row {
    private val cells = mutableListOf<String>()
    fun cell(text: String) { cells.add(text) }
    fun build() = cells
}
fun table(init: Table.() -> Unit) = Table().apply(init).build()
val t = table {
    row { cell("A"); cell("B") }
    row { cell("C"); cell("D") }
}

@DslMarker (Controle de Escopo)

@DslMarker previne vazamento implícito de receptor—dentro de um bloco DSL aninhado, você só pode chamar métodos do receptor mais interno. Sem ele, Kotlin permitiria chamar métodos de receptores externos, levando a DSLs confusas/buggy. Anote todas as classes DSL com a mesma annotation marker.

kotlin
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(b: Body.() -> Unit) { /*...*/ } }
@HtmlDsl
class Body { fun p(text: String) { /*...*/ } }
fun html(init: HTML.() -> Unit) = HTML().apply(init)
// Without @DslMarker, inner 'this' could call outer methods
html {
    body {
        // p() is unambiguous—Body's method
        // body() would be an error (not Body's method)
        p("Hello")
    }
}

Infix Functions

funções infix habilitam sintaxe de linguagem natural (a to b, 1 until 10). Devem ser funções member ou extension com um único parâmetro. Usadas intensivamente em DSLs, frameworks de teste e bibliotecas matemáticas. Exemplos built-in: to (Pair), until/step (ranges) e operações de coleção.

kotlin
infix fun Int.times(str: String) = str.repeat(this)
fun main() {
    println(3 times "ab")  // "ababab"
    // Equivalent to: 3.times("ab")
    // No dot, no parentheses—reads like natural language
}
// Common in testing
infix fun <T> T.shouldEqual(expected: T) =
    assert(this == expected)
fun test() { 5 shouldEqual 5 }
// Also: to, until, step, in, etc.

Operator Overloading

Operator overloading permite que objetos usem +, -, *, [], (), etc. com sintaxe natural. Marque funções com operator. Sobrecarregue sensatamente—tipos matemáticos (Vec, Matrix, Money) se beneficiam; sobrecarga arbitrária prejudica legibilidade. get/set habilitam indexação; invoke torna objetos callable. Operadores de range (rangeTo, contains) alimentam for-loops.

kotlin
data class Vec(val x: Int, val y: Int) {
    operator fun plus(o: Vec) = Vec(x + o.x, y + o.y)
    operator fun minus(o: Vec) = Vec(x - o.x, y - o.y)
    operator fun times(s: Int) = Vec(x * s, y * s)
    operator fun unaryMinus() = Vec(-x, -y)
    operator fun get(i: Int) = when (i) { 0 -> x; 1 -> y; else -> throw IndexOutOfBoundsException() }
}
fun main() {
    val a = Vec(1, 2)
    val b = Vec(3, 4)
    println(a + b)        // Vec(x=4, y=6)
    println(a * 2)        // Vec(x=2, y=4)
    println(-a)           // Vec(x=-1, y=-2)
    println(a[0])         // 1
}

Function Types com Receiver

Function types com receiver (A.(B) -> C) permitem que lambdas acessem o receptor como 'this'. Esta é a fundação das DSLs do Kotlin e scope functions (apply, run, with). apply retorna o receptor; run retorna o resultado da lambda. Domine isso para construir DSLs internas expressivas e type-safe.

kotlin
// Lambda with receiver: A.(B) -> C
val greet: String.(Int) -> String = { times -> repeat(times) { this + "!" } }
fun main() {
    println("Hello".greet(3))  // Hello!!!Hello!!!Hello!!!
    // 'this' is the String receiver
}
// Build DSLs with receiver
class StringBuilder {
    private val parts = mutableListOf<String>()
    fun add(s: String) { parts.add(s) }
}
fun build(init: StringBuilder.() -> Unit) =
    StringBuilder().apply(init)
val sb = build { add("a"); add("b") }
16

Testes (JUnit 5, MockK, Turbine)

Noções Básicas de JUnit 5

JUnit 5 (Jupiter) é a estrutura de testes padrão do Kotlin. @Test marca testes; @BeforeEach/@AfterEach executam setup/teardown por teste. Use nomes com backticks para legibilidade. @ParameterizedTest + @ValueSource executam um teste com múltiplas entradas. assertThrows verifica exceções. @Disabled pula testes.

kotlin
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
    lateinit var calc: Calculator
    @BeforeEach fun setup() { calc = Calculator() }
    @Test fun `add two numbers`() {
        assertEquals(5, calc.add(2, 3))
    }
    @Test fun `divide by zero throws`() {
        assertThrows<ArithmeticException> { calc.divide(10, 0) }
    }
    @ParameterizedTest
    @ValueSource(ints = [1, 2, 3])
    fun `positive numbers`(n: Int) { assertTrue(n > 0) }
    @Disabled("TODO") @Test fun skip() {}
}

MockK (Mocking)

MockK é a biblioteca de mocking idiomática do Kotlin (lida com classes final, coroutines, extension functions). every { } returns/throws faz stub; verify { } verifica chamadas. Use relaxed = true para pular stubbing de métodos void. MockK suporta suspend functions nativamente via coEvery/coVerify—essencial para teste de coroutine.

kotlin
import io.mockk.*
interface UserService { fun find(id: Int): String?; fun save(name: String): Int }
class Test {
    val mock = mockk<UserService>()
    @Test fun test() {
        every { mock.find(1) } returns "Alice"
        every { mock.find(any()) } returns null
        every { mock.save(any()) } throws RuntimeException("fail")
        verify { mock.find(1) }              // called once
        verify(exactly = 2) { mock.find(any()) }
        confirmVerified(mock)
    }
}
// relaxUnitFun / relaxed = true for void methods

Turbine (Testes de Flow)

Turbine é a biblioteca padrão para testar Flow. test { } coleta em um escopo controlado; awaitItem() afirma a próxima emissão, awaitComplete()/awaitError() verificam término. Ela trata timing e virtual time corretamente. Combine com runTest para testes de Flow amigáveis a coroutines, rápidos e determinísticos.

kotlin
import app.cash.turbine.test
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import kotlin.test.*
class FlowTest {
    @Test fun testFlow() = runTest {
        flowOf(1, 2, 3).test {
            assertEquals(1, awaitItem())
            assertEquals(2, awaitItem())
            assertEquals(3, awaitItem())
            awaitComplete()
        }
    }
    @Test fun testError() = runTest {
        flow<Int> { throw RuntimeException("x") }.test {
            assertTrue(awaitError() is RuntimeException)
        }
    }
}

Testes de Coroutines (runTest)

runTest usa virtual time—delays pulam instantaneamente, tornando testes async rápidos e determinísticos. Use StandardTestDispatcher + advanceUntilIdle() para controle manual. Injete um TestDispatcher no código de produção (em vez de Dispatchers.Main) para testabilidade. Evite runBlocking em testes—runTest é a maneira moderna.

kotlin
import kotlinx.coroutines.test.*
import kotlinx.coroutines.*
import kotlin.test.*
class RepoTest {
    @Test fun fetch() = runTest {
        // Virtual time: delay(1000) completes instantly
        val result = async { delay(1000); "data" }.await()
        assertEquals("data", result)
    }
    @Test fun withDispatcher() = runTest {
        val scheduler = StandardTestDispatcher()
        val scope = CoroutineScope(scheduler)
        // advanceUntilIdle() runs pending coroutines
        scope.launch { /*...*/ }
        scheduler.advanceUntilIdle()
    }
}

Asserções e Estilos do Kotest

Kotest oferece asserções expressivas (shouldBe, shouldContain) e múltiplos estilos de spec (StringSpec, FunSpec, BehaviorSpec para BDD). Integra com property-based testing e tem matchers ricos. Escolha-o em vez do JUnit para testes idiomáticos e legíveis do Kotlin. Pode rodar junto com JUnit 5.

kotlin
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.string.shouldStartWith
class MyTest : StringSpec({
    "addition works" {
        1 + 1 shouldBe 2
    }
    "list contains" {
        listOf(1, 2, 3) shouldContain 2
    }
    "string prefix" {
        "hello" shouldStartWith "he"
    }
})
// Other styles: FunSpec, BehaviorSpec, ShouldSpec
17

Programação Funcional Profundo

Scope Functions (let, run, with, apply, also)

Scope functions diferem por receptor (this vs it) e valor de retorno (this vs result). apply/also retornam o receptor (encadeamento, builders); let/run/with retornam o resultado da lambda (transformações). Use apply para configuração, let para verificações de null/transformações, also para efeitos colaterais. Não exagere—legibilidade primeiro.

kotlin
data class User(var name: String, var age: Int)
fun main() {
    val u = User("A", 1)
    // apply: configure & return this (builders)
    val configured = u.apply { age = 30 }
    // let: transform, it = receiver, returns lambda result
    val name = u.let { it.name.uppercase() }
    // run: transform, this = receiver, returns result
    val s = u.run { "$name:$age" }
    // with: like run but called as function
    val str = with(u) { "$name:$age" }
    // also: side effects, returns this (logging/chaining)
    u.also { println("Created $it") }
}

Sequences (Lazy Evaluation)

Sequences avaliam lazymente—cada elemento flui por todo o pipeline antes do próximo iniciar (como Java Streams). Isso evita coleções intermediárias e short-circuits (take/find). Use para datasets grandes ou pipelines de múltiplos passos. Para listas pequenas, List eager é frequentemente mais rápida (menos overhead). generateSequence constrói streams infinitos.

kotlin
fun main() {
    // Eager: List processes each step fully
    val eager = (1..10).toList()
        .map { it * 2 }
        .filter { it > 5 }
    // Lazy: Sequence processes element-by-element
    val lazy = (1..10).asSequence()
        .map { println("map $it"); it * 2 }
        .filter { println("filter $it"); it > 5 }
        .take(2)
        .toList()  // only processes until 2 found
    // Use Sequence for large/pipelined data
    // generateSequence for infinite streams
    val naturals = generateSequence(1) { it + 1 }
}

Inline Functions e reified

inline elimina overhead de objeto/alocação de lambda inlining bytecode—crítico para loops quentes. Também habilita non-local returns (return de função externa dentro de uma lambda). Parâmetros de tipo reified tornam info de tipo genérico disponível em runtime (T::class, is T), mas exigem inline. Use para helpers type-safe como filterIsInstance.

kotlin
// inline: copies bytecode at call site (no lambda overhead)
inline fun measure(block: () -> Unit): Long {
    val start = System.currentTimeMillis()
    block()
    return System.currentTimeMillis() - start
}
// non-local return: inline lambdas can return from outer fun
inline fun forEach(list: List<Int>, f: (Int) -> Unit) {
    for (i in list) f(i)
}
// reified: access generic type at runtime
inline fun <reified T> List<*>.filterIsInstance() =
    filter { it is T } as List<T>
val strs = listOf(1, "a", 2, "b").filterIsInstance<String>()

Funções de Ordem Superior

Funções de ordem superior recebem ou retornam funções. Elas são a espinha dorsal do Kotlin funcional—map, filter, fold, compose. Function types como (Int) -> Int são first-class. compose constrói pipelines. Use-as para abstrair padrões, habilitar reuso de código e escrever código declarativo. Marque inline para caminhos críticos de desempenho.

kotlin
fun <T, R> List<T>.mapTo(transform: (T) -> R): List<R> {
    val result = mutableListOf<R>()
    for (item in this) result.add(transform(item))
    return result
}
fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int =
    { x -> f(g(x)) }
fun main() {
    val double = { x: Int -> x * 2 }
    val inc = { x: Int -> x + 1 }
    val doubleThenInc = compose(inc, double)
    println(doubleThenInc(3))  // 7
    listOf(1, 2, 3).mapTo { it * it }
}

Recursão e tailrec

tailrec converte funções tail-recursive (onde a recursão é a última operação) em loops, prevenindo stack overflow. A chamada recursiva deve estar em tail position—sem multiplicação/adição pendente. Use um parâmetro accumulator para tornar funções tail-recursive. Essencial para loops estilo funcional em entradas grandes.

kotlin
// Regular recursion: stack overflow on large n
fun factorial(n: Int): Long =
    if (n <= 1) 1 else n * factorial(n - 1)
// tailrec: compiler optimizes to a loop (no stack growth)
tailrec fun factorialTail(n: Int, acc: Long = 1): Long =
    if (n <= 1) acc else factorialTail(n - 1, acc * n)
// Fibonacci with tailrec
tailrec fun fib(n: Int, a: Long = 0, b: Long = 1): Long =
    when (n) { 0 -> a; 1 -> b; else -> fib(n - 1, b, a + b) }
fun main() {
    println(factorialTail(10000))  // no stack overflow
}
18

Generics e Variance

Classes e Funções Genéricas

Generics habilitam código type-safe e reutilizável. Classes usam <T>; funções declaram <T> antes do tipo de retorno. Constraints (T : Entity) restringem o bound do tipo. Diferente do Java, generics do Kotlin são reified para funções inline e têm declaration-site variance, tornando APIs genéricas mais seguras e ergonômicas.

kotlin
class Box<T>(val value: T) {
    fun get(): T = value
}
fun <T> singletonList(item: T): List<T> = listOf(item)
fun main() {
    val intBox = Box(42)        // Box<Int> inferred
    val strBox = Box<String>("hi")
    val nums = singletonList(1)
}
// Generic constraints
class Repository<T : Entity> {
    fun save(item: T) { /* T is Entity subtype */ }
}
interface Entity { val id: Int }

Variance (in/out)

Variance controla relacionamentos de subtipo de generics. out (covariant): um Producer<Dog> é um Producer<Animal>—seguro porque você apenas lê T. in (contravariant): um Sink<Animal> é um Sink<Dog>—seguro porque você apenas escreve T. Coleções mutáveis são invariantes (leitura + escrita). Use out/in para projetar APIs seguras e flexíveis.

kotlin
// out (covariant): Producer<Sub> is Producer<Super>
interface Source<out T> { fun next(): T }
// in (contravariant): Consumer<Super> is Consumer<Sub>
interface Sink<in T> { fun put(item: T) }
// invariant (default): neither
class MutableList<T> {
    fun add(item: T) {}
    fun get(): T = TODO()
}
open class Animal
class Dog : Animal()
val src: Source<Animal> = Source<Dog>()  // OK (out)
val sink: Sink<Dog> = Sink<Animal>()     // OK (in)

Variance na Prática

Function types usam variance automaticamente: parâmetros são 'in', tipos de retorno são 'out'. É por isso que (Dog) -> Unit é atribuível a (Animal) -> Unit. O princípio PECS (Producer Extends, Consumer Super) mapeia para out/in do Kotlin. Projete interfaces genéricas com out quando elas apenas produzem T, in quando elas apenas consomem T.

kotlin
// Function types are variant by nature
// (T) -> R is contravariant in T, covariant in R
val dogHandler: (Dog) -> Unit = { println(it) }
val animalHandler: (Animal) -> Unit = dogHandler  // OK
// Covariant return types
interface Repository<out T> { fun find(id: Int): T }
class DogRepo : Repository<Dog> {
    override fun find(id: Int): Dog = Dog()
}
val repo: Repository<Animal> = DogRepo()  // OK
// PECS rule: Producer extends, Consumer super
// Kotlin: out = producer, in = consumer

Type Projections e Star Projection

Type projections tornam temporariamente um tipo variant em um use-site. Array<out Any> significa 'Array de algum subtipo de Any, legível'. Array<in Any> significa 'gravável como Any'. Star projection (*) significa 'algum tipo desconhecido'—útil quando você apenas precisa de size/contains, não do tipo de elemento. Como wildcards do Java.

kotlin
fun copy(from: Array<out Any>, to: Array<in Any>) {
    // from: only read (covariant projection)
    // to: only write (contravariant projection)
    for (i in from.indices) to[i] = from[i]
}
// Star projection: unknown type, read-only
fun printSize(list: List<*>) {
    println(list.size)  // OK (no type needed)
    // list[0]  // type is Any? — limited use
}
val mixed: List<*> = listOf(1, "a", 3.0)
// Use when type is irrelevant or unknown

Parâmetros de Tipo Reified

reified preserva info de tipo genérico em runtime (normalmente apagada na JVM). Exige inline (o tipo é conhecido em cada call site). Habilita verificações is T, T::class e filterIsInstance. Sem reified, você passaria um parâmetro Class<T> manualmente. Limitação: tipos reified não podem ser usados em funções non-inline ou como parâmetros de tipo de classe.

kotlin
// reified requires inline
inline fun <reified T> List<*>.filterIsInstance(): List<T> =
    filter { it is T } as List<T>
inline fun <reified T> Any.castTo(): T = this as T
inline fun <reified T> typeOf() = T::class.simpleName
fun main() {
    val list = listOf(1, "a", 2, "b", 3)
    val strs: List<String> = list.filterIsInstance()
    println(strs)  // [a, b]
    println(typeOf<String>())  // String
    // Without reified, T is erased at runtime
    // reified makes T::class and 'is T' available
}
19

Null Safety Profundo

Tipos Nullable

Kotlin distingue tipos nullable (T?) e non-null (T) em tempo de compilação. ?. safe call retorna null se o receptor é null. ?: Elvis fornece um padrão. !! lança NPE (evite). O compilador impõe verificações de null, eliminando NullPointerException em código Kotlin.

kotlin
var name: String = "Alice"  // Non-null
// name = null  // Error
var nickname: String? = null  // Nullable
// Safe call
println(nickname?.length)  // null if nickname is null
// Elvis operator
val len = nickname?.length ?: 0  // 0 if null
// Not-null assertion (use sparingly)
val len2 = nickname!!.length  // NPE if null

let e run

let executa um bloco se o valor for non-null (it = o valor). run chama um bloco com o objeto como receptor, retorna o resultado. apply configura um objeto, retorna o objeto. also realiza efeitos colaterais, retorna o objeto. Essas scope functions reduzem verificações de null e melhoram a legibilidade.

kotlin
val name: String? = "Alice"
name?.let {
    println("Length: ${it.length}")  // Only if not null
}
// run: object + block
val result = "Hello".run {
    length  // Returns 5
}
// apply: configure object
val list = mutableListOf<Int>().apply {
    add(1); add(2); add(3)
}

lateinit

lateinit adia a inicialização de propriedades non-null. Não pode ser usado com primitivos ou tipos nullable. Lança UninitializedPropertyAccessException se acessado antes da init. ::prop.isInitialized verifica. Útil para injeção de dependência e propriedades gerenciadas por lifecycle.

kotlin
class Service {
    lateinit var dependency: Database
    fun init() {
        dependency = connectDatabase()
    }
    fun use() {
        if (::dependency.isInitialized) {
            dependency.query()
        }
    }
}

Coleções Nullable

Coleções podem conter elementos nullable (List<String?>) ou ser nullable (List<String>?). filterNotNull remove nulls. firstOrNull retorna null em vez de lançar. Trate elementos nullable com safe calls. Seja explícito sobre nullability em tipos de coleção.

kotlin
val list: List<String?> = listOf("a", null, "b")
val filtered = list.filterNotNull()  // ["a", "b"]
val lengths = list.map { it?.length }  // [1, null, 1]
val first = list.firstOrNull()  // "a" or null
val firstNonEmpty = list.firstOrNull { it?.isNotEmpty() == true }

Platform Types

Platform types surgem do interop com Java onde a nullability é desconhecida. Kotlin não pode impor null safety para eles. Sempre declare tipos nullable explicitamente para valores de retorno Java. Use annotations @Nullable/@NotNull no Java. Annotations JSR-305 ajudam Kotlin a inferir nullability.

kotlin
// Java interop: platform type (no null info)
val name: String = javaObject.getName()  // May be null!
// Kotlin does not know if Java returns null
// Fix: explicit nullable type
val name2: String? = javaObject.getName()
// Or @Nullable annotation in Java
20

Construção de DSL

Builder com Lambda Receiver

Lambda com receiver (T.() -> Unit) habilita sintaxe DSL. Dentro da lambda, this é o objeto receptor. Métodos podem ser chamados sem qualificação. É assim que Kotlin constrói DSLs type-safe como Gradle, HTML e SQL builders.

kotlin
class StringBuilder {
    private val parts = mutableListOf<String>()
    fun line(s: String) { parts.add(s) }
    fun build() = parts.joinToString("\n")
}
fun buildString(init: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.init()  // Execute lambda with sb as receiver
    return sb.build()
}

HTML DSL

HTML DSL usa builders aninhados com lambda receivers. Cada tag é uma função que cria um child builder. A lambda configura o child. Produz HTML type-safe e composável. kotlinx.html é uma implementação real. O mesmo padrão funciona para qualquer estrutura hierárquica.

kotlin
fun html(init: HTML.() -> Unit): HTML {
    val h = HTML(); h.init(); return h
}
class HTML {
    fun body(init: Body.() -> Unit) { /* ... */ }
}
html {
    body {
        // this: Body
        p("Hello")
    }
}

@DslMarker

@DslMarker previne acesso implícito ao receptor a escopos externos. Sem ele, tanto métodos HTML quanto Body são acessíveis, causando confusão. A annotation restringe acesso ao receptor mais interno. Torna DSLs type-safe e não ambíguas. Essencial para DSLs complexas.

kotlin
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(...) {} }
@HtmlDsl
class Body { fun p(...) {} }
html {
    body {
        // p()  // OK: in Body scope
        // body()  // Error: in Body scope, not HTML
    }
}

Gradle DSL

Gradle Kotlin DSL usa o mesmo padrão de builder. plugins, dependencies são funções com lambda receivers. implementation, testImplementation são funções de configuração de dependência. Type-safe: compilador verifica nomes de função e tipos de parâmetro. Muito melhor que Groovy para refatoração.

kotlin
plugins {
    kotlin("jvm") version "1.9.0"
}
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    testImplementation(kotlin("test"))
}
// All are function calls with lambda receivers

Anko SQL

Anko (e Exposed) fornecem DSLs SQL type-safe. Comparações de coluna são tipadas. Não pode comparar uma coluna string com um int. A DSL gera SQL. Previne injeção de SQL e erros de tipo. O mesmo padrão se aplica a qualquer linguagem específica de domínio.

kotlin
fun users(where: SqlExpressionBuilder.() -> Op<Boolean>) {
    // DSL for SQL queries
}
users {
    (Users.age greater 18) and (Users.name like "A%")
}
// Type-safe SQL: compiler checks column types
21

Coleções

Operações de List

Coleções Kotlin têm APIs funcionais ricas. map, filter, reduce são padrão. groupBy particiona por chave. chunked divide em listas de tamanho fixo. windowed cria janelas deslizantes. Todas retornam novas coleções. Use asSequence() para lazy evaluation.

kotlin
val list = listOf(1, 2, 3, 4, 5)
val doubled = list.map { it * 2 }
val evens = list.filter { it % 2 == 0 }
val sum = list.reduce { acc, n -> acc + n }
val grouped = list.groupBy { it % 2 }  // {0=[2,4], 1=[1,3,5]}
val chunked = list.chunked(2)  // [[1,2],[3,4],[5]]

Sequence

Sequence é uma coleção lazy (como Java Stream). Operações são avaliadas sob demanda. Sem coleções intermediárias. Eficiente para coleções grandes com operações encadeadas. Use asSequence() para converter. Force com toList(), toSet(), etc.

kotlin
val result = (1..1000000).asSequence()
    .map { it * 2 }
    .filter { it > 100 }
    .take(10)
    .toList()
// Lazy: only computes what is needed
// No intermediate collections

Destructuring

Destructuring divide objetos em variáveis. Funciona com Pair, Triple, data classes e entradas de Map. Funções componentN() a habilitam. withIndex() pareia índice e valor. Útil para múltiplos valores de retorno e iteração. Data classes auto-geram componentN.

kotlin
val (a, b) = Pair(1, "hello")
val (x, y, z) = Triple(1, 2, 3)
for ((index, value) in list.withIndex()) {
    println("$index: $value")
}
data class Point(val x: Int, val y: Int)
val (px, py) = Point(1, 2)

Mutável vs Imutável

Kotlin tem tanto coleções mutáveis (MutableList) quanto imutáveis (List). Prefira imutáveis para segurança. toList() cria uma cópia imutável. toMutableList() cria uma cópia mutável. As interfaces imutáveis não expõem métodos de mutação, prevenindo modificação acidental.

kotlin
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)  // OK
val immutable = listOf(1, 2, 3)
// immutable.add(4)  // Error: no add method
// Convert
val imm = mutable.toList()
val mut = imm.toMutableList()

Associate e Partition

toMap/associate convertem para maps. partition divide em duas listas por predicado. flatten mescla listas aninhadas. flatMap mapeia e aplaina. Esses substituem loops verbosos por expressões declarativas. Todos retornam novas coleções.

kotlin
val list = listOf("a" to 1, "b" to 2)
val map = list.toMap()  // {a=1, b=2}
val byLength = list.associate { it.first to it.second }
val (evens, odds) = list.partition { it.second % 2 == 0 }
// evens: [(a,1)?] odds: [(b,2)]
val flat = listOf(listOf(1), listOf(2, 3)).flatten()  // [1,2,3]
22

Armadilhas Comuns

== vs ===

Kotlin == chama equals (igualdade de valor), diferentemente do Java. === verifica igualdade de referência. Use == para comparação de valor. === raramente é necessário. Para Integer, valores -128 a 127 são cacheados, então === pode ser verdadeiro ou falso. Sempre use == para valores.

kotlin
val a = Integer(127)
val b = Integer(127)
a == b  // true (value equality, calls equals)
a === b  // true (Integer caches -128 to 127)
val c = Integer(128)
val d = Integer(128)
c == d  // true
c === d  // false (not cached)

Companion Object

Kotlin não tem membros static. companion object contém métodos e constantes 'static'. const val é uma verdadeira constante em tempo de compilação. O companion object é uma instância singleton. @JvmStatic torna métodos chamáveis como static a partir do Java. Use funções top-level para statics verdadeiros.

kotlin
class MyClass {
    companion object {
        const val CONSTANT = 42
        fun create() = MyClass()
    }
}
MyClass.CONSTANT  // 42
MyClass.create()  // Static-like access
// companion object is a real object, not static

Data Class Copy

Data classes auto-geram copy() que cria uma cópia modificada. O original é inalterado (imutável). Apenas campos especificados mudam. Útil para atualizações. Combinado com destructuring, data classes são poderosas para modelar dados. Evite var em data classes para imutabilidade.

kotlin
data class User(val name: String, val age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31)  // New instance
// alice is unchanged
// copy uses named arguments for changed fields

Exaustividade de Sealed Class

Sealed classes restringem subtipos ao mesmo arquivo/package. when em sealed classes é exaustivo: o compilador avisa se uma ramificação está faltando. Sem else necessário se todos os cases são cobertos. Adicionar um novo subtipo causa avisos em todos os lugares. Ideal para state machines e results.

kotlin
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
fun handle(r: Result) = when (r) {
    is Success -> r.value
    // Warning: Missing Failure branch
}
// Fix: handle all cases
fun handle(r: Result) = when (r) {
    is Success -> r.value
    is Failure -> 0
}  // No else needed

Extension vs Member

Funções member sempre têm precedência sobre extension functions com a mesma assinatura. Extensions são resolvidas em tempo de compilação (dispatch estático), members em runtime (dispatch dinâmico). Extensions não podem sobrescrever members. Use extensions para funções utilitárias, não para polimorfismo.

kotlin
class Foo {
    fun bar() = "member"  // Member function
}
fun Foo.bar() = "extension"  // Extension function
Foo().bar()  // "member" - members win!
// Extensions are resolved statically
// Members are resolved dynamically (virtual)
23

Kotlin Multiplatform

Common Code

Kotlin Multiplatform (KMP) compartilha código entre plataformas. expect/actual declara implementações específicas de plataforma. commonMain tem código compartilhado. Source sets específicos de plataforma implementam actual. Compartilhe lógica de negócios, mantenha UI nativa. Gradle configura targets.

kotlin
// commonMain/src/Platform.kt
expect fun getPlatformName(): String
// androidMain/src/Platform.kt
actual fun getPlatformName(): String = "Android"
// iosMain/src/Platform.kt
actual fun getPlatformName(): String = "iOS"
// Shared business logic
class Greeting {
    fun greet() = "Hello from ${getPlatformName()}"
}

Shared Module

Projetos multiplatform usam bloco kotlin no Gradle. Defina targets (android, ios). commonMain tem dependências compartilhadas. Source sets de plataforma podem ter deps específicas de plataforma. iOS usa Kotlin/Native para compilação direta. Compartilhe lógica, não UI.

kotlin
// build.gradle.kts (shared module)
kotlin {
    androidTarget()
    iosX64(); iosArm64(); iosSimulatorArm64()
    sourceSets {
        val commonMain by getting {
            dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") }
        }
    }
}

Networking (Ktor)

Ktor é um cliente HTTP multiplatform. A API é comum, o engine é específico de plataforma. Android usa OkHttp/Android engine. iOS usa Darwin. Coroutines funcionam entre plataformas. Compartilhe networking, banco de dados (SQLDelight) e lógica de negócios.

kotlin
// Shared HTTP client
suspend fun fetchUser(): User {
    return client.get("https://api.example.com/user").body()
}
// Platform-specific engine
// Android: OkHttp or Android
// iOS: Darwin
// Common code uses the same API

SQLDelight

SQLDelight gera Kotlin type-safe a partir de SQL. Arquivos .sq contêm SQL com named queries. Gera objetos de consulta type-safe. Funciona entre plataformas (Android, iOS, JVM). SQL é a fonte da verdade. Migrações de schema são rastreadas. Alternativa ao Room para multiplatform.

kotlin
// .sq file: User.sq
CREATE TABLE User (id INTEGER, name TEXT);
selectById: SELECT * FROM User WHERE id = ?;
insertUser: INSERT INTO User (id, name) VALUES (?, ?);
// Generated Kotlin code
val queries: UserQueries = database.userQueries
queries.insertUser(1, "Alice")
val user = queries.selectById(1).executeAsOne()

Compose Multiplatform

Compose Multiplatform estende Jetpack Compose para iOS, Desktop e Web. Compartilhe código UI entre plataformas. Mesma API @Composable. Entry points específicos de plataforma. Ainda experimental para iOS. Reduz duplicação de UI. Plugin Gradle: compose-multiplatform.

kotlin
// Shared UI with Compose Multiplatform
@Composable
fun Greeting(name: String) {
    Text("Hello, $name!")
}
// Android
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { Greeting("World") }
    }
}
// Desktop
fun main() = application {
    Window { Greeting("Desktop") }
}
24

Testes Kotlin

JUnit 5

JUnit 5 é a estrutura de testes padrão. Kotlin permite nomes de teste com backticks para legibilidade. assertEquals, assertThrows são asserções comuns. @BeforeEach, @AfterEach para setup/teardown. @ParameterizedTest para testes data-driven. Use kotlin.test para multiplatform.

kotlin
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
    @Test
    fun `test addition`() {
        val calc = Calculator()
        assertEquals(5, calc.add(2, 3))
    }
    @Test
    fun `throws on negative`() {
        assertThrows<IllegalArgumentException> { calc.sqrt(-1) }
    }
}

MockK

MockK é uma biblioteca de mocking nativa do Kotlin. Suporta classes final e extension functions. every faz stub, verify verifica chamadas. @MockK cria mocks, @InjectMockKs os injeta. coEvery/coVerify para coroutines. Melhor suporte Kotlin que Mockito.

kotlin
class UserServiceTest {
    @MockK
    lateinit var repo: UserRepository
    @InjectMockKs
    lateinit var service: UserService
    @BeforeEach
    fun setup() {
        MockKAnnotations.init(this)
        every { repo.find(1) } returns User("Alice")
    }
    @Test
    fun `test find`() {
        assertEquals("Alice", service.find(1).name)
        verify { repo.find(1) }
    }
}

Kotest

Kotest é uma estrutura de testes Kotlin-first. Múltiplos estilos: StringSpec, BehaviorSpec, FunSpec. shouldBe é uma asserção fluent. Property testing com Arb. Suporta testes data-driven. Integra com Spring e Ktor. Mais idiomático Kotlin que JUnit.

kotlin
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class CalculatorTest : StringSpec({
    "addition should work" {
        Calculator().add(2, 3) shouldBe 5
    }
    "should throw on negative" {
        shouldThrow<IllegalArgumentException> { Calculator().sqrt(-1) }
    }
})

Testes de Coroutine

runTest de kotlinx-coroutines-test fornece virtual time. Delays pulam instantaneamente. advanceUntilIdle executa coroutines pendentes. Muito mais rápido que tempo real. Use TestDispatcher para controle fino. Biblioteca Turbine testa emissões de Flow.

kotlin
@Test
fun `test async`() = runTest {
    val result = fetchData()
    assertEquals("data", result)
}
// runTest replaces runBlocking with virtual time
// advanceUntilIdle() runs all pending coroutines
// Delay skips instantly

Turbine (Testes de Flow)

Turbine testa emissões de Flow. awaitItem obtém próxima emissão. awaitComplete/afirma conclusão. awaitError afirma um erro. bloco test suspende até o flow concluir. Muito mais limpo que coletar manualmente. Essencial para testes de Flow.

kotlin
@Test
fun `test flow`() = runTest {
    flowOf(1, 2, 3).test {
        awaitItem() shouldBe 1
        awaitItem() shouldBe 2
        awaitItem() shouldBe 3
        awaitComplete()
    }
}
// Test errors
flow<Int> { throw Exception() }.test {
    awaitError() shouldBe Exception()
}
25

Coroutines Profundo

Coroutine Scope

CoroutineScope define um lifetime para coroutines. viewModelScope auto-cancela no clear do VM. SupervisorJob previne falhas filhas de cancelar irmãos. Scopes customizados precisam de cancelamento explícito. Scopes propagam cancelamento para filhos. Nunca use GlobalScope em produção (não pode ser cancelado).

kotlin
class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch {
            val data = api.getData()
            _data.value = data
        }
    }
}
// Custom scope
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch { /* ... */ }
scope.cancel()  // Cancel all children

Dispatchers

Dispatchers roteiam coroutines para thread pools. Main: thread de UI (Android). IO: E/S bloqueante (64+ threads). Default: trabalho CPU (threads de contagem de CPU). Unconfined: thread do chamador (avançado). withContext troca o dispatcher temporariamente. Escolher o dispatcher certo melhora desempenho e previne congelamentos de UI.

kotlin
launch(Dispatchers.Main) { updateUI() }
launch(Dispatchers.IO) { writeFile() }
launch(Dispatchers.Default) { heavyComputation() }
launch(Dispatchers.Unconfined) { runAnywhere() }
// Switch context
withContext(Dispatchers.IO) {
    val data = readDisk()
}

Operadores de Flow

Operadores de Flow: map transforma, filter seleciona, flatMapMerge/flatMapConcat encadeia flows. Cold flows começam fresh por collector. StateFlow mantém um valor (como LiveData). SharedFlow faz broadcast para múltiplos collectors. stateIn converte cold para hot. buffer/conflate controlam backpressure.

kotlin
flowOf(1, 2, 3, 4, 5)
    .map { it * 2 }
    .filter { it > 4 }
    .collect { println(it) }  // 6, 8, 10

// Cold flow: emits per collector
// StateFlow: hot, stateful
// SharedFlow: hot, broadcast
val state = MutableStateFlow(0)
state.value = 1

Tratamento de Exceções

CoroutineExceptionHandler captura exceções não tratadas em launch. Exceções de async propagam para await. SupervisorJob isola falhas filhas. CancellationException é especial: relançada, não capturada por catch. Nunca engula CancellationException. Use try/finally ou use() para limpeza. Cancelamento propaga através de chamadas suspend.

kotlin
val handler = CoroutineExceptionHandler { _, e ->
    Log.e("TAG", "Caught: $e", e)
}
scope.launch(handler) {
    throw RuntimeException("oops")
}
// try/catch in coroutine
try {
    riskyCall()
} catch (e: Exception) {
    // handle
}

Channels

Channels permitem comunicação coroutine-to-coroutine. send suspende quando cheio, receive suspende quando vazio. capacity: buffered (RENDEZVOUS=0, UNLIMITED, CONFLATED). close() sinaliza conclusão. produce cria uma coroutine produtora. Channel é hot: valores são consumidos uma vez. Prefira Flow para a maioria dos casos de uso.

kotlin
val channel = Channel<Int>(capacity = 10)
launch {
    for (i in 1..5) channel.send(i)
    channel.close()
}
launch {
    for (x in channel) println(x)
}
// Produce pattern
fun numbers() = produce {
    for (i in 1..10) send(i)
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.