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.