Neste tutorial, você vai aprender tudo sobre delegação de propriedades em Kotlin — um recurso elegante que permite reutilizar lógica de acesso a propriedades sem repetir código. Vamos explorar a palavra-chave by, os delegates da standard library como lazy, observable e vetoable, delegação via Map e como criar seus próprios delegates customizados. Se você já conhece classes e objetos e lambdas, esta pronto para esse topico.

O que é Delegação de Propriedades?

Em Kotlin, uma propriedade normalmente armazena seu valor em um campo (backing field). Mas as vezes você precisa de comportamento extra toda vez que o valor e lido ou escrito — logar a mudanca, validar o novo valor, calcular sob demanda, buscar de um cache e por aí vai.

Você poderia escrever getters e setters customizados, mas se essa mesma lógica se repete em várias propriedades ou classes, o código fica duplicado. A delegação de propriedades resolve esse problema: você delega o armazenamento é o acesso da propriedade para outro objeto usando a palavra-chave by.

class Exemplo {
    var texto: String by MeuDelegate()
}

Quando alguem lê exemplo.texto, o Kotlin chama o método getValue() do delegate. Quando alguem escreve exemplo.texto = "novo", o Kotlin chama setValue(). O objeto delegate encapsula toda a lógica.

lazy: Inicialização Preguicosa

O delegate mais usado do Kotlin e provavelmente o lazy. Ele adia a inicialização de uma propriedade até o momento em que ela e acessada pela primeira vez. Depois disso, o valor calculado fica em cache e e retornado diretamente nas leituras seguintes.

class ConfiguracaoApp {
    val dadosPesados: List<String> by lazy {
        println("Carregando dados... (so acontece uma vez)")
        carregarDoBancoDeDados()
    }

    private fun carregarDoBancoDeDados(): List<String> {
        // Simula uma operacao custosa
        return listOf("config1", "config2", "config3")
    }
}

fun main() {
    val config = ConfiguracaoApp()
    println("Objeto criado, dados ainda nao carregados")
    println(config.dadosPesados) // Aqui o bloco lazy e executado
    println(config.dadosPesados) // Aqui usa o cache
}

Por padrão, lazy e thread-safe (usa LazyThreadSafetyMode.SYNCHRONIZED). Se você sabe que a propriedade sera acessada apenas por uma thread, pode usar lazy(LazyThreadSafetyMode.NONE) para evitar o custo de sincronizacao:

val valor: String by lazy(LazyThreadSafetyMode.NONE) {
    "Inicializado sem sincronizacao"
}

O lazy só funciona com val, já que o valor e calculado uma única vez e nunca muda depois disso. Para cenários onde você precisa de inicialização tardia com var, considere usar lateinit.

observable: Reagindo a Mudancas

O delegate Delegates.observable() permite executar um callback toda vez que o valor de uma propriedade muda. E muito útil para atualizar a interface, logar mudancas ou notificar outros componentes.

import kotlin.properties.Delegates

class Carrinho {
    var quantidadeItens: Int by Delegates.observable(0) { propriedade, valorAntigo, valorNovo ->
        println("${propriedade.name} mudou de $valorAntigo para $valorNovo")
        atualizarBadge(valorNovo)
    }

    private fun atualizarBadge(quantidade: Int) {
        println("Badge atualizado: $quantidade itens")
    }
}

fun main() {
    val carrinho = Carrinho()
    carrinho.quantidadeItens = 3  // Imprime a mudanca e atualiza o badge
    carrinho.quantidadeItens = 7  // Idem
}

O callback recebe tres parametros: a referência a propriedade (KProperty), o valor antigo e o valor novo. Importante: o callback e chamado depois que o valor já foi atribuido.

vetoable: Validando Antes de Atribuir

Se você precisa validar um valor antes de aceita-lo, use Delegates.vetoable(). O callback retorna true para aceitar a mudanca ou false para rejeita-la, mantendo o valor anterior.

import kotlin.properties.Delegates

class Conta {
    var saldo: Double by Delegates.vetoable(0.0) { _, valorAntigo, valorNovo ->
        if (valorNovo < 0) {
            println("Operação negada: saldo nao pode ser negativo (tentou: $valorNovo)")
            false
        } else {
            println("Saldo atualizado: $valorAntigo -> $valorNovo")
            true
        }
    }
}

fun main() {
    val conta = Conta()
    conta.saldo = 1000.0  // Aceito
    conta.saldo = 500.0   // Aceito
    conta.saldo = -200.0  // Rejeitado, saldo continua 500.0
    println("Saldo final: ${conta.saldo}") // 500.0
}

Esse padrão e particularmente útil para propriedades que representam configurações ou limites que não devem ultrapassar certos valores.

Delegação por Map

Um caso de uso muito prático e delegar propriedades para um Map. Isso e especialmente útil ao trabalhar com dados dinâmicos, como JSON parseado ou configurações carregadas de um arquivo.

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

fun main() {
    val mapa = mapOf(
        "nome" to "Carlos Oliveira",
        "email" to "[email protected]",
        "idade" to 32
    )

    val usuario = Usuario(mapa)
    println("${usuario.nome}, ${usuario.email}, ${usuario.idade}")
    // Carlos Oliveira, [email protected], 32
}

O Kotlin usa o nome da propriedade como chave no mapa. Para propriedades mutaveis (var), basta usar MutableMap:

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

fun main() {
    val dados = mutableMapOf<String, Any?>(
        "tema" to "escuro",
        "fontSize" to 14
    )

    val config = Configuração(dados)
    config.tema = "claro"
    println(dados["tema"]) // "claro" — o mapa e atualizado
}

Esse padrão e uma alternativa elegante ao uso de data classes quando a estrutura dos dados não e conhecida em tempo de compilação.

Criando Delegates Customizados

Para criar seu próprio delegate, você precisa implementar as interfaces ReadOnlyProperty (para val) ou ReadWriteProperty (para var):

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

class TrimDelegate : ReadWriteProperty<Any?, String> {
    private var valor: String = ""

    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return valor
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        valor = value.trim()
    }
}

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

fun main() {
    val form = Formulario()
    form.nome = "  Maria Silva  "
    form.email = "  [email protected]  "
    println("'${form.nome}'")   // 'Maria Silva'
    println("'${form.email}'")  // '[email protected]'
}

O TrimDelegate automaticamente remove espacos em branco das pontas toda vez que um valor e atribuido. Esse tipo de lógica reutilizavel e o ponto forte da delegação.

Caso Prático: SharedPreferences Delegate

No desenvolvimento Android, um dos usos mais elegantes de delegates customizados e encapsular o acesso a SharedPreferences. Em vez de repetir chamadas getString(), putString() e afins espalhadas pelo código, você cria um delegate que faz isso transparentemente.

import android.content.SharedPreferences
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class StringPreference(
    private val prefs: SharedPreferences,
    private val chave: String,
    private val valorPadrao: String = ""
) : ReadWriteProperty<Any?, String> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return prefs.getString(chave, valorPadrao) ?: valorPadrao
    }

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

// Função auxiliar para facilitar o uso
fun SharedPreferences.string(chave: String, padrao: String = "") =
    StringPreference(this, chave, padrao)

// Uso
class Preferencias(prefs: SharedPreferences) {
    var nomeUsuario: String by prefs.string("nome_usuario")
    var tema: String by prefs.string("tema", "escuro")
    var idioma: String by prefs.string("idioma", "pt-BR")
}

Repare como a extension function string() torna a declaracao das propriedades extremamente limpa. Quem usa a classe Preferencias nem precisa saber que por tras dos panos os dados estao sendo persistidos em SharedPreferences.

Caso Prático: Delegate com Log para ViewModel

Outro cenário comum e criar um delegate que loga todas as mudancas de estado em um ViewModel:

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

class LoggedProperty<T>(
    private var valor: T,
    private val tag: String = "ViewModel"
) : ReadWriteProperty<Any?, T> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): T = valor

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        println("[$tag] ${property.name}: $valor -> $value")
        valor = value
    }
}

fun <T> logged(valorInicial: T, tag: String = "ViewModel") =
    LoggedProperty(valorInicial, tag)

// Uso
class PedidoViewModel {
    var status: String by logged("pendente", "PedidoVM")
    var total: Double by logged(0.0, "PedidoVM")
}

fun main() {
    val vm = PedidoViewModel()
    vm.status = "processando"
    // [PedidoVM] status: pendente -> processando
    vm.total = 149.90
    // [PedidoVM] total: 0.0 -> 149.90
}

Esse tipo de delegate e muito útil durante o desenvolvimento para rastrear mudancas de estado sem poluir a lógica de negocios com chamadas de log.

Combinando Delegates

Uma técnica avançada e combinar delegates. Por exemplo, uma propriedade que e lazy na primeira leitura mas observable nas escritas subsequentes. Embora a standard library não oferca isso diretamente, você pode implementar o comportamento que precisar criando seu próprio delegate:

class LazyObservable<T>(
    private val inicializador: () -> T,
    private val onChange: (antigo: T, novo: T) -> Unit
) : ReadWriteProperty<Any?, T> {

    private var valor: T? = null
    private var inicializado = false

    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        if (!inicializado) {
            valor = inicializador()
            inicializado = true
        }
        @Suppress("UNCHECKED_CAST")
        return valor as T
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        val antigo = if (inicializado) valor as T else inicializador()
        valor = value
        inicializado = true
        onChange(antigo, value)
    }
}

Quando Usar Delegação de Propriedades

A delegação de propriedades brilha nos seguintes cenários:

  • Inicialização preguicosa: quando calcular o valor inicial e custoso e pode não ser necessário. Use lazy.
  • Observacao de mudancas: quando outros componentes precisam ser notificados. Use observable.
  • Validação: quando valores precisam ser verificados antes da atribuicao. Use vetoable.
  • Persistencia transparente: SharedPreferences, banco de dados ou cache sem poluir o código de negocios.
  • Dados dinâmicos: quando a estrutura vem de um mapa ou JSON. Use delegação por Map.

Evite usar delegates em excesso ou para lógica trivial. Se um getter ou setter simples resolve, não há necessidade de abstrair com delegação. Como em tudo no desenvolvimento de software, o bom senso e a melhor ferramenta.

Conclusão

A delegação de propriedades e um dos recursos mais elegantes do Kotlin e exemplifica bem a filosofia da linguagem de reduzir boilerplate sem sacrificar clareza. Com lazy, observable, vetoable e delegates customizados, você consegue encapsular comportamentos complexos de forma reutilizavel e transparente.

Se você quer se aprofundar em padrões de delegação de forma mais ampla — incluindo delegação de classes com by — consulte a entrada sobre delegation no glossario. E para ver delegates em acao dentro de arquiteturas reais, confira o tutorial sobre MVVM, onde delegates como lazy e observables são amplamente utilizados.