Referências de método e lambdas em propriedades preguiçosas
É tudo sobre o bytecode
Conteudo
TLDR
Tenha cuidado ao usar referências de método e lambdas no Kotlin, especialmente em combinação com propriedades e classes lazy que são criadas pela estrutura do Android (atividades, fragmentos, serviços, etc). As referências de método e lambdas são traduzidas em diferentes java-bytecode que podem causar problemas se não forem tratadas corretamente.
O problema
Eu tive esse problema quando meu projeto de repente começou a travar em uma das telas com um estranho NullPointerException (NPE). Pareceu que, ao obter informações da intenção durante o uso de injeções assistidas, a intenção era nula (Spoiler: tinha algo a ver com inicializações de apresentador preguiçosas e referências de método).
Vamos começar
Todo o código pode ser encontrado no repositório do autor (Aqui).
Para ilustrar o problema, vamos criar um aplicativo simples. Consiste em duas telas. A primeira tela é o local onde as SeekBars podem ser usadas para filtrar pessoas dependendo de sua idade. Quando o usuário terminar de deslizar, ele pode abrir uma segunda tela que mostrará as pessoas com base nesses critérios (o próprio filtro é passado dentro do intent). Então, se o usuário clicar no nome de qualquer pessoa, ele receberá um brinde para essa pessoa em particular.
Usaremos a seguinte lógica para criar as telas:
private const val KEY_USER_PREDICATE = "KEY_USER_PREDICATE" fun newInstanceLambda(context: Context, predicate: ((UserItem) -> Boolean)? = null): Intent = Intent(context, UserLambdaActivity::class.java).apply { putExtra(KEY_USER_PREDICATE, predicate as Serializable?) } fun newInstanceReference(context: Context, predicate: ((UserItem) -> Boolean)? = null): Intent = Intent(context, UserReferenceActivity::class.java).apply { putExtra(KEY_USER_PREDICATE, predicate as Serializable?) }
O código para criar essas atividades é o mesmo, exceto para a atividade específica que o usuário está iniciando. As atividades também são as mesmas, exceto por um pequeno detalhe. Uma das atividades passa um método de referência para o adaptador do usuário. Outra atividade usa lambda.
class UserReferenceActivity : BaseUserActivity() { override val userAdapter: UserAdapter = UserAdapter(presenter::greetUser) } class UserLambdaActivity : BaseUserActivity() { override val userAdapter: UserAdapter = UserAdapter { presenter.greetUser(it) } } abstract class BaseUserActivity : AppCompatActivity(), UserView { abstract val userAdapter: UserAdapter /** * Precisamos criar o apresentador preguiçosamente, pois ele deve usar dados do intent que estarão disponíveis após a atividade .onCreate **/ @Suppress("UNCHECKED_CAST") protected val presenter: UserPresenter by lazy { UserPresenter.newInstance( userView = this, predicate = intent.getSerializableExtra(KEY_USER_PREDICATE) as ((UserItem) -> Boolean)? ) } ... }
Como você pode ver, o apresentador é criado preguiçosamente, porque ele precisa acessar as informações passadas dentro do intent (que estarão disponíveis depois que o framework fizer toda a lógica de inicialização relacionada à atividade). Essa é a injeção assistida, pois o parâmetro é resolvido no tempo de execução e é retirado do intent. Vamos tentar esses dois exemplos. O exemplo que usa lambdas funciona perfeitamente. No entanto, quando tentamos usar referências de método (que são mais concisas e frequentemente usadas), é quando as coisas ficam interessantes.
java.lang.NullPointerException: Attempt to invoke virtual method ‘java.io.Serializable android.content.Intent.getSerializableExtra(java.lang.String)’ on a null object reference
UserPresenter é criado lentamente, portanto, deve ser criado após o método onCreate da atividade e o intent não deve ser nulo nesse momento. Porém, no caso de referências de método, o UserPresenter é criado antes disso. Na verdade, é inicializado no momento em que o adaptador é criado. Como isso é possível? Vamos mergulhar no código java descompilado e tentar descobrir.
Primeiro, vamos dar uma olhada no código que usa lambdas.
@NotNull private final UserAdapter userAdapter = new UserAdapter((Function1)(new Function1() { ... public final void invoke(int it) { UserLambdaActivity.this.getPresenter().greetUser(it); } }));
É muito simples. Uma instância anônima da classe Function1 é criada. E o método invoke é aquele que será chamado quando o usuário clicar no item. Mas no caso da referência do método, é um pouco mais complicado:
@NotNull private final UserAdapter userAdapter = new UserAdapter((Function1)(new Function1(this.getPresenter()) { ... }));
Função1 anônima também é criada. Mas aqui o apresentador é passado para o construtor como parâmetro. Mesmo que seja preguiçoso, isso irá inicializá-lo instantaneamente, fazendo com que tente acessar o intent que, em resultado, produz NPE.
Lazy inicialização
Isso também pode causar problemas em outros casos. Inicializar classes às vezes pode ser muito caro ou mesmo nunca acontecer, essa é toda a filosofia da inicialização lazy.
class A { fun makeB() = "B" } val a by lazy { println("Making value a") A() } fun main() { val b by lazy(a::makeB) // Prints: Making value a }
Mas, devido ao problema descrito, a propriedade b seria criada imediatamente, mesmo que esse não seja o comportamento pretendido.
Conclusão
Para resumir tudo, tenha cuidado e evite usar referências de método em combinação com suas propriedades preguiçosas. Isso impedirá que seu aplicativo seja inicializado caro, bem como evitará travamentos relacionados a classes inicializadas pelo framework, como atividades, fragmentos, serviços do Android, etc.