Quando utilizar inicialização lazy do Kotlin
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:
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.
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 😉