Referências de método e lambdas em propriedades preguiçosas

Tempo de leitura: 3 minutes

É tudo sobre o bytecode

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.