- NoBeanDefFoundException
- Código de exemplo
- Detalhes da implementação
- Por que este problema ocorre?
- Detalhes sobre o crash
- Correção
- Referências
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.
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.
- A tela inicial é aberta;
- O botão da tela iniciar é tocado;
- Dois intents são disparados para uma mesma activity, que faz o uso de
loadKoinModules
eunloadKoinModules
; - A primeira activity é aberta;
- A segunda activity é aberta (ficando no topo da pilha de navegação);
- A segunda activity está funcionando perfeitamente, a cada toque no botão dela, é exibido um texto na tela;
- 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
; - 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 oloadKoinModules
não irá rodar novamente; - 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;
- Crash no app com
NoBeanDefFoundException
.
Curiosidades sobre este crash
- 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.
- 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.
- 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.
- 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 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!