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.
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) // trueString 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.
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.
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 nullableConversã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.
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.
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 5Strings
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.
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")) // trueStrings 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.
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 whitespaceConstruçã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.
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.
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.
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)) // trueEstruturas 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.
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)) // 1Map
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).
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/updateSet
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.
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 addedArray 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.
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.
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())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.
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 typesWhen (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.
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.
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.
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 possibleBreak 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.
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")
}
}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.
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 = voidLambdas 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.
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 parameterExtension 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).
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 typeInline 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'.
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") // abababVararg 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.
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"))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.
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 propertyData 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.
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.
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 casesObject 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.
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 methodHeranç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.
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"
}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.
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?.citylet (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.
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.
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.
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.
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)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.
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, 5FlatMap 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.
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.
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.
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.
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]]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.
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 1sAsync 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.
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.
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 coroutinesFlow (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.
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.
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() }
}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.
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 exceptionsTipo 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.
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()) // nullExceçõ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.
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.
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.
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 annotationCoroutines 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.
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.
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 stateTratamento 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.
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).
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.
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
}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.
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.
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.
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.
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.
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) }
}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.
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/moduleSealed 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.
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.
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 itModelagem 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.
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.
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)) // 14Delegated 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.
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-threadedobservable 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.
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.
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.
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.
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
}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.
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.
@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.
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.
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.
// 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") }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.
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.
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 methodsTurbine (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.
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.
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.
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, ShouldSpecProgramaçã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.
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.
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.
// 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.
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.
// 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
}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.
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.
// 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.
// 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 = consumerType 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.
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 unknownParâ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.
// 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
}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.
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 nulllet 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.
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.
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.
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.
// 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 JavaConstruçã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.
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.
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.
@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.
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 receiversAnko 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.
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 typesColeçõ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.
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.
val result = (1..1000000).asSequence()
.map { it * 2 }
.filter { it > 100 }
.take(10)
.toList()
// Lazy: only computes what is needed
// No intermediate collectionsDestructuring
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.
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.
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.
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]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.
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.
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 staticData 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.
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 fieldsExaustividade 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.
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 neededExtension 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.
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)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.
// 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.
// 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.
// 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 APISQLDelight
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.
// .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.
// 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") }
}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.
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.
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.
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.
@Test
fun `test async`() = runTest {
val result = fetchData()
assertEquals("data", result)
}
// runTest replaces runBlocking with virtual time
// advanceUntilIdle() runs all pending coroutines
// Delay skips instantlyTurbine (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.
@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()
}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).
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 childrenDispatchers
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.
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.
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 = 1Tratamento 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.
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.
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)
}Snippets de Kotlin relacionados
Copy-paste ready code for common tasks.
Null Safety
Use nullable types, safe calls, Elvis, and smart casts for null-safe code.
Data Classes
Model immutable data with auto-generated equals, copy, and destructuring.
Coroutines
Use launch, async, await, and structured concurrency with supervisorScope.
Extension Functions
Add methods to existing types with extensions and infix operators.
Sealed Classes
Model closed hierarchies and UI state with sealed classes and when.
When Expression
Branch on values, ranges, and types with when as statement or expression.
Collections
Filter, map, group, partition, and chunk with functional operators.
Delegation
Delegate interfaces, lazy properties, observables, and custom delegates.
Was this helpful?