Quando utilizar inicialização lazy do Kotlin

Tocando num fluxograma genérico

Quando escrevemos código em Activities, é muito comum criarmos properties que serão reutilizadas por funções membros, como por exemplo, o famoso adapter de um RecyclerView.

Porém, no Kotlin, precisamos considerar algumas abordagens que não são tão convenientes, como é caso de valores nullables para o RecyclerView e seu adapter:

Código da Activity de lista de notas com properties para o Adapter e o RecyclerView com valores nullable

Nessa abordagem temos 2 detalhes em ambas as properties:

  • Podem receber null: todas as vezes seremos obrigados a garantir a referência não nula;
  • Valores são mutáveis: corremos o risco de esquecer a inicialização.

Para lidar com esse tipo de situação, temos algumas alternativas e vamos explorá-las neste artigo, bora lá? 😃

Inicialização atrasada com lateinit

A primeira delas é conhecida por inicialização atrasada, tecnicamente, lateinit. A partir dela, podemos declarar properties similares a atributos no Java, ou seja, sem inicializar em sua declaração:

class NoteListActivity : AppCompatActivity() {

private lateinit var adapter: NoteListAdapter
private lateinit var recyclerView: RecyclerView

// Rest of the code

}

Com o lateinit temos uma grande vantagem, não é permitido tipos nullables. Portanto, resolvemos o detalhe de ter que lidar com null. Mas, essa solução mantém outros detalhes não tão desejáveis e vamos entendê-los.

Detalhes problemáticos da inicialização por lateinit

Todas as vezes que utilizarmos uma property lateinit ela precisa de uma inicialização, caso contrário, o aplicativo vai quebrar com a seguinte exception:

kotlin.UninitializedPropertyAccessException: lateinit property adapter has not been initialized

Isso significa que qualquer falha do desenvolvedor, pode ocasionar em um crash inesperado. Existem maneiras de evitar esse tipo de comportamento, como é o caso de sempre verificar se existe valor com a property via referência isInitialized (que só funciona a partir da versão 1.2).

Mas ainda deixa a responsabilidade para quem está escrevendo o código…

O outro detalhe é que ainda mantemos uma property mutável que pode ter o seu valor alterado a qualquer momento. Em outras palavras, o lateinit resolve um problema, mas mantém outros detalhes não desejados!

Inicialização por delegação de property via lazy

Uma outra técnica de inicialização é por meio de delegação de propriedade. Dentre as opções existentes na Kotlin Standard Library, temos a inicialização preguiçosa, tecnicamente, lazy initialization.

A ideia do lazy é permitir a inicialização de property na primeira vez que for utilizada, e então, nas próximas vezes de uso, o valor atribuído é devolvido imediatamente, como se fosse um cache:

class NoteListActivity : AppCompatActivity() {

private val adapter: NoteListAdapter by lazy {
NoteListAdapter(notes(), this)
}
private val recyclerView: RecyclerView by lazy {
note_list_recyclerview
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_note_list)
configureRecyclerView()
}

private fun configureRecyclerView() {
// initialize the recyclerView and adapter here
recyclerView.adapter = adapter
val layoutManager = StaggeredGridLayoutManager(
2, StaggeredGridLayoutManager.VERTICAL)
recyclerView.layoutManager = layoutManager
}

// rest of the code

}

Dessa forma, a inicialização vai ocorrer sem o problema de, por exemplo, inicializar o RecyclerView no momento errado devido ao ciclo de vida.

Também temos um ganho de performance, pois o código dentro do escopo do lazy só vai ser executado durante a inicialização, portanto, se a inicialização for custosa, só vai ser feita quando for necessário/utilizado e apenas uma única vez. 😉

É válido notar que a inicialização lazy, obriga o uso de properties imutáveis (um benefício e tanto!), e também, é mais flexível, pois permite declarar o tipo da property tanto explicitamente, como também implicitamente:

private val adapter: NoteListAdapter by lazy {
NoteListAdapter(notes(), this)
}
private val recyclerView by lazy {
note_list_recyclerview
}

Também, se preferir, é possível utilizar tipos que aceitam nulos… Como o nosso objetivo é evitá-los ao máximo, não parece uma boa decisão mantê-los.

Além dos benefícios, é muito importante conhecer os detalhes impactantes de soluções poderosas como essa, sendo assim, vamos explorar essas situações.

Gif de gatinho com frase ‘sounds good to me’

Peculiaridades da inicialização lazy

A primeira delas está relacionada no nosso problema inicial, uma inicialização de property segura considerando o ciclo de vida de componentes do Android.

Em outras palavras, se acessarmos as properties de inicialização lazy em locais indevidos, como por exemplo, o RecyclerView antes de setar/criar a View:

private val adapter by lazy {
NoteListAdapter(notes(), this)
}
private val recyclerView by lazy {
note_list_recyclerview
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
recyclerView.adapter = adapter
setContentView(R.layout.activity_note_list)
// rest of the code
}

Tomaremos um NPE… Sim! Por mais que o Kotlin tente evitar ao máximo o famoso NPE, quando estamos no cenário lazy, essas peculiaridades devem ser levadas em consideração.

Um outro caso, é que a inicialização, por padrão, é thread safe, ou seja, tudo é feito de maneira síncrona. Se esse comportamento não for desejado, é possível modificar o modo do lazy para os seguintes enums:

  • PUBLICATION: quando você quer permitir a alteração via acesso simultâneo de threads; (Bem arriscado!)
  • NONE: quando você tem a certeza de que sempre vai operar em uma thread e não quer manter o lock. (Arriscado também, e você será o responsável por garantir isso)

Para modificar os modos, basta apenas enviar o modo via argumento:

private val adapter by lazy(mode = LazyThreadSafetyMode.NONE) {
NoteListAdapter(notes(), this)
}
O modo padrão é o enum SYNCHRONIZED, por isso que ele é thread safe quando não definimos o modo.

Para saber mais

Além dos pontos que foram apresentados no artigo, existem ferramentas de avaliação de qualidade de código com observações em relação ao uso do lateinit.

Um exemplo é o Detekt que sugere a ordem esperada do modificador de acesso e também tem uma opção para criar alertas sobre uso do lateinit que pode ser ativada que na própria descrição mostra um pouco do que foi falado no artigo, a possibilidade de modificar o valor ou não ser inicializado.

Inclusive, uma das sugestões como alternativas é a utilização de propriedades delegadas 😉

Conclusão

Neste artigo aprendemos possibilidades comuns que temos para inicializar properties no Kotlin.

Em cenários que temos ciclo de vida, notamos que existe diversas peculiaridades, como lidar com null, inicializações que podem ser atrasadas (lateinit). ou até mesmo, inicializações que podem ser preguiças (lazy).

Gostou do artigo? Deixe o seu feedback nos comentários! Se possível, aquele like para aumentar a divulgação.


Quer aprender mais sobre Kotlin tanto no mundo mobile como no back-end? Então confira este agregador de conteúdo onde listo todos os conteúdos que escrevi de Kotlin e os que serão publicados mais pra frente 😉