DevTools

Cheatsheet Kotlin

Linguagem moderna para Android, backend e multiplataforma

Voltar às linguagens
Kotlin
95 cards encontrados
Categorias:
Versões:

Básico e Tipos


11 cards
Hello World
fun main() {
    println("Olá, Mundo!")
}

// Sem classe obrigatória (diferente de Java)
// main não precisa de args nem tipo de retorno

O ponto de entrada é a função main — não precisa de classe envolvente nem de tipo de retorno declarado. O println escreve uma linha no stdout. O compilador infere tudo automaticamente.

Null safety
var nome: String? = null  // tipo nullable

val len = nome?.length     // safe call (null se null)
val tam = nome?.length ?: 0 // elvis: default se null
val n = nome!!.length      // força (NPE se null)

// Executa só se não-null:
nome?.let { println(it) }

O sistema de tipos elimina NullPointerException em compilação. O ? marca tipos nullable, o ?. encadeia chamadas com segurança e o ?: (elvis) fornece defaults. O let{} executa blocos só quando o valor existe.

Operadores
// Aritméticos:
+ - * / %
val r = 10 / 3   // 3 (inteiro)

// Comparação e lógicos:
==  !=  >  <  >=  <=
&&  ||  !

// Range e pertença:
1..10        // range
5 in 1..10   // true

// Atribuição composta:
+=  -=  *=  /=  %=

// Não há ++/-- prefixo ambíguo

A divisão inteira trunca. O operador in verifica pertença a ranges e coleções. == compara por valor (como equals em Java); === compara referências. Não há conversões automáticas em expressões mistas.

Variáveis (val e var)
val nome = "Kotlin"   // imutável (read-only)
var idade = 10        // mutável
idade = 11            // OK

val pi: Double = 3.14 // tipo explícito

// Inferência de tipo:
val x = 42            // Int
val s = "texto"       // String

Use val por defeito (imutável) para evitar bugs; só use var quando o valor precisar de mudar. O tipo é inferido a partir do valor, mas pode ser anotado explicitamente com : Tipo.

Arrays
val nums = arrayOf(1, 2, 3, 4, 5)
val zeros = IntArray(5) { 0 }
val pares = Array(5) { i -> i * 2 }

println(nums[0])    // 1
println(nums.size)  // 5
nums[0] = 10        // mutável

// Iterar:
for (n in nums) println(n)

Arrays têm tamanho fixo após criação. IntArray/DoubleArray são versões otimizadas sem boxing. O lambda no construtor inicializa cada posição. Para coleções dinâmicas prefira List/MutableList.

lateinit e lazy
// lateinit: inicialização tardia (var)
class Activity {
    lateinit var db: Database

    fun onCreate() {
        db = Database.abrir()
    }
}

// lazy: calcula no 1.º acesso (val)
val config: Config by lazy {
    Config.carregarDoFicheiro()
}

lateinit declara propriedades não-nulas inicializadas depois (ex: injeção de dependências) — aceder antes lança exceção. O by lazy adia o cálculo de um val até ao primeiro acesso (thread-safe por defeito).

Tipos de dados
val inteiro: Int = 42
val longo: Long = 100_000_000L
val decimal: Double = 3.14
val flag: Boolean = true
val letra: Char = 'A'
val texto: String = "Kotlin"
val nada: Unit = Unit

// Underscore para legibilidade:
val milhao = 1_000_000

Kotlin trata os primitivos como objetos (sem distinção int/Integer como em Java). O underscore em números melhora a legibilidade. O Unit equivale ao void do Java mas é um tipo real.

Type aliases
typealias UserId = Long
typealias Callback = (String) -> Unit
typealias Tabela = Map<String, List<Any>>

fun registar(id: UserId, cb: Callback) {
    cb("utilizador $id registado")
}

registar(42L) { println(it) }

typealias cria nomes legíveis para tipos complexos — ótimo para callbacks, IDs e mapas aninhados. Não cria um tipo novo (é só um atalho), mas melhora muito a legibilidade das assinaturas.

Nothing e Any
// Any: super-tipo de todos
val obj: Any = 42
val obj2: Any = "texto"

// Nothing: função nunca retorna
fun falhar(msg: String): Nothing {
    throw IllegalStateException(msg)
}

// Útil em expressões:
val x = valor ?: falhar("sem valor")
// aqui x é não-null

Any é o super-tipo de todos os tipos (como Object em Java). Nothing indica que uma função nunca retorna normalmente (lança exceção ou loop infinito) — ajuda o compilador a inferir nullability após ?:.

Strings e templates
val nome = "Mundo"
val saudacao = "Olá, $nome!"
val template = "2 + 2 = ${2 + 2}"

// Multilinha (triple-quoted):
val multi = """
    |Linha 1
    |Linha 2
""".trimMargin()

// Sem escape de aspas:
val json = """{"nome": "$nome"}"""

String templates com $ evitam concatenação; para expressões use ${}. Triple-quoted strings (\"\"\") preservam formatação e são ideais para SQL/JSON — o trimMargin() remove a indentação.

Conversões de tipos
val x = 42
val s = x.toString()    // Int -> String
val n = "123".toInt()   // String -> Int
val d = x.toDouble()    // Int -> Double
val l = x.toLong()      // Int -> Long

// Sem conversões implícitas:
// val d: Double = x  // ERRO!
val d2: Double = x.toDouble()

// toIntOrNull (seguro):
val inv = "abc".toIntOrNull() // null

Kotlin não faz conversões numéricas implícitas — use toInt(), toDouble(), etc. explicitamente. O toIntOrNull() retorna null em vez de lançar exceção se a string não for um número válido.

Controlo de Fluxo


10 cards
If como expressão
// if retorna valor:
val max = if (a > b) a else b

// Com blocos:
val msg = if (nota >= 10) {
    println("Aprovado")
    "OK"
} else {
    "Reprovado"
}

// Não existe operador ternário ?:
// Use if-else como expressão

Em Kotlin o if é uma expressão que retorna um valor — substitui o operador ternário (que não existe). O último statement de cada bloco é o valor retornado. Torna o código mais declarativo.

While e do-while
var n = 10
while (n > 0) {
    n--
}

// do-while executa pelo menos 1x:
do {
    val input = readLine()
    println("Lido: $input")
} while (input != "sair")

O while verifica a condição antes de executar (pode nunca executar). O do-while executa pelo menos uma vez — ideal para menus e validação de input onde precisa de ler antes de verificar.

Smart casts
fun processar(obj: Any) {
    if (obj is String) {
        // obj já é String aqui:
        println(obj.length)
        println(obj.uppercase())
    }

    if (obj !is List<*>) return
    // obj é List a partir daqui:
    println(obj.size)
}

O compilador faz smart cast automático após verificações is/!is — dentro do bloco, a variável é tratada como o tipo testado sem cast manual. Só funciona com val locais ou propriedades imutáveis.

When
val resultado = when (x) {
    1 -> "um"
    2, 3 -> "dois ou três"
    in 4..10 -> "entre 4 e 10"
    !in 20..30 -> "fora de 20-30"
    else -> "outro"
}

// when é expressão (retorna valor)
// else é obrigatório se não for exaustivo

O when substitui o switch do Java com padrões mais poderosos: múltiplos valores separados por vírgula, ranges com in, negação com !in. É uma expressão que retorna valor.

Break, continue e labels
for (i in 1..10) {
    if (i == 3) continue // salta o 3
    if (i == 7) break    // termina no 7
    print(i)             // 1 2 4 5 6
}

// Labels para ciclos aninhados:
outer@ for (i in 1..3) {
    for (j in 1..3) {
        if (i * j > 4) break@outer
    }
}

continue salta para a próxima iteração, break termina o ciclo. Labels (outer@) permitem controlar ciclos aninhados — break@outer sai diretamente do ciclo exterior.

return, break em lambdas
fun procurar(lista: List<Int>) {
    lista.forEach {
        if (it == 3) return  // retorna da função!
    }
}

// Label para retornar só do lambda:
lista.forEach loop@{
    if (it == 3) return@loop // só deste lambda
    println(it)
}

// Nome implícito do lambda:
lista.forEach {
    if (it > 5) return@forEach
}

Um return dentro de um lambda retorna da função envolvente (non-local return). Para retornar só do lambda use labels (return@forEach). Isto é essencial para controlar o fluxo em funções de ordem superior.

When avançado
// Sem argumento (if-else chain):
val msg = when {
    x < 0 -> "negativo"
    x == 0 -> "zero"
    else -> "positivo"
}

// Com tipos (smart cast):
fun tipo(obj: Any) = when (obj) {
    is String -> "Texto: ${obj.length}"
    is Int -> "Número: $obj"
    is List<*> -> "Lista: ${obj.size}"
    else -> "?"
}

O when sem argumento funciona como cadeia if-else mais legível. Com is, o Kotlin faz smart cast automático — dentro do branch, obj já é tratado como o tipo testado sem cast manual.

Ranges e progressões
val r1 = 1..10        // IntRange (inclusivo)
val r2 = 1 until 10   // 1..9 (exclui último)
val r3 = 'a'..'z'     // CharRange
val r4 = 10 downTo 1  // decrescente

if (5 in r1) println("contém")
for (c in 'a'..'f') print(c)  // abcdef

val lista = (1..5).toList()   // [1,2,3,4,5]

Ranges são objetos Iterable que suportam iteração e verificação de pertença (in). until exclui o último elemento (útil para índices). Funcionam com Int, Long e Char.

Ciclos for e ranges
for (i in 1..5) print(i)         // 1 2 3 4 5
for (i in 5 downTo 1) print(i)   // 5 4 3 2 1
for (i in 0..10 step 2) print(i) // 0 2 4 6 8 10
for (i in 1 until 5) print(i)    // 1 2 3 4

// Com índice e valor:
for ((i, v) in lista.withIndex()) {
    println("$i: $v")
}

Ranges (..) criam sequências inclusivas. downTo inverte a direção, step define o incremento e until exclui o último. O withIndex() dá acesso ao índice e valor simultaneamente.

Expressões (try/when)
// try como expressão:
val n = try {
    "42".toInt()
} catch (e: NumberFormatException) {
    0
}

// when como expressão:
val code = when (status) {
    "ok" -> 200
    else -> 500
}

// if como expressão:
val max = if (a > b) a else b

Diferente de Java, if/when/try são expressões que retornam valores em Kotlin. Isto elimina variáveis temporárias e torna o código mais declarativo. O último statement de cada bloco é o valor.

Funções e Lambdas


11 cards
Declaração de funções
fun soma(a: Int, b: Int): Int {
    return a + b
}

// Corpo de expressão (retorno inferido):
fun dobro(x: Int) = x * 2

// Sem retorno útil (Unit):
fun saudar(nome: String) {
    println("Olá, $nome!")
}

Funções com corpo de expressão (=) são mais concisas e o tipo de retorno é inferido. O Unit (sem retorno) pode ser omitido. Os tipos dos parâmetros são sempre obrigatórios.

Extension functions
fun String.capitalize(): String =
    replaceFirstChar { it.uppercase() }

fun Int.isEven(): Boolean = this % 2 == 0

fun List<Int>.segundo(): Int? =
    if (size >= 2) this[1] else null

// Uso:
"kotlin".capitalize() // "Kotlin"
4.isEven()            // true
listOf(1, 2).segundo() // 2

Extension functions adicionam métodos a classes existentes sem herança nem modificação do código fonte. São resolvidas estaticamente em compilação. Ideais para utilitários e APIs mais expressivas via fun Tipo.metodo().

Operator overloading
data class Vec(val x: Int, val y: Int)

operator fun Vec.plus(o: Vec) =
    Vec(x + o.x, y + o.y)

operator fun Vec.times(n: Int) =
    Vec(x * n, y * n)

val v = Vec(1, 2) + Vec(3, 4)  // Vec(4, 6)
val d = Vec(1, 2) * 3          // Vec(3, 6)

Operator overloading permite usar +, -, *, [] com tipos customizados. Basta marcar a função com operator e usar o nome convencionado (plus, times, get).

Default e named args
fun greet(nome: String, prefixo: String = "Olá") =
    "$prefixo, $nome!"

greet("Ana")              // "Olá, Ana!"
greet("Ana", "Bom dia")   // "Bom dia, Ana!"

// Named arguments:
fun config(host: String, port: Int = 80, ssl: Boolean = false) {}
config("localhost", ssl = true) // port default 80

Parâmetros com default eliminam sobrecargas — a mesma função serve para várias chamadas. Named args melhoram a legibilidade e permitem saltar parâmetros com default, especificando só os necessários.

Higher-order functions
// Recebe função:
fun operacao(a: Int, b: Int, op: (Int, Int) -> Int): Int {
    return op(a, b)
}
operacao(3, 4) { x, y -> x + y } // 7

// Retorna função:
fun multiplicador(fator: Int): (Int) -> Int =
    { n -> n * fator }

val triplo = multiplicador(3)
triplo(5) // 15

Higher-order functions recebem ou retornam outras funções — a base da programação funcional. O tipo (Int, Int) -> Int descreve a assinatura. Permitem criar factories, estratégias e composição de comportamento.

Varargs e spread
// vararg: N argumentos do mesmo tipo
fun soma(vararg nums: Int) = nums.sum()
soma(1, 2, 3, 4, 5) // 15

// Spread operator (*):
val arr = intArrayOf(1, 2, 3)
soma(*arr) // 6

// vararg com outros parâmetros:
fun log(nivel: String, vararg msgs: String) {
    msgs.forEach { println("[$nivel] $it") }
}
log("INFO", "arrancou", "pronto")

vararg aceita N argumentos do mesmo tipo (internamente é Array). O spread operator (*) passa um array como vararg. Pode combinar com parâmetros normais usando named args.

Lambdas
val soma = { a: Int, b: Int -> a + b }
val dobro: (Int) -> Int = { it * 2 }
val nada: () -> Unit = { println("oi") }

// it: nome implícito (1 parâmetro)
listOf(1, 2, 3).forEach { println(it) }

// Múltiplos parâmetros:
val mult = { x: Int, y: Int -> x * y }
mult(3, 4) // 12

Lambdas são funções anónimas de primeira classe. it é o nome implícito quando há um só parâmetro. O tipo (Int) -> Int descreve a assinatura. São a base das operações de coleções e DSLs.

Scope functions
// let: transforma + null-safety
val len = nome?.let { it.trim().length }

// apply: configura (retorna o próprio)
val p = Pessoa().apply { nome = "Ana"; idade = 25 }

// run: bloco com resultado
val info = p.run { "$nome tem $idade" }

// also: efeito secundário
lista.also { println("Tam: ${it.size}") }

// with: operações no objeto
with(p) { println(nome); println(idade) }

Scope functions eliminam variáveis temporárias. let para transformações null-safe, apply para configuração/builders, run para computação, also para efeitos secundários e with para múltiplas operações no mesmo objeto.

Funções infix e suspend
// infix: chamada sem ponto nem parênteses
infix fun Int.mais(x: Int) = this + x
val r = 5 mais 3   // 8

// "to" é infix (cria Pair):
val par = "a" to 1  // Pair("a", 1)

// suspend: função de coroutine
suspend fun fetchDados(): String {
    delay(1000)
    return "dados"
}

Funções infix (1 parâmetro, membro ou extensão) podem ser chamadas sem ponto nem parênteses — to é o exemplo mais comum. suspend marca funções que podem ser suspensas em coroutines (só chamáveis de outra suspend ou coroutine).

Trailing lambda
// Lambda como último argumento:
listOf(1, 2, 3).forEach({ println(it) })

// Trailing lambda (fora dos parênteses):
listOf(1, 2, 3).forEach { println(it) }

// Se for o único argumento:
listOf(1, 2, 3).map { it * 2 }

// Vários lambdas:
fun build(init: Builder.() -> Unit, validate: () -> Boolean) {}
build({ /* config */ }) { /* valida */ }

Quando o lambda é o último argumento, pode ir fora dos parênteses (trailing lambda) — essencial para APIs fluentes e DSLs. Se for o único argumento, os parênteses podem ser omitidos por completo.

Inline functions
// inline copia o lambda para o call site:
inline fun medirTempo(bloco: () -> Unit) {
    val inicio = System.currentTimeMillis()
    bloco()
    println("${System.currentTimeMillis() - inicio}ms")
}

// noinline: não inline este lambda
inline fun foo(a: () -> Unit, noinline b: () -> Unit) {}

// crossinline: proíbe non-local return
inline fun bar(crossinline b: () -> Unit) {}

inline copia o corpo do lambda para o call site, eliminando alocação de objetos — crucial em loops. noinline exclui um lambda específico; crossinline proíbe non-local returns quando o lambda é chamado noutro contexto.

Classes e Objetos


11 cards
Classes e construtores
class Pessoa(val nome: String, var idade: Int) {
    var email: String = ""

    init {
        require(idade >= 0) { "Idade inválida" }
    }

    // Construtor secundário:
    constructor(nome: String) : this(nome, 0)
}

val p = Pessoa("Ana", 25)

O primary constructor na declaração da classe é conciso — val/var criam propriedades automaticamente. O bloco init valida na criação. Construtores secundários delegam no primário com this().

Interfaces
interface Nadador {
    fun nadar(): String

    // Método com implementação default:
    fun flutuar(): String = "a flutuar"
}

interface Voador {
    fun voar(): String
}

class Pato : Nadador, Voador {
    override fun nadar() = "a nadar"
    override fun voar() = "a voar"
}

Interfaces podem ter métodos abstratos e métodos com implementação default. Uma classe pode implementar múltiplas interfaces (mas herdar de uma só classe). Se houver conflito de defaults, use super<Interface>.

Companion object
class User private constructor(val nome: String) {
    companion object Factory {
        fun create(json: String): User = User(json)
        fun guest() = User("Convidado")
        const val MAX = 100
    }
}

// Acesso via nome da classe:
val u = User.create("{...}")
val g = User.guest()
println(User.MAX)

companion object substitui membros static do Java — acede-se via ClassName.membro(). Ideal para factories, constantes (const val) e métodos utilitários. Pode ter nome ou ser anónimo.

Propriedades
class Retangulo(val largura: Int, val altura: Int) {
    // Propriedade calculada:
    val area: Int
        get() = largura * altura

    // Com setter customizado:
    var nome: String = ""
        set(value) {
            field = value.trim()
        }
}

val r = Retangulo(3, 4)
println(r.area) // 12 (calculado)

Propriedades podem ter get() customizado (calculadas) e set() com lógica. O field é a backing field (o valor real). Não há métodos getter/setter boilerplate como em Java — tudo é propriedade.

Sealed classes
sealed class Resultado<out T> {
    data class Sucesso<T>(val dados: T) : Resultado<T>()
    data class Erro(val msg: String) : Resultado<Nothing>()
    object ACarregar : Resultado<Nothing>()
}

fun tratar(r: Resultado<String>) = when (r) {
    is Resultado.Sucesso -> "Dados: ${r.dados}"
    is Resultado.Erro -> "Erro: ${r.msg}"
    Resultado.ACarregar -> "..."
} // exaustivo — sem else

sealed class restringe a hierarquia a um conjunto fixo de subclasses. O compilador verifica exaustividade no when — se adicionar um novo caso, todos os when quebram em compilação. Ideal para estados e resultados.

Delegation (by)
interface Printer { fun print(msg: String) }

class ConsolePrinter : Printer {
    override fun print(msg: String) = println(msg)
}

// Delega toda a interface:
class Logger(printer: Printer) : Printer by printer

val log = Logger(ConsolePrinter())
log.print("olá") // imprime no console

A delegation (by) implementa o padrão de delegação sem boilerplate — a classe encaminha todas as chamadas da interface para o objeto delegado. Pode sobrescrever métodos específicos se necessário.

Data classes
data class User(val nome: String, val email: String, val idade: Int)

val u1 = User("Ana", "ana@mail.com", 25)
val u2 = u1.copy(nome = "Maria") // novo objeto

println(u1)        // User(nome=Ana, ...)
println(u1 == u2)  // false (compara valores)

// Destructuring:
val (nome, email) = u1

data class gera automaticamente equals(), hashCode(), toString(), copy() e componentN(). Ideais para modelos, DTOs e estados. O copy() "modifica" imutáveis criando uma cópia com campos alterados.

Enum classes
enum class Estado(val cor: String) {
    ATIVO("#00FF00"),
    INATIVO("#FF0000"),
    PENDENTE("#FFAA00");

    fun descricao() = "Estado: $name ($cor)"
}

val e = Estado.ATIVO
println(e.cor)        // #00FF00
println(e.ordinal)    // 0
val todos = Estado.values()

Enums em Kotlin são classes completas — podem ter propriedades, métodos e implementar interfaces. Cada constante é uma instância acedida por Estado.ATIVO. Úteis para estados e conjuntos fixos; values() lista todos.

Nested e inner classes
class Exterior {
    val x = 10

    // Nested (sem referência ao exterior):
    class Nested {
        fun ola() = "nested"
    }

    // Inner (acede ao exterior):
    inner class Inner {
        fun ola() = "x = $x"
    }
}

val n = Exterior.Nested()
val i = Exterior().Inner()

Classes aninhadas (nested) não têm referência à instância exterior (como static em Java). Classes inner mantêm a referência e acedem aos membros do exterior. Use inner só quando realmente precisa do contexto externo.

Herança (open)
open class Animal(val nome: String) {
    open fun som(): String = "..."
}

class Pato(nome: String) : Animal(nome) {
    override fun som() = "Quack!"
}

// super:
class Cao(nome: String) : Animal(nome) {
    override fun som() = "Au! " + super.som()
}

Classes em Kotlin são final por defeito — use open para permitir herança. Métodos também precisam de open para serem sobrescritos com override. O super chama a implementação da superclasse.

Object (singleton)
// Singleton thread-safe:
object AppConfig {
    val apiUrl = "https://api.exemplo.com"
    fun getVersion() = "1.0.0"
}

// Uso:
println(AppConfig.apiUrl)

// Object expression (anónimo):
val listener = object : OnClickListener {
    override fun onClick() = println("clicado")
}

object cria singletons thread-safe com lazy initialization (uma única instância). Substitui classes utilitárias com métodos estáticos. Object expressions criam instâncias anónimas de interfaces (como classes anónimas em Java).

Coleções


11 cards
Listas
val imutavel = listOf(1, 2, 3, 4, 5)
val mutavel = mutableListOf(1, 2, 3)

mutavel.add(4)
mutavel.removeAt(0)
mutavel[0] = 10

println(imutavel.first())  // 1
println(imutavel.last())   // 5
println(imutavel.size)     // 5

listOf() cria listas imutáveis (read-only) — a API não expõe métodos de modificação. mutableListOf() permite add/remove/set. Prefira imutáveis por defeito para evitar efeitos secundários.

Ordenação
val nums = listOf(5, 2, 8, 1, 9)

val asc = nums.sorted()            // [1,2,5,8,9]
val desc = nums.sortedDescending() // [9,8,5,2,1]

// Por propriedade:
val porIdade = pessoas.sortedBy { it.idade }
val porNome = pessoas.sortedByDescending { it.nome }

// Comparador customizado:
val custom = nums.sortedWith(compareBy({ it % 2 }, { it }))

sorted() retorna nova lista ordenada (não modifica a original). sortedBy ordena por uma propriedade extraída. sortedWith aceita um Comparator customizado. compareBy compõe múltiplos critérios.

flatMap e flatten
val listas = listOf(listOf(1, 2), listOf(3, 4), listOf(5))

val plana = listas.flatten()     // [1,2,3,4,5]
val flat = listas.flatMap { it } // [1,2,3,4,5]

// Transformar e achatar:
val palavras = frases.flatMap { it.split(" ") }

// Palavras únicas de todas as frases:
val unicas = frases
    .flatMap { it.split(" ") }
    .distinct()

flatMap transforma cada elemento numa coleção e achata tudo numa única lista. flatten() achata listas aninhadas diretamente. É ideal para extrair e combinar elementos de estruturas nested.

Map e Set
val mapa = mapOf("a" to 1, "b" to 2, "c" to 3)
println(mapa["a"])              // 1
println(mapa.getOrDefault("z", 0)) // 0

val mutMap = mutableMapOf<String, Int>()
mutMap["d"] = 4

val set = setOf(1, 2, 2, 3, 3) // {1, 2, 3}
println(3 in set) // true

Map armazena pares chave-valor com acesso O(1) por chave. Set garante elementos únicos — duplicados são ignorados. Ambos têm versões mutáveis. O to cria Pair usado na construção de mapas.

Pesquisa
val nums = listOf(5, 2, 8, 1, 9)

val achado = nums.find { it > 7 }   // 8 (primeiro)
val ultimo = nums.findLast { it < 5 } // 1
val indice = nums.indexOf(8)        // 2

val existe = nums.any { it > 8 }    // true
val todos = nums.all { it > 0 }     // true
val nenhum = nums.none { it < 0 }   // true

val conta = nums.count { it % 2 == 0 } // 2

find retorna o primeiro elemento que satisfaz o predicado (ou null). any/all/none verificam condições booleanas. count conta os que passam. indexOf dá a posição de um valor.

distinct, chunked e zip
val unicos = listOf(1, 1, 2, 3, 3).distinct() // [1,2,3]

val grupos = (1..7).chunked(3)
// [[1,2,3], [4,5,6], [7]]

val a = listOf("a", "b", "c")
val b = listOf(1, 2, 3)
val pares = a.zip(b) // [(a,1), (b,2), (c,3)]

val (x, y) = listOf(1, 2, 3).partition { it % 2 == 0 }
// x=[2], y=[1,3]

distinct() remove duplicados. chunked(n) divide em sublistas de tamanho fixo (útil para paginação). zip combina duas listas em pares. partition divide em duas listas (passam/não passam no predicado).

map e filter
val nums = listOf(1, 2, 3, 4, 5, 6)

val dobros = nums.map { it * 2 }
// [2, 4, 6, 8, 10, 12]

val pares = nums.filter { it % 2 == 0 }
// [2, 4, 6]

val nomes = pessoas.map { it.nome }

val maiores = pessoas.filter { it.idade >= 18 }

map transforma cada elemento mantendo o tamanho; filter seleciona os que passam no predicado. Ambas retornam novas listas (não modificam a original). São as operações mais usadas em coleções.

Agrupamento
val pessoas = listOf(
    Pessoa("Ana", 25), Pessoa("Rui", 30), Pessoa("Mia", 25)
)

val porIdade = pessoas.groupBy { it.idade }
// {25=[Ana,Mia], 30=[Rui]}

val nomes = pessoas.associateBy { it.nome }
// {"Ana"=Pessoa(...), ...}

val pares = pessoas.associate { it.nome to it.idade }
// {"Ana"=25, "Rui"=30, "Mia"=25}

groupBy agrupa elementos por chave num Map<K, List<V>>. associateBy cria um Map indexado por chave. associate constrói pares chave-valor arbitrários. Essenciais para transformar listas em mapas.

joinToString e operações de texto
val nums = listOf(1, 2, 3, 4, 5)

val csv = nums.joinToString(", ")
// "1, 2, 3, 4, 5"

val custom = nums.joinToString(
    separator = " | ",
    prefix = "[",
    postfix = "]"
) { it.toString() }
// "[1 | 2 | 3 | 4 | 5]"

val palavras = "a b c".split(" ")  // [a, b, c]
val junto = palavras.joinToString("") // "abc"

joinToString() converte coleções em string com separador, prefixo e postfix customizáveis, e um transformador por elemento. split() faz o inverso. São ideais para construir CSVs, logs e output formatado.

reduce, fold e sum
val nums = listOf(1, 2, 3, 4, 5)

val soma = nums.reduce { acc, n -> acc + n } // 15
val total = nums.sum()                        // 15

// fold: com valor inicial (tipos diferentes):
val texto = nums.fold("") { acc, n -> "$acc$n " }

val produto = nums.fold(1) { acc, n -> acc * n } // 120

reduce acumula todos os elementos num único valor (o primeiro é o inicial). fold aceita um valor inicial explícito e permite tipos diferentes entre acumulador e elementos. sum() é um atalho para somar números.

Sequences (lazy)
// Eager: cria listas intermédias
val r1 = lista.filter { it > 5 }.map { it * 2 }.take(3)

// Lazy: processa elemento a elemento
val r2 = lista.asSequence()
    .filter { it > 5 }
    .map { it * 2 }
    .take(3)
    .toList() // só aqui processa

// Infinita:
val naturais = generateSequence(1) { it + 1 }
val primeiros = naturais.take(10).toList()

Sequences processam elemento a elemento (lazy) em vez de criar listas intermédias a cada operação. Essencial para coleções grandes ou infinitas — só computa o necessário quando a operação terminal (toList()) é chamada.

Avançado


11 cards
Coroutines básico
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000) // suspende sem bloquear
        println("Mundo")
    }
    println("Olá")
}
// Olá (imediato) -> Mundo (após 1s)

Coroutines são funções suspendíveis que permitem concorrência sem threads bloqueadas. launch inicia uma coroutine fire-and-forget. delay suspende sem bloquear a thread — ideal para I/O, timers e rede.

Exceções em coroutines
val handler = CoroutineExceptionHandler { _, e ->
    println("Erro: ${e.message}")
}

CoroutineScope(Dispatchers.Default).launch(handler) {
    throw RuntimeException("falhou")
}

// try/catch com async:
val r = async { operacao() }
try {
    r.await()
} catch (e: Exception) {
    println("erro: $e")
}

Erros em launch propagam para o handler de exceção (CoroutineExceptionHandler). Em async, a exceção é relançada no await() — use try/catch. Um CoroutineScope cancela os filhos em caso de falha (structured concurrency).

Reflection
import kotlin.reflect.full.*

class Pessoa(val nome: String, val idade: Int)

val p = Pessoa("Ana", 25)
val klass = p::class

println(klass.simpleName)        // "Pessoa"
klass.memberProperties.forEach {
    println("${it.name} = ${it.get(p)}")
}

// Function reference:
val fn = ::println
fn("olá")

Reflection (com kotlin-reflect) inspeciona classes, propriedades e funções em runtime. ::class obtém o KClass, memberProperties lista as propriedades. Function references (::println) passam funções como valores.

async / await
fun main() = runBlocking {
    val r1 = async { fetchDados1() }
    val r2 = async { fetchDados2() }

    // Espera ambos em paralelo:
    val total = r1.await() + r2.await()
    println("Total: $total")
}

suspend fun fetchDados1(): Int {
    delay(1000); return 42
}

async inicia uma coroutine que retorna um Deferred (future). await() suspende até o resultado estar disponível. Múltiplos async executam em paralelo — ideal para chamadas de rede concorrentes que reduzem o tempo total.

DSL building
class HtmlBuilder {
    val elements = mutableListOf<String>()
    fun div(c: String) { elements.add("<div>$c</div>") }
    fun p(c: String) { elements.add("<p>$c</p>") }
}

fun html(block: HtmlBuilder.() -> Unit): String {
    val b = HtmlBuilder().apply(block)
    return b.elements.joinToString("\n")
}

val page = html {
    div("Título")
    p("Conteúdo")
}

DSLs em Kotlin usam extension lambdas (Receiver.() -> Unit) para criar APIs declarativas. O objeto recetor torna-se o this implícito dentro do bloco. Usado em Gradle, Ktor, Exposed e Jetpack Compose.

Structured concurrency
fun main() = runBlocking {
    // coroutineScope: espera todos os filhos
    coroutineScope {
        launch { tarefa1() }
        launch { tarefa2() }
    } // só continua quando ambas terminam

    // supervisorScope: falha não cancela irmãos
    supervisorScope {
        val a = launch { throw Exception() }
        val b = launch { tarefa2() } // continua
    }
}

Structured concurrency garante que coroutines têm ciclo de vida definido. coroutineScope espera todos os filhos e propaga falhas. supervisorScope isola falhas — um filho falhar não cancela os irmãos. Essencial para código robusto.

Dispatchers
import kotlinx.coroutines.*

fun main() = runBlocking {
    // CPU-bound:
    val r = withContext(Dispatchers.Default) {
        calculoPesado()
    }

    // I/O (rede, disco):
    val dados = withContext(Dispatchers.IO) {
        lerFicheiro()
    }

    // UI (Android):
    withContext(Dispatchers.Main) { atualizarUi() }
}

Dispatchers definem em que thread pool a coroutine corre. Dispatchers.Default para CPU-bound, Dispatchers.IO para I/O (rede/disco), Dispatchers.Main para a UI (Android). withContext muda de dispatcher sem criar nova coroutine.

Delegates (lazy/observable)
// lazy: calcula 1x no primeiro acesso
val config: Config by lazy {
    Config.carregarDoFicheiro()
}

// observable: notifica mudanças
var nome: String by Delegates.observable("") { _, old, new ->
    println("$old -> $new")
}

// vetoable: pode rejeitar
var idade: Int by Delegates.vetoable(0) { _, _, new ->
    new >= 0
}

by lazy adia computação cara até ao primeiro acesso (singleton thread-safe). observable notifica após cada mudança. vetoable permite rejeitar valores inválidos antes de aplicar. Todos eliminam boilerplate de getter/setter.

Channel (comunicação)
import kotlinx.coroutines.channels.Channel

fun main() = runBlocking {
    val channel = Channel<Int>()

    launch {
        for (i in 1..5) channel.send(i * i)
        channel.close()
    }

    for (valor in channel) {
        println(valor) // 1, 4, 9, 16, 25
    }
}

Channel permite comunicação entre coroutines (produtor/consumidor). send() envia (suspende se cheio), receive() recebe (suspende se vazio). close() termina o canal. Iterar em channel recebe até fechar.

Flow (reativo)
import kotlinx.coroutines.flow.*

fun numeros(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(100)
        emit(i)
    }
}

runBlocking {
    numeros()
        .filter { it % 2 == 0 }
        .map { it * 10 }
        .collect { println(it) } // 20, 40
}

Flow é a API reativa de Coroutines — emite múltiplos valores assincronamente. É cold (só executa quando collect é chamado). Suporta operadores como filter, map, take — similar a RxJava mas integrado.

Annotations
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Log(val nivel: String = "INFO")

@Log(nivel = "DEBUG")
fun operacao() { }

// Annotations da stdlib:
@Deprecated("use novo()", ReplaceWith("novo()"))
fun antigo() {}

@JvmStatic
fun utilitario() {}

Annotations adicionam metadados ao código. @Target define onde se aplica, @Retention se está disponível em runtime. A stdlib tem @Deprecated (com sugestão de substituição) e @JvmStatic/@JvmOverloads para interop com Java.

OO e Genéricos


10 cards
Visibility modifiers
class Exemplo {
    public val a = 1     // público (default)
    private val b = 2    // só nesta classe
    protected val c = 3  // classe + subclasses
    internal val d = 4   // só neste módulo
}

// Top-level:
private fun helper() {}  // só neste ficheiro
internal val config = 1  // só neste módulo

Kotlin tem quatro modificadores: public (default), private (classe/ficheiro), protected (classe + subclasses) e internal (visível no módulo). Tudo é público por defeito — restrinja com private o máximo possível.

reified e inline
// reified mantém o tipo em runtime:
inline fun <reified T> tipoDe(x: Any): Boolean =
    x is T

tipoDe<String>("olá") // true
tipoDe<Int>("olá")    // false

// Uso prático:
inline fun <reified T> Gson.fromJson(json: String): T =
    fromJson(json, T::class.java)

Normalmente os tipos genéricos são apagados em runtime (type erasure). reified (só com inline) preserva o tipo, permitindo verificações is T e obter T::class. Muito usado em serialização e reflection.

Value classes (inline)
@JvmInline
value class Email(val valor: String) {
    init { require("@" in valor) { "email inválido" } }
    fun dominio() = valor.substringAfter("@")
}

val e = Email("ana@mail.com")
println(e.dominio()) // mail.com

// Em runtime é só String (sem overhead de objeto)

value class (inline class) envolve um único valor sem overhead de objeto em runtime — type safety de um tipo distinto com performance de um primitivo. Ideal para IDs, emails e newtypes com validação no init.

Abstract classes
abstract class Forma {
    abstract fun area(): Double

    // Método concreto:
    fun descrever() = "Área: ${area()}"
}

class Circulo(val raio: Double) : Forma() {
    override fun area() = Math.PI * raio * raio
}

// Não pode instanciar:
// val f = Forma() // ERRO

Classes abstract não podem ser instanciadas e podem ter métodos abstratos (sem corpo) que as subclasses implementam. Combinam herança com contrato — úteis quando há código partilhado mas comportamento específico por subtipo.

Type checks e casts
fun processar(obj: Any) {
    if (obj is String) {
        println(obj.uppercase()) // smart cast
    }

    // Cast explícito:
    val s = obj as? String  // seguro (null se falhar)
    val n = obj as Int      // lança ClassCastException

    // Star projection:
    fun tamanho(lista: List<*>) = lista.size
}

is verifica o tipo com smart cast automático. as? é o cast seguro (retorna null se falhar); as lança exceção. List<*> (star projection) aceita listas de qualquer tipo quando não precisa do tipo concreto.

Builder pattern
class PedidoBuilder {
    var item: String = ""
    var quantidade: Int = 1
    var urgente: Boolean = false
    fun build() = Pedido(item, quantidade, urgente)
}

fun pedido(block: PedidoBuilder.() -> Unit): Pedido =
    PedidoBuilder().apply(block).build()

val p = pedido {
    item = "Livro"
    quantidade = 2
    urgente = true
}

O builder pattern em Kotlin usa extension lambda (Builder.() -> Unit) com apply — cria uma DSL declarativa para configurar objetos. Mais legível que construtores com muitos parâmetros opcionais.

Generics
// Classe genérica:
class Box<T>(val value: T)
val intBox = Box(42)       // Box<Int> inferido
val strBox = Box("texto")

// Função genérica:
fun <T> primeiro(lista: List<T>): T? =
    lista.firstOrNull()

// Constraint:
fun <T : Comparable<T>> maior(a: T, b: T): T =
    if (a > b) a else b

Generics permitem código reutilizável com type safety. O compilador infere tipos na maioria dos casos. Constraints (T : Comparable<T>) restringem os tipos aceites a quem implementa uma interface.

equals e hashCode
class Ponto(val x: Int, val y: Int) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Ponto) return false
        return x == other.x && y == other.y
    }

    override fun hashCode(): Int = 31 * x + y
}

// data class gera isto automaticamente!
data class Ponto2(val x: Int, val y: Int)

Sobrescreva equals() e hashCode() juntos para comparação por valor (necessário em Set/Map). === compara referências. Prefira data class que gera ambos automaticamente.

Variância (in/out)
// out (covariância): só produz T
class Produtor<out T>(val item: T) {
    fun obter(): T = item
}
val p: Produtor<Any> = Produtor("texto") // OK

// in (contravariância): só consome T
class Consumidor<in T> {
    fun consumir(item: T) { println(item) }
}
val c: Consumidor<String> = Consumidor<Any>() // OK

out (covariância) significa que a classe só produz TProdutor<String> é um Produtor<Any>. in (contravariância) só consome T. Permite subtipagem segura em genéricos (PECS: Producer Extends, Consumer Super).

Sealed interfaces
sealed interface Evento {
    data class Clique(val x: Int, val y: Int) : Evento
    data class Tecla(val codigo: Int) : Evento
    object Fecho : Evento
}

fun tratar(e: Evento) = when (e) {
    is Evento.Clique -> "clique em ${e.x},${e.y}"
    is Evento.Tecla -> "tecla ${e.codigo}"
    Evento.Fecho -> "fecho"
} // exaustivo

sealed interface (Kotlin 1.5+) combina interfaces com hierarquia fechada — as implementações são conhecidas em compilação, permitindo when exaustivo. Mais flexível que sealed class porque uma classe pode implementar vários.

Erros e Null Safety


10 cards
Try / Catch
// try é expressão:
val n = try {
    "42".toInt()
} catch (e: NumberFormatException) {
    0
} finally {
    println("tentativa feita")
}

// Múltiplos catch:
try {
    operacao()
} catch (e: IllegalArgumentException) {
    println("argumento: ${e.message}")
} catch (e: Exception) {
    println("outro: $e")
}

try é uma expressão que retorna valor — o último statement do bloco try ou catch é o resultado. O finally executa sempre. Pode ter múltiplos catch por tipo de exceção.

Safe calls encadeados
// Encadeamento seguro:
val cidade = pessoa?.endereco?.cidade?.nome

// Equivalente sem safe call (verboso):
// if (pessoa != null && pessoa.endereco != null ...)

// Com let (só executa se não-null):
pessoa?.endereco?.let { end ->
    println(end.cidade)
    enviarCorreio(end)
}

// Combina com elvis:
val nome = pessoa?.nome ?: "Desconhecido"

Safe calls (?.) encadeiam-se — se qualquer elemento for null, o resultado é null sem NullPointerException. Combinados com let executam blocos só quando toda a cadeia existe. Substitui null-checks aninhados.

Operador !! (not-null)
var nome: String? = "Kotlin"

// Força non-null (perigoso):
val len = nome!!.length

// Se for null -> NullPointerException
// nome = null
// nome!!.length // CRASH!

// Prefira sempre alternativas seguras:
val a = nome?.length ?: 0       // elvis
val b = nome?.let { it.length } // let

O operador !! converte um tipo nullable em non-null, lançando NullPointerException se for null. É um "eu sei o que faço" — evite-o. Prefira ?., ?: ou let para código verdadeiramente null-safe.

Throw e exceções
fun dividir(a: Int, b: Int): Int {
    if (b == 0) {
        throw IllegalArgumentException("divisão por zero")
    }
    return a / b
}

// throw é expressão (tipo Nothing):
fun validar(x: Int): Int =
    if (x < 0) throw IllegalArgumentException("negativo")
    else x

throw lança exceções (todas são Throwable). Em Kotlin não há checked exceptions — não é obrigatório declarar ou capturar. throw é uma expressão do tipo Nothing, útil em expressões if/elvis.

require e check
// require: valida argumentos (IllegalArgumentException)
fun criarUser(nome: String, idade: Int) {
    require(nome.isNotBlank()) { "nome vazio" }
    require(idade in 0..150) { "idade inválida: $idade" }
}

// check: valida estado (IllegalStateException)
fun iniciar() {
    check(!iniciado) { "já iniciado" }
    iniciado = true
}

// assert (só em debug):
assert(x > 0) { "x deve ser positivo" }

require() valida argumentos de funções (lança IllegalArgumentException). check() valida o estado do objeto (lança IllegalStateException). Ambos aceitam uma mensagem lazy. Tornam as precondições explícitas e documentadas.

Plataform types
// Código Java pode retornar null:
// String s = javaLib.getText(); // pode ser null

// Kotlin trata como platform type (T!):
val s = javaLib.getText() // String!
// Compila, mas pode dar NPE em runtime

// Torne explícito:
val seguro: String? = javaLib.getText()
val len = seguro?.length ?: 0

// @Nullable/@NotNull no Java ajudam

Platform types (T!) vêm de código Java sem anotações de nullability — o Kotlin não sabe se podem ser null. A responsabilidade é do programador. Anote como String? explicitamente e use safe calls ao interagir com Java.

Custom exceptions
class SaldoInsuficienteException(
    val saldo: Double,
    val valor: Double
) : Exception("Saldo $saldo insuficiente para $valor")

fun levantar(saldo: Double, valor: Double) {
    if (valor > saldo) {
        throw SaldoInsuficienteException(saldo, valor)
    }
}

try {
    levantar(100.0, 150.0)
} catch (e: SaldoInsuficienteException) {
    println(e.saldo)
}

Crie exceções customizadas estendendo Exception (recuperável) ou RuntimeException. Adicione propriedades relevantes (como saldo) para contexto. Mantenha a hierarquia de exceções simples e específica.

Result type
val resultado: Result<Int> = runCatching {
    "42".toInt()
}

resultado.onSuccess { println("Ok: $it") }
resultado.onFailure { println("Erro: $it") }

// Obter valor:
val n = resultado.getOrDefault(0)
val n2 = resultado.getOrNull()
val n3 = resultado.getOrThrow()

// Encadear:
val dobro = resultado.map { it * 2 }

Result<T> encapsula sucesso ou falha sem lançar exceção. runCatching{} captura exceções num Result. onSuccess/onFailure tratam cada caso; map transforma o sucesso. Alternativa funcional a try/catch.

Elvis operator
// Default se null:
val len = nome?.length ?: 0

// Com throw:
fun obterNome(): String {
    return input?.trim()
        ?: throw IllegalStateException("sem nome")
}

// Encadeado:
val valor = a ?: b ?: c ?: "default"

// Com return:
fun processar(x: Int?) {
    val n = x ?: return // early return se null
    println(n * 2)
}

O operador elvis (?:) fornece um valor alternativo quando o lado esquerdo é null. Combina com throw (lança se null), return (early exit) e encadeamento. É a ferramenta central da null-safety idiomática.

runCatching
// Em vez de try/catch:
val valor = runCatching {
    api.fetchDados()
}.getOrElse { erro ->
    println("Falha: $erro")
    dadosLocais()
}

// Transformar resultado:
val processado = runCatching { parse(input) }
    .map { it.uppercase() }
    .recover { "default" }
    .getOrThrow()

runCatching{} executa um bloco e captura qualquer exceção num Result. getOrElse fornece alternativa em caso de falha; recover transforma o erro em valor. Permite pipelines funcionais sem try/catch verboso.

Ferramentas e Idioms


10 cards
Idioms essenciais
// Singleton:
object Recurso { }

// Imutável por defeito:
val lista = listOf(1, 2, 3)

// Expressões em tudo:
val max = if (a > b) a else b

// Data class para modelos:
data class User(val nome: String)

// Scope functions:
val s = StringBuilder().apply { append("oi") }

Idioms Kotlin: prefira val (imutável), use data class para modelos, expressões em vez de statements, object para singletons e scope functions para eliminar variáveis temporárias. Código conciso e expressivo.

Testes
import kotlin.test.*

class CalculadoraTest {
    @Test
    fun `soma dois números`() {
        val r = soma(2, 3)
        assertEquals(5, r)
    }

    @Test
    fun `lança em divisão por zero`() {
        assertFailsWith<IllegalArgumentException> {
            dividir(1, 0)
        }
    }

    @BeforeTest fun setup() { }
    @AfterTest fun cleanup() { }
}

Kotlin Test (kotlin.test) fornece asserções multiplataforma. Testes podem ter nomes com espaços entre crases. assertEquals verifica igualdade, assertFailsWith verifica exceções. @BeforeTest/@AfterTest fazem setup/cleanup.

Performance e boxing
// Primitivos otimizados:
val arr = IntArray(1000) { it }  // sem boxing
val lista = List(1000) { it }    // Int? boxed

// inline evita alocação de lambdas:
inline fun repetir(n: Int, bloco: () -> Unit) {
    for (i in 0 until n) bloco()
}

// Sequences para grandes coleções:
val r = dados.asSequence()
    .filter { it > 0 }
    .map { it * 2 }
    .take(10)
    .toList()

Use IntArray/DoubleArray para evitar boxing. inline elimina alocação de objetos lambda em loops. Sequences evitam listas intermédias em cadeias de operações. Prefira val e imutabilidade para otimizações do compilador.

Stdlib útil
// Strings:
"olá".uppercase()        // "OLÁ"
"a,b,c".split(",")       // [a, b, c]
"  x  ".trim()           // "x"

// Números:
val r = (1..10).random()
val n = 3.14159.roundToInt() // 3

// Coleções:
listOf(1, 2, 3).shuffled()
listOf(1, 2, 3).reversed()
"a" to 1                 // Pair

A stdlib do Kotlin é rica: uppercase/split/trim para strings, random/roundToInt para números, shuffled/reversed para coleções. O to cria Pair. Reduz muito o código utilitário.

Coroutines test
import kotlinx.coroutines.test.*

@Test
fun testeAsync() = runTest {
    // delay é saltado automaticamente:
    val resultado = fetchDados() // suspend
    assertEquals("dados", resultado)
}

@Test
fun testeConcorrente() = runTest {
    val r1 = async { operacao1() }
    val r2 = async { operacao2() }
    assertEquals(42, r1.await() + r2.await())
}

kotlinx-coroutines-test fornece runTest que executa coroutines de teste de forma determinística — delay é saltado (virtual time), tornando testes rápidos. async/launch funcionam normalmente dentro do teste.

Boas práticas
// 1. val por defeito, var só se necessário
val total = calcular()

// 2. Imutável: listOf em vez de mutableListOf
val itens = listOf(1, 2, 3)

// 3. Expressões em vez de statements
val r = if (x > 0) x else -x

// 4. data class para modelos
data class Dto(val id: Int, val nome: String)

// 5. Null-safety em vez de !!
val n = valor?.length ?: 0

Boas práticas: val por defeito, coleções imutáveis, expressões em vez de statements, data class para modelos e null-safety idiomática (nunca !!). Siga o Kotlin Coding Conventions oficial para estilo consistente.

Interop com Java
// Kotlin chama Java diretamente:
val lista = java.util.ArrayList<String>()
val data = java.time.LocalDate.now()

// Java chama Kotlin:
// (compila para bytecode JVM normal)

// Anotações para interop:
@JvmStatic fun util() {}       // static real
@JvmOverloads fun f(a: Int, b: Int = 0) {}
@JvmField val x = 1            // campo público

@file:JvmName("Utils")         // nome do ficheiro

Kotlin é 100% interoperável com Java — chama bibliotecas Java diretamente e vice-versa. Anotações @JvmStatic, @JvmOverloads, @JvmField e @JvmName controlam como o código Kotlin aparece para o Java.

Multiplatform
// commonMain (partilhado):
expect fun plataforma(): String

// androidMain:
actual fun plataforma() = "Android"

// iosMain:
actual fun plataforma() = "iOS"

// jvmMain:
actual fun plataforma() = "JVM"

// Uso no código comum:
fun saudar() = "Olá de ${plataforma()}"

Kotlin Multiplatform partilha lógica de negócio entre plataformas (Android, iOS, JVM, JS, Native). expect declara uma API no código comum; actual fornece a implementação específica de cada plataforma. Ideal para networking, modelos e validação.

Gradle Kotlin DSL
// build.gradle.kts:
plugins {
    kotlin("jvm") version "1.9.0"
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    testImplementation(kotlin("test"))
}

tasks.test {
    useJUnitPlatform()
}

kotlin {
    jvmToolchain(17)
}

O Gradle Kotlin DSL (build.gradle.kts) usa Kotlin em vez de Groovy — type-safe, autocompletar no IDE e refatorável. plugins{} aplica plugins, dependencies{} gere dependências. É o padrão recomendado para projetos novos.

Null-safe design
// Retorne não-null sempre que possível:
fun nome(): String = "Ana"   // nunca null

// Use nullable só quando necessário:
fun encontrar(id: Int): User? =
    base.firstOrNull { it.id == id }

// Force o tratamento no call site:
val u = encontrar(1)
    ?: throw NotFoundException("user 1")

// Tipos não-null + elvis = código seguro

Design null-safe: retorne tipos não-null por defeito; use nullable (?) só quando a ausência é um estado válido. Force o tratamento no call site com ?: ou let. Quanto menos nullables na API, mais seguro o código.