O que é Property Delegate em Kotlin?

Property Delegate (delegação de propriedade) é um mecanismo do Kotlin que permite delegar a lógica de leitura (get) e escrita (set) de uma propriedade para outro objeto. Em vez de implementar a lógica diretamente na propriedade, você usa a palavra-chave by para apontar para um objeto delegado que controla o comportamento.

Os delegates mais conhecidos da biblioteca padrão são lazy, observable e vetoable, mas você pode criar os seus próprios para qualquer necessidade.

Sintaxe básica

class Exemplo {
    // Delegando para lazy: inicializa na primeira leitura
    val configuracao: String by lazy {
        println("Calculando...")
        "Valor configurado"
    }
}

fun main() {
    val obj = Exemplo()
    println(obj.configuracao) // Imprime "Calculando..." e "Valor configurado"
    println(obj.configuracao) // Imprime apenas "Valor configurado" (ja calculado)
}

A palavra-chave by indica que a propriedade esta delegando suas operações de get (e set, se for var) para o objeto a direita.

Como funciona internamente

Quando você escreve val x by delegate, o compilador gera código equivalente a:

// Você escreve:
class Exemplo {
    val x: String by MeuDelegate()
}

// O compilador gera algo como:
class Exemplo {
    private val x_delegate = MeuDelegate()

    val x: String
        get() = x_delegate.getValue(this, ::x)
}

Para propriedades var, o compilador também gera o setter:

// var y by delegate gera:
var y: String
    get() = y_delegate.getValue(this, ::y)
    set(value) { y_delegate.setValue(this, ::y, value) }

Delegates da biblioteca padrão

lazy

Inicializa o valor na primeira leitura e cacheia para acessos subsequentes:

val dadosPesados: List<String> by lazy {
    println("Carregando dados pesados...")
    carregarDoBanco()
}

Modos de thread safety do lazy:

// Thread-safe (padrao): sincronizado
val a by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { calcular() }

// Sem sincronizacao: mais rapido, mas nao thread-safe
val b by lazy(LazyThreadSafetyMode.NONE) { calcular() }

// Publication: permite calculo duplicado, mas garante que todos veem o mesmo valor
val c by lazy(LazyThreadSafetyMode.PUBLICATION) { calcular() }

observable

Notifica quando o valor muda:

import kotlin.properties.Delegates

var nome: String by Delegates.observable("Inicial") { propriedade, valorAntigo, valorNovo ->
    println("${propriedade.name} mudou de '$valorAntigo' para '$valorNovo'")
}

fun main() {
    nome = "Ana"    // Imprime: nome mudou de 'Inicial' para 'Ana'
    nome = "Bruno"  // Imprime: nome mudou de 'Ana' para 'Bruno'
}

vetoable

Permite rejeitar mudancas de valor:

var idade: Int by Delegates.vetoable(0) { _, _, novoValor ->
    novoValor >= 0 // So aceita valores nao-negativos
}

fun main() {
    idade = 25   // Aceito
    println(idade) // 25
    idade = -5   // Rejeitado
    println(idade) // 25 (nao mudou)
}

notNull

Similar ao lateinit, mas funciona com tipos primitivos delegados:

var contador: Int by Delegates.notNull<Int>()

fun main() {
    // println(contador) // IllegalStateException!
    contador = 42
    println(contador)  // 42
}

Criando um delegate customizado

Para criar um delegate, implemente os operadores getValue e opcionalmente setValue:

import kotlin.reflect.KProperty

class LoggingDelegate<T>(private var valor: T) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("Lendo ${property.name}: $valor")
        return valor
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, novoValor: T) {
        println("Escrevendo ${property.name}: $valor -> $novoValor")
        valor = novoValor
    }
}

class Usuario {
    var nome: String by LoggingDelegate("Sem nome")
    var idade: Int by LoggingDelegate(0)
}

fun main() {
    val u = Usuario()
    u.nome = "Ana"     // Escrevendo nome: Sem nome -> Ana
    println(u.nome)    // Lendo nome: Ana
    u.idade = 30       // Escrevendo idade: 0 -> 30
}

Exemplo prático: SharedPreferences delegate

Um caso de uso real no Android e delegar propriedades para SharedPreferences:

class PreferenceDelegate(
    private val prefs: SharedPreferences,
    private val chave: String,
    private val padrao: String
) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return prefs.getString(chave, padrao) ?: padrao
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, valor: String) {
        prefs.edit().putString(chave, valor).apply()
    }
}

fun SharedPreferences.string(chave: String, padrao: String = "") =
    PreferenceDelegate(this, chave, padrao)

// Uso
class Configurações(prefs: SharedPreferences) {
    var tema: String by prefs.string("tema", "claro")
    var idioma: String by prefs.string("idioma", "pt-BR")
    var nomeUsuario: String by prefs.string("nome_usuario", "")
}

fun main() {
    val config = Configurações(obterPrefs())
    config.tema = "escuro"           // Salva automaticamente no SharedPreferences
    println(config.tema)             // Le automaticamente do SharedPreferences
}

Delegate para Map

Kotlin tem suporte embutido para delegar propriedades a um Map:

class Usuario(mapa: Map<String, Any?>) {
    val nome: String by mapa
    val idade: Int by mapa
    val email: String by mapa
}

fun main() {
    val dados = mapOf(
        "nome" to "Ana",
        "idade" to 30,
        "email" to "ana@email.com"
    )
    val usuario = Usuario(dados)
    println(usuario.nome)  // Ana
    println(usuario.idade) // 30
}

Para propriedades mutaveis, use MutableMap:

class Configuração(mapa: MutableMap<String, Any?>) {
    var tema: String by mapa
    var fontSize: Int by mapa
}

Interfaces ReadOnlyProperty e ReadWriteProperty

Para maior clareza, seus delegates podem implementar interfaces oficiais:

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class ValidatedString(private var valor: String) : ReadWriteProperty<Any?, String> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): String = valor

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        require(value.isNotBlank()) { "${property.name} nao pode ser vazio" }
        valor = value
    }
}

class Formulario {
    var nome: String by ValidatedString("")
    var email: String by ValidatedString("")
}

Quando usar Property Delegates

  • Inicialização preguicosa: lazy para valores calculados sob demanda.
  • Observacao de mudancas: observable e vetoable para reagir a alteracoes de estado.
  • Persistencia transparente: delegar para SharedPreferences, banco de dados ou cache.
  • Validação automatica: verificar restricoes a cada escrita sem repetir código.
  • Logging e auditoria: registrar acessos e modificacoes de propriedades.

Casos de Uso no Mundo Real

  1. Persistencia transparente em Android: delegates que encapsulam SharedPreferences ou DataStore permitem que propriedades de uma classe de configuração leiam e escrevam automaticamente no armazenamento persistente. O desenvolvedor interage com a propriedade como se fosse um campo normal, sem se preocupar com a camada de persistencia.

  2. Injecao de dependência em frameworks como Koin: o Koin usa property delegates (by inject() e by viewModel()) para injetar dependências de forma preguicosa em Activities, Fragments e ViewModels. Isso elimina boilerplate de resolucao manual e torna o código mais declarativo.

  3. Binding de views em Android legado: antes do Jetpack Compose, bibliotecas como ViewBinding e ButterKnife usavam delegates para vincular views do XML a propriedades da Activity ou Fragment, evitando chamadas repetidas a findViewById.

  4. Validacao e sanitizacao automatica de dados: em sistemas de formularios ou APIs, delegates customizados podem validar valores a cada escrita (rejeitando dados invalidos) ou sanitizar entradas (removendo espacos extras, normalizando formatos) de forma transparente para o restante do código.

Boas Praticas

  • Use lazy com o modo de thread safety adequado: em contextos single-thread (como a thread principal do Android), use LazyThreadSafetyMode.NONE para evitar o overhead de sincronizacao desnecessaria.
  • Implemente as interfaces ReadOnlyProperty ou ReadWriteProperty: ao criar delegates customizados, implementar essas interfaces torna o contrato explicito e facilita a leitura e manutenção do código.
  • Evite efeitos colaterais pesados em getValue: o getter de uma propriedade delegada e chamado toda vez que a propriedade e lida. operações caras (como acesso a disco ou rede) devem ser cacheadas internamente pelo delegate.
  • Prefira delegates a campos com lógica duplicada: se você perceber que várias propriedades da mesma classe possuem lógica identica de validacao, logging ou persistencia, extraia essa lógica para um delegate reutilizavel.
  • Documente delegates customizados com exemplos de uso: como delegates alteram o comportamento implicito de propriedades, outros desenvolvedores precisam entender o que acontece por tras do by. Um KDoc com exemplo e essencial.

Perguntas Frequentes

P: Qual a diferenca entre lazy e lateinit? R: lazy e um delegate para propriedades val que inicializa o valor na primeira leitura e o cacheia. lateinit e um modificador para propriedades var que permite declarar a propriedade sem valor inicial, atribuindo-o depois. lazy garante que o valor e calculado exatamente uma vez; lateinit permite reatribuicao e não suporta tipos primitivos.

P: Posso usar property delegates com propriedades de nivel de arquivo (top-level)? R: Sim. Property delegates funcionam em propriedades de classes, objetos, interfaces e também em propriedades top-level. Para propriedades top-level, o parametro thisRef do getValue/setValue sera null, já que não há um objeto proprietario.

P: Como faco para delegar uma propriedade a outra propriedade da mesma classe? R: A partir do Kotlin 1.4, você pode usar a sintaxe val novoNome by ::nomeAntigo para delegar uma propriedade a outra usando referência de propriedade. Isso e útil para renomear propriedades mantendo compatibilidade com código existente.

P: Property delegates impactam a performance da aplicação? R: O impacto e minimo para a maioria dos casos. O delegate adiciona uma camada de indirection (uma chamada de método extra para getValue/setValue), mas o JIT do JVM normalmente otimiza isso. O lazy com SYNCHRONIZED tem um custo de sincronizacao na primeira leitura, mas leituras subsequentes sao rapidas.

Erros comuns

  1. Nao entender que lazy e thread-safe por padrão: o modo SYNCHRONIZED tem overhead. Use NONE quando thread safety não e necessária.

  2. Criar delegates com efeitos colaterais pesados no getValue: o getter e chamado toda vez que a propriedade e lida. Operações caras devem ser cacheadas.

  3. Esquecer de implementar setValue para var: se o delegate não tem setValue, ele só funciona com val.

  4. Confundir delegação de propriedade com delegação de classe: class A by b delega a interface de uma classe; val x by delegate delega uma propriedade. São mecanismos diferentes.

  5. Usar lazy em propriedades que mudam: lazy e para val. Se você precisa de inicialização tardia com var, use lateinit ou Delegates.notNull.

Termos relacionados

  • lazy: delegate da biblioteca padrão para inicialização preguicosa.
  • lateinit: alternativa ao delegate notNull para inicialização tardia.
  • Delegation: conceito mais amplo que inclui delegação de classes e interfaces.
  • observable/vetoable: delegates para monitorar e controlar mudancas de valor.
  • val/var: delegates para val precisam apenas de getValue; delegates para var precisam também de setValue.
  • KProperty: objeto de reflexao que representa a propriedade sendo delegada.

Property Delegates são um dos recursos mais elegantes do Kotlin, permitindo separar a lógica transversal (logging, persistencia, válidação) da lógica de negócio de forma limpa e reutilizavel. Dominar esse conceito abre portas para APIs expressivas e código sem boilerplate.