O que e lateinit em Kotlin?

O modificador lateinit permite declarar uma propriedade var nao-nullable sem inicializa-la no momento da declaracao. Voce esta dizendo ao compilador: “eu garanto que vou inicializar essa propriedade antes de usa-la”. Isso e util quando a inicializacao depende de um framework (como injecao de dependencias) ou acontece em um metodo de ciclo de vida.

Sem lateinit, toda propriedade nao-nullable precisa ser inicializada no construtor ou na declaracao. Com lateinit, voce adia essa inicializacao para um momento posterior.

Sintaxe basica

class MinhaActivity {
    lateinit var repositorio: Repositorio

    fun onCreate() {
        repositorio = Repositorio() // Inicializada depois da construcao
        repositorio.carregar()
    }
}

Sem lateinit, voce teria que usar nullable (Repositorio?) e lidar com verificacoes de null em todo lugar, ou inicializar com um valor dummy que nao faz sentido.

Regras do lateinit

O lateinit tem restricoes especificas:

class Exemplo {
    // OK: var + tipo nao-nullable + nao-primitivo
    lateinit var nome: String

    // ERRO: val nao pode ser lateinit (precisa ser var)
    // lateinit val constante: String

    // ERRO: tipos primitivos nao podem ser lateinit
    // lateinit var contador: Int

    // ERRO: tipo nullable nao faz sentido com lateinit
    // lateinit var opcional: String?
}

Resumo das regras:

  • Deve ser var (nao val).
  • Deve ser tipo nao-nullable.
  • Nao pode ser tipo primitivo (Int, Long, Boolean, etc.).
  • Nao pode ter getter ou setter customizado.
  • Pode ser usada em propriedades de classe ou top-level.

Verificando inicializacao com isInitialized

A partir do Kotlin 1.2, voce pode verificar se uma propriedade lateinit ja foi inicializada:

class Servico {
    lateinit var conexao: Conexao

    fun verificar() {
        if (::conexao.isInitialized) {
            println("Conexao ativa: $conexao")
        } else {
            println("Conexao ainda nao foi estabelecida")
        }
    }

    fun desconectar() {
        if (::conexao.isInitialized) {
            conexao.fechar()
        }
    }
}

A sintaxe ::propriedade.isInitialized usa referencia de propriedade. Isso e util em metodos de limpeza ou quando a inicializacao e condicional.

lateinit em Android

O caso de uso mais classico de lateinit e em Activities e Fragments do Android:

class UsuarioActivity : AppCompatActivity() {
    lateinit var binding: ActivityUsuarioBinding
    lateinit var viewModel: UsuarioViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityUsuarioBinding.inflate(layoutInflater)
        setContentView(binding.root)
        viewModel = ViewModelProvider(this)[UsuarioViewModel::class.java]

        configurarUI()
    }

    private fun configurarUI() {
        binding.botaoSalvar.setOnClickListener {
            viewModel.salvar()
        }
    }
}

Aqui, binding e viewModel nao podem ser inicializados no construtor porque dependem do ciclo de vida da Activity. O lateinit e a solucao natural.

lateinit com injecao de dependencias

Frameworks como Dagger, Hilt e Koin frequentemente usam lateinit para injecao:

class MeuServico {
    @Inject
    lateinit var repositorio: Repositorio

    @Inject
    lateinit var logger: Logger

    fun executar() {
        logger.info("Executando...")
        repositorio.processar()
    }
}

O framework de DI preenche as propriedades apos a construcao do objeto, antes de qualquer metodo de negocio ser chamado.

lateinit vs lazy

Ambos adiam a inicializacao, mas de formas diferentes:

class Comparacao {
    // lateinit: inicializacao explicita, var, mutavel
    lateinit var valorLateInit: String

    // lazy: inicializacao automatica na primeira leitura, val, imutavel
    val valorLazy: String by lazy {
        println("Inicializando lazy...")
        "Valor calculado"
    }

    fun inicializar() {
        valorLateInit = "Valor definido"
    }
}

fun main() {
    val obj = Comparacao()

    // lazy: inicializa automaticamente na primeira leitura
    println(obj.valorLazy) // Imprime "Inicializando lazy..." e "Valor calculado"

    // lateinit: voce controla quando inicializar
    obj.inicializar()
    println(obj.valorLateInit)
}

Quando usar cada um:

  • lateinit: quando a inicializacao depende de um fator externo (DI, ciclo de vida, framework).
  • lazy: quando o valor pode ser calculado a partir de informacoes ja disponiveis, mas voce quer adiar o calculo.

O que acontece se acessar antes de inicializar

class Perigo {
    lateinit var dados: String

    fun acessar() {
        println(dados) // UninitializedPropertyAccessException!
    }
}

fun main() {
    val obj = Perigo()
    obj.acessar() // kotlin.UninitializedPropertyAccessException:
                   // lateinit property dados has not been initialized
}

A excecao UninitializedPropertyAccessException e lancada com uma mensagem clara indicando qual propriedade nao foi inicializada. E mais informativa que um NullPointerException.

lateinit em testes

lateinit e muito comum em classes de teste:

class CalculadoraTest {
    lateinit var calculadora: Calculadora
    lateinit var logger: MockLogger

    @BeforeEach
    fun setup() {
        logger = MockLogger()
        calculadora = Calculadora(logger)
    }

    @Test
    fun `soma dois numeros`() {
        val resultado = calculadora.somar(2, 3)
        assertEquals(5, resultado)
    }

    @Test
    fun `registra operacao no log`() {
        calculadora.somar(2, 3)
        assertTrue(logger.mensagens.contains("Soma: 2 + 3 = 5"))
    }
}

O metodo @BeforeEach inicializa as propriedades antes de cada teste, garantindo um estado limpo.

Quando usar lateinit

  • Injecao de dependencias: quando frameworks preenchem propriedades apos a construcao.
  • Ciclo de vida: em Android, quando propriedades dependem de onCreate ou onViewCreated.
  • Testes: para inicializar objetos em metodos @BeforeEach ou @Before.
  • Configuracao tardia: quando a inicializacao depende de dados que chegam depois da construcao.

Erros comuns

  1. Acessar antes de inicializar: o erro mais obvio. Sempre garanta que a propriedade e inicializada antes do primeiro acesso. Use isInitialized quando houver duvida.

  2. Usar lateinit quando lazy e melhor: se o valor pode ser calculado a partir de dados ja disponiveis, lazy e mais seguro porque e automatico e imutavel.

  3. Usar lateinit para contornar null safety: declarar lateinit so para evitar ? e verificacoes de null e um mau uso. Se o valor pode legitimamente ser null, use String?.

  4. Lateinit em propriedades que nunca sao reinicializadas: se a propriedade e definida uma unica vez e nunca muda, considere lazy com val para garantir imutabilidade.

  5. Nao tratar o caso de nao-inicializacao em cleanup: em metodos como onDestroy, verificar isInitialized antes de acessar propriedades que podem nao ter sido inicializadas (ex: se onCreate falhou).

Termos relacionados

  • val: variavel somente leitura que nao pode ser usada com lateinit.
  • var: variavel mutavel, requisito para lateinit.
  • lazy: delegacao de propriedade que inicializa na primeira leitura, alternativa ao lateinit.
  • Nullable: tipos que aceitam null (String?), alternativa ao lateinit quando null e um estado valido.
  • Property Delegate: mecanismo que lazy usa internamente, permitindo logica customizada de leitura e escrita.
  • Dependency Injection: padrao de design onde lateinit e frequentemente usado para receber dependencias.

O lateinit e uma ferramenta pragmatica que resolve situacoes reais onde a inicializacao no construtor nao e possivel. Usado com disciplina, ele mantem o codigo limpo e livre de verificacoes de null desnecessarias, enquanto a verificacao em tempo de execucao garante que erros de inicializacao sejam detectados cedo com mensagens claras.