A inicialização diferida (lazy initialization) é um mecanismo em Kotlin no qual uma propriedade de um objeto é inicializada não no momento da criação, mas no primeiro acesso a ela. De acordo com JetBrains, 2024, lateinit e lazy são duas ferramentas integradas para implementar essa estratégia. Ambas resolvem o problema da inicialização diferida, mas diferem fundamentalmente no mecanismo de funcionamento e no escopo de aplicação.
Pontos principais
Inicialização diferida é um padrão no qual uma propriedade de classe recebe seu valor não no momento da construção do objeto, mas depois, sob demanda. Em Kotlin, esse padrão é implementado de duas maneiras fundamentalmente diferentes: o modificador lateinit e o delegado lazy.
Ambos os mecanismos resolvem um problema comum — uma propriedade deve existir na classe, mas seu valor é desconhecido no momento da criação do objeto, ou seu cálculo é muito intensivo em recursos para ser executado desnecessariamente. De acordo com Google I/O 2023, até 40% das propriedades em um aplicativo Android típico podem ser otimizadas por meio da inicialização diferida, reduzindo o tempo de inicialização em 15–25%.
A escolha entre lateinit e lazy é determinada por três fatores: mutabilidade da propriedade (var ou val), seu ciclo de vida (atribuição única ou múltipla) e requisitos de segurança de thread (acesso single-thread ou multi-thread).
O primeiro e mais comum cenário é a Injeção de Dependência. O framework (Dagger, Hilt, Koin) injeta dependências após a criação do objeto, portanto, a propriedade não pode ser inicializada no construtor. Sem lateinit, todas as dependências teriam que ser declaradas como nullable e verificadas a cada uso.
O segundo cenário são os recursos pesados: bancos de dados, clientes de rede, gerenciadores de arquivos. Sua criação requer tempo e memória, portanto, devem ser inicializados apenas quando realmente usados. lazy é ideal para esses casos, garantindo criação única.
A terceira situação são os componentes Android (Activity, Fragment, ViewModel), cujo ciclo de vida é gerenciado pelo sistema operacional. Propriedades que dependem de onCreate, onViewCreated ou do bloco init do ViewModel não podem ser inicializadas no construtor.
lateinit é um modificador para propriedades var que permite ao compilador Kotlin adiar a inicialização. O compilador não exige uma atribuição de valor no construtor, mas gera uma verificação em tempo de execução em cada acesso: se a propriedade não estiver inicializada, lança UninitializedPropertyAccessException.
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
Limitações do lateinit: a propriedade deve ser declarada como var (não val), não anulável e não de tipo primitivo (Int, Double, Boolean, etc.). O motivo é que tipos primitivos compilam para primitivos JVM, que não têm um estado “não inicializado”. Para propriedades anuláveis, a inicialização diferida é desnecessária: null já significa ausência de valor.
Para verificar o estado de uma propriedade lateinit, use a referência embutida através do operador ::: ::propertyName.isInitialized. Esta é a única maneira segura de verificar se uma propriedade foi inicializada sem arriscar uma exceção. A verificação está disponível apenas na mesma classe ou classe interna, não em código externo.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit não adiciona sobrecarga após a inicialização: uma vez atribuído o valor, o acesso à propriedade é idêntico ao acesso direto ao campo. O único custo é a verificação de inicialização em cada leitura antes da atribuição. Após a inicialização, o compilador JIT otimiza a verificação.
Uma observação importante: propriedades lateinit não podem ser usadas em classes inline e não são suportadas para propriedades com getters/setters personalizados. Se uma propriedade requer acesso computado, use lazy em vez de lateinit.
lazy é um delegado de propriedade integrado à biblioteca padrão do Kotlin. Ele calcula o valor no primeiro acesso à propriedade e armazena em cache o resultado para todas as chamadas subsequentes. Ao contrário do lateinit, lazy funciona apenas com val, tornando a propriedade imutável após a inicialização.
class UserRepository {
private val database: Database by lazy {
Database.create("users.db")
}
fun getUser(id: String): User {
return database.query("SELECT * FROM users WHERE id = ?", id)
}
}
lazy aceita um parâmetro opcional LazyThreadSafetyMode que controla o mecanismo de segurança de thread. O padrão é SYNCHRONIZED — verificação dupla com bloqueio, garantindo inicialização única mesmo sob acesso concorrente de várias threads.
O modo PUBLICATION permite inicialização paralela: várias threads podem executar o bloco de inicialização simultaneamente, mas o resultado só é aceito da primeira a concluir. É mais rápido que SYNCHRONIZED sob alta contenção, mas aumenta o consumo de recursos.
O modo NONE desativa completamente a sincronização. Use-o apenas para propriedades cujo acesso é garantido a partir de uma única thread. Neste modo, lazy opera com sobrecarga mínima — quase como uma atribuição direta.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy é a escolha certa para dependências inicializadas uma vez: repositórios, clientes de rede, caches, bancos de dados. A semântica de val protege contra sobrescrita acidental, e a segurança de thread padrão torna o código seguro em ambientes multi-thread. lazy também funciona corretamente com tipos primitivos, o que é impossível com lateinit.
No Android, lazy é frequentemente usado para inicializar dependências do ViewModel via by viewModels() ou para criar clientes Retrofit. No entanto, tenha cuidado: se um bloco lazy capturar uma referência a uma Activity ou Fragment, pode causar um vazamento de memória, pois o delegado mantém o fechamento durante toda a vida da propriedade.
A escolha entre lateinit e lazy não é uma questão de preferência, mas uma decisão arquitetônica determinada pela natureza da propriedade. Cada mecanismo resolve sua própria tarefa e suas áreas de aplicação se sobrepõem apenas parcialmente.
| Critério | lateinit | lazy |
|---|---|---|
| Tipo de propriedade | apenas var | apenas val |
| Anulável | não permitido | permitido |
| Tipos primitivos | não permitidos | permitidos |
| Segurança de thread | não garantida | SYNCHRONIZED por padrão |
| Verificação de estado | ::x.isInitialized | não necessária |
| Exceção em erro | UninitializedPropertyAccessException | erro no bloco init |
| Cache | não aplicável | cálculo único |
| Android Binding | View Binding, Data Binding | não usado |
| Frameworks DI | Dagger, Hilt, Koin | injeção manual |
Use lateinit quando uma propriedade precisar mudar após a inicialização ou sua criação for gerenciada por código externo. Um exemplo típico é o View Binding em Android Activity: o binding é criado no onCreate, mas permanece var porque o framework não suporta val para este cenário.
Use lazy quando uma propriedade for inicializada uma vez, seu cálculo for caro e o valor não mudar durante a vida do objeto. Um exemplo clássico é a criação preguiçosa de um cliente Retrofit ou banco de dados Room no primeiro acesso ao repositório.
Ambos os mecanismos podem ser usados simultaneamente dentro de uma mesma classe. Por exemplo, lateinit para View Binding e lazy para um repositório. Esta é uma prática normal que reflete diferentes requisitos para diferentes propriedades. O principal é não confundir a semântica: não use lateinit onde val é necessário, e não use lazy para propriedades que precisam ser reatribuídas.
O erro mais comum com lateinit é acessar a propriedade antes de ela ser inicializada. Isso leva ao UninitializedPropertyAccessException, que não é capturado em tempo de compilação porque Kotlin confia que o desenvolvedor garanta a ordem correta de inicialização. A solução é sempre verificar o estado via ::property.isInitialized antes do acesso em situações ambíguas.
O segundo problema comum é usar lateinit para propriedades que são semanticamente val. Se o valor é definido uma vez e nunca muda, lazy é a escolha mais correta. Torna a propriedade imutável, evita sobrescrita acidental e adiciona segurança de thread gratuitamente.
O terceiro erro é lazy com efeitos colaterais. O bloco de inicialização lazy não deve modificar o estado externo nem depender da ordem de inicialização de outras propriedades lazy, pois a sequência de cálculo depende do primeiro acesso e pode não ser óbvia. Se propriedades lazy referenciarem umas às outras, isso leva a dependência circular e StackOverflowError.
O quarto problema são vazamentos de memória via lazy no Android. Se um bloco lazy capturar uma referência a uma Activity ou Fragment, o delegado mantém o fechamento e o coletor de lixo não pode liberar o componente mesmo após sua destruição. A solução é usar lazy apenas com objetos de curta duração ou passar o contexto da Application em vez da Activity.
O quinto erro típico é tentar aplicar lateinit a tipos primitivos. O compilador Kotlin bloqueia isso no nível de sintaxe, mas os desenvolvedores tentam contornar a limitação através de wrappers anuláveis. Isso leva a verificações null desnecessárias e anula completamente os benefícios da inicialização diferida.
Perguntas frequentes
lateinit é um modificador para propriedades var, permitindo a inicialização após o construtor. lazy é um delegado para propriedades val, que calcula o valor no primeiro acesso e o armazena em cache. lateinit não suporta tipos primitivos e nullable, enquanto lazy é seguro para threads por padrão.
Sim, através da referência embutida à propriedade: ::propertyName.isInitialized. O método retorna true se a propriedade já foi inicializada. Esta é a única maneira segura de evitar UninitializedPropertyAccessException ao trabalhar com campos lateinit.
Tipos primitivos — Int, Double, Boolean e outros — compilam para primitivos JVM (int, double, boolean), que não têm um estado “não inicializado”. lateinit usa null como um sinalizador, e primitivos não podem ser null, portanto o mecanismo é fisicamente impossível para esses tipos.
O padrão é LazyThreadSafetyMode.SYNCHRONIZED — verificação dupla com bloqueio, garantindo inicialização única sob acesso concorrente de várias threads. Para cenários single-thread, use NONE; para alta contenção, use PUBLICATION.
Quando a propriedade precisar mudar após a inicialização ou sua criação for gerenciada pelo framework. Um exemplo típico é o View Binding em Android Activity: o binding é criado no onCreate e deve ser var. Para dependências val inicializadas uma vez, use lazy.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também