Compartilhe:

Publicado em 26 de April de 2023

Quebrando o app com Koin — Parte 1

Quebra-cabeça

Foto de Pierre Bamin via Unsplash

NoBeanDefFoundException

Vivências com o koin-android 3.3.1

Este crash pode ocorrer de N formas distintas, mas neste artigo iremos apresentar como ele pode ocorrer através do uso incorreto de carga e descarga de um — ou mais — módulo(s) Koin.

No caso abordado, veremos como este crash pode “surgir” durante o fluxo de navegação de um app Android, ao passar pelo mesmo trecho de código duas vezes.

Sem delongas, aqui está o stacktrace do crash.

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: br.com.velantasistemas.koinscopedactivity, PID: 16162
    java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:558)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
     Caused by: java.lang.reflect.InvocationTargetException
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003) 
     Caused by: org.koin.core.error.NoBeanDefFoundException: No definition found for class:'br.com.velantasistemas.koinscopedactivity.MVP$Presenter' q:''. Check your definitions!
        at org.koin.core.scope.Scope.throwDefinitionNotFound(Scope.kt:298)
        at org.koin.core.scope.Scope.resolveValue(Scope.kt:268)
        at org.koin.core.scope.Scope.resolveInstance(Scope.kt:231)
        at org.koin.core.scope.Scope.get(Scope.kt:210)
        at br.com.velantasistemas.koinscopedactivity.TrickyActivity.loadContent(TrickyActivity.kt:46)
        at br.com.velantasistemas.koinscopedactivity.TrickyActivity.onCreate$lambda-0(TrickyActivity.kt:21)
        at br.com.velantasistemas.koinscopedactivity.TrickyActivity.$r8$lambda$EpzWUtygs7IJBCcvIjAqBtuU9XM(Unknown Source:0)
        at br.com.velantasistemas.koinscopedactivity.TrickyActivity$$ExternalSyntheticLambda0.onClick(Unknown Source:2)
        at android.view.View.performClick(View.java:7455)
        at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1219)
        at android.view.View.performClickInternal(View.java:7432)
        at android.view.View.access$3700(View.java:835)
        at android.view.View$PerformClick.run(View.java:28810)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7842)
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
crash_exception hosted with ❤ by GitHub

Comenta se você já tomou um desses em produção. :)


Código de exemplo

Se quiser pular diretamente para o código, vá para o repositório https://github.com/pedrofsn/android-koin-playground e entre na branch koin_crash_part1-problem.

Antes de demonstrar o código é importante entendermos o que este app de exemplo faz. Com direito a uma GIF animada e um texto explicativo.

Demonstração do crash no app de exemplo

Ao abrir o app nos depararemos com a primeira tela — a MainActivity, que nada mais tem do que um botão para abrir a nossa feature. Quando este botão é tocado, ocorre o disparo de 2 intents através de um startActivity, cada intent carregará um valor numérico (um id fictício, respectivamente 1 e 2). Estes intents apontarão para uma mesma activity de destino, a TrickActivity.

Detalhes da implementação

Vamos ao detalhamento completo do código-fonte do nosso app. Começando pela tela inicial, tão simples quanto mencionada no início.

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.button.setOnClickListener {
            val intent: (Int) -> Intent = { id ->
                Intent(this, TrickyActivity::class.java).apply {
                    putExtra("id", id)
                }
            }
            startActivity(intent(1))
            startActivity(intent(2))
        }
    }
}

Já adentrando a nossa feature, abaixo temos a interface que ditará o funcionamento da camada de View e Presenter do nosso app.

interface MVP {

    interface View {
        fun loadContent()
        fun showContent(message: String)
    }

    interface Presenter {
        fun initialize()
    }
}

A seguir temos a implementação do nosso contrato da View definida acima, especificamente a nossa Activity.

class TrickyActivity : ScopeActivity(), MVP.View {

    private lateinit var binding: ActivityTrickyBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        // Carregamento do módulo Koin de maneira manual
        loadKoinModules(Modules.instance)
        super.onCreate(savedInstanceState)

        // Inicialização do data binding para acesso aos componentes de UI
        binding = ActivityTrickyBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Click do botão que invoca a função 'loadContent()'
        binding.button.setOnClickListener { loadContent() }
    }

    override fun loadContent() {
        // Acesso ao bean definido no módulo Koin
        val presenter = scope.get<MVP.Presenter> {
            val id = intent.getIntExtra("id", -1)
            parametersOf(this@TrickyActivity, id)
        }

        // Utilização do bean obtido através de DI com o Koin
        presenter.initialize()
    }

    // Exibindo o dado recebido que foi enviado pelo Presenter
    override fun showContent(message: String) {
        binding.textView.text = message
    }

    override fun onDestroy() {
        super.onDestroy()
        // Descarregando o módulo Koin
        unloadKoinModules(Modules.instance)
    }
}

Abaixo temos a implementação do Presenter que recebe a referência da View no seu construtor — algo comum em uma arquitetura MVP — e um id via DI q̶u̶e̶ ̶s̶ó̶ ̶s̶e̶r̶v̶e̶ ̶p̶a̶r̶a̶ ̶e̶n̶f̶e̶i̶t̶a̶r̶ ̶o̶ ̶n̶o̶s̶s̶o̶ ̶e̶x̶e̶m̶p̶l̶o.

class PresenterImpl(
    private val view: MVP.View?,
    private val id: Int
) : MVP.Presenter {

    override fun initialize() {
        val message = "TrickyActivity#${id}\n${Calendar.getInstance()}"
        view?.showContent(message)
    }

}

Aqui temos a definição do nosso módulo Koin, lembrando que o mesmo está sendo carregado e descarregado manualmente dentro da camada de View, através das extension functions loadKoinModules e unloadKoinModules.

object Modules {
    val instance = module {
        /**
         * Com o scope estamos atrelando as depedências declaradas neste 
         * módulo ao escopo da nossa activity - carinhosamente chamada de 
         * TrickActivity.
         */
        scope<TrickyActivity> {
            /**
             * Com o scoped o nosso presenter se comportará de maneira similar
             * a um singleton, no entanto será destruído após o término do 
             * ciclo de vida da activity na qual foi atrelado.
            */
            scoped<MVP.Presenter> { (view: MVP.View, id: Int) -> 
                PresenterImpl(view, id) 
            }
        }
    }
}

Por fim, mas não menos importante, temos a nossa Application class que estará viva durante todo o ciclo de vida da aplicação. Nesta, inicializamos a biblioteca do Koin através da extension function startKoin, já com as clássicas depedências androidLogger() e androidContext() — mas que pouco vem ao caso neste exemplo.

class Application : Application() {

    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidLogger()
            androidContext(androidContext = this@Application)
        }
    }
}

Pronto, passamos por todo o código de exemplo.


Por que este problema ocorre?

Por favor, reveja a GIF animada do início do artigo novamente, continuaremos a explicação da ocorrência deste crash a partir dela.

  1. A tela inicial é aberta;
  2. O botão da tela iniciar é tocado;
  3. Dois intents são disparados para uma mesma activity, que faz o uso de loadKoinModules e unloadKoinModules;
  4. A primeira activity é aberta;
  5. A segunda activity é aberta (ficando no topo da pilha de navegação);
  6. A segunda activity está funcionando perfeitamente, a cada toque no botão dela, é exibido um texto na tela;
  7. A segunda activity é fechada através do botão voltar, portanto sai da pilha de navegação. Ao ser destruída, é realizado o descarregamento do módulo Koin através da extension function unloadKoinModules;
  8. A primeira activity agora está no topo da pilha de navegação. Ressalto que o onCreate() dela não será executado novamente tendo em vista que já passamos por esta etapa do ciclo de vida da activity, portanto o loadKoinModules não irá rodar novamente;
  9. O botão é pressionado, e aqui acontece uma tentativa de acessar o Presenter na árvore de depedências do Koin, porém ele não será encontrado pois na etapa 7 pois foi removido da árvore;
  10. Crash no app com NoBeanDefFoundException.

Curiosidades sobre este crash

  1. Este é um erro causado pela biblioteca do Koin?

Não exatamente. A exception claramente é da biblioteca do Koin, agora o que causou o disparo da mesma foi, de fato, o seu uso incorreto.

  1. Este é um cenário real em uma aplicação Android?

Com toda certeza, principalmente se a gestão dos módulos for realizada de maneira manual.

  1. Este é um cenário comum em uma aplicação Android?

Em grandes projetos, quando se trabalha em features modularizadas, isto pode ocorrer facilmente em componentes android (como activities, fragments e afins) que precisam gerir seus módulos de dependências do Koin — de maneira granular — e sem o uso da Application class (que está em um módulo diferente, geralmente o :app). Outro cenário completamente possível é no uso SDKs com o Koin.

  1. Este erro poderia ser pego em tempo de compilação?

Da forma como foi apresentado, infelizmente não, pois este é um crash que ocorre em runtime (ou seja, em tempo de execução).


Se liga aí, que é hora da CORREÇÃO!

Então… depende! Assim como este crash pode ocorrer de N formas, também podemos resolver de N formas.

Papagaio promovido a Sênior

Papagaio que que aprendeu a dizer “depende"e foi promovido a sênior. | Foto por Luis Desiro em Unsplash

Abaixo explicarei as três abordagens para conseguirmos contornar este crash. Cada uma implementada no mesmo projeto de exemplo, em branches distintas.

Abordagem 1

A primeira, eu diria que é “tiro e queda”, que seria a remoção do código que faz o carregamento e descarregamento do módulo Koin de dentro do componente android. E traz como contra a necessidade do acesso a application class para registrar o módulo Koin, dentro do startKoin.

Veja a implementação na branch koin_crash_part1-solution-with_application_class_management.

Abordagem 2

Esta abordagem é bastante sagaz e não requer o acesso a application class. Nesta, apenas manipulamos a forma de inicialização do nosso presenter, do exemplo, para fazer o uso do late init var do Kotlin. Garantindo que o mesmo seja sempre inicializado via DI assim que a activity passar pelo fluxo do onCreate().

Veja a implementação nesta branch koin_crash_part1-solution-changing_approach_to_intialize_variable.

Abordagem 3

Por fim, mas não não menos importante, temos a terceira abordagem que nada mais é do que uma variação da segunda abordagem a fim de se usar um val com by lazy ao invés de var.

Mas para que funcione corretamente, será necessário que haja alguma interação com o presenter durante o onCreate(), para que seja disparado a inicialização lenta do Kotlin. No caso do exemplo, como não temos nenhum método a ser utilizado no onCreate(), adicionei apenas um presenter.toString() — por favor, não me julguem por isto.

Veja a implementação a nesta branch koin_crash_part1-solution-with_val.

That’s all folks!

Referências

Tags:Android

Publicado por: PagBank Engineering