Koin Context Isolation: setup correctly and avoid random NoBeanDefFoundException - android

I have an Android library module that I package and publish. It's a dependency in different apps, some that use Koin and some that don't, therefore I want to use Koin's context isolation.
Having followed the docs, I added a koin component and context as follows:
internal object LocalKoinContext {
lateinit var koinApplication: KoinApplication
}
// Custom KoinComponent using the local instance & not the Global context
interface CustomKoinComponent : KoinComponent {
// override the used Koin instance to use the local koin application instance (LocalKoinContext.koinApplication)
override fun getKoin(): Koin = LocalKoinContext.koinApplication.koin
}
and add create the koinApplication like this:
// Local Koin application instance
LocalKoinContext.koinApplication = koinApplication {
// use AndroidLogger as Koin Logger - default Level.INFO
androidLogger(Level.ERROR)
// use the Android context given there
androidContext(applicationContext)
// load properties from assets/koin.properties file
androidFileProperties()
// declare used modules
modules(theModule)
}
I have an activity that uses a view model that lives within the local Koin component and I used to instantiate the koinApplication within the onCreate of the that activity:
class FirstActivity : AppCompatActivity(),
CustomKoinComponent {
private val sharedViewModel by viewModel<MySharedViewModel>()
...
override fun onCreate(savedInstanceState: Bundle?) {
...
// Local Koin application instance
LocalKoinContext.koinApplication = koinApplication {
// use AndroidLogger as Koin Logger - default Level.INFO
androidLogger(Level.ERROR)
// use the Android context given there
androidContext(applicationContext)
// load properties from assets/koin.properties file
androidFileProperties()
// declare used modules
modules(theModule)
}
}
The issue is that I get random NoBeanDefFoundException
Caused by org.koin.core.error.NoBeanDefFoundException: No definition found for class:'my.package.name.MySharedViewModel'. Check your definitions!
at org.koin.core.scope.Scope.throwDefinitionNotFound(Scope.java:264)
at org.koin.core.scope.Scope.resolveInstance(Scope.java:233)
at org.koin.core.scope.Scope.get(Scope.java:204)
at org.koin.androidx.viewmodel.factory.DefaultViewModelFactory.create(DefaultViewModelFactory.java:11)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:187)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:150)
at org.koin.androidx.viewmodel.ViewModelResolverKt.get(ViewModelResolverKt.java:23)
at org.koin.androidx.viewmodel.ViewModelResolverKt.resolveInstance(ViewModelResolverKt.java:12)
at org.koin.androidx.viewmodel.scope.ScopeExtKt.getViewModel(ScopeExtKt.java:86)
at org.koin.androidx.viewmodel.scope.ScopeExtKt.getViewModel(ScopeExtKt.java:72)
at org.koin.androidx.viewmodel.ext.android.ViewModelStoreOwnerExtKt.getViewModel(ViewModelStoreOwnerExtKt.java:68)
at org.koin.androidx.viewmodel.ext.android.ViewModelStoreOwnerExtKt.getViewModel$default(ViewModelStoreOwnerExtKt.java:58)
at my.package.name.FirstActivity$special$$inlined$viewModel$default$1.invoke(FirstActivity.java:75)
I suspect it's because sometimes the koinApplication doesn't have time to set itself up but as it's rare I haven't been able to reproduce the issue.
In the answer to this stack question, the dev creates a ContentProvider to instantiate the koinApplication. I've tried it out and so far so good (although, I will only know in months from now), but is this truly the way to go? Seems to be an overkill to have to create a content provider just for that.
Can anyone shed some light on where we are supposed to instantiate the local KoinApplication?

Can anyone shed some light on where we are supposed to instantiate the local KoinApplication?
You should do this inside your Application level class, for example this is what I have on my Application class onCreate
startKoin {
androidLogger(Level.ERROR)
androidContext(this#VeroApp)
modules(AppModules.modules) <-- list of defined koin modules
}
I'm not so sure about the usage of local KoinApplication interface. I know that AppCompatActivity for example is out of the box supported and you can just do a without extending anything
private val sharedViewModel by viewModel<MySharedViewModel>()

Related

Integrate Koin in AAR

I have an AAR project, and i'm trying to integrate Koin (my first Koin project).
I'm following Koin instruction for context isolation (https://insert-koin.io/docs/reference/koin-core/context-isolation/). and i will be glad to get some help.
I need to register the koin context this way:
MyKoinContext.koinApp = KoinApp
I don't understand when should i call this line and what is the KoinApp (should be koinApplication()?)
The second part that i'm not sure is that i should configure KoinComponent:
abstract class CustomKoinComponent : KoinComponent {
// Override default Koin instance, initially target on GlobalContext to yours
override fun getKoin(): Koin = MyKoinContext.koinApp?.koin
}
but getKoin() should return Koin and not Koin? how sholud i changed it.

Inject into arbitrary Logic class that is not instanciated by Hilt

I'm currently migrating an app from anko-sqlite to room as anko has been discontinued a long time ago. In the process of doing so I introduced a repository layer and thought I would try to introduce dependency injection to get references to the repository instances in all my classes.
Prior to this I used a singleton instance that I just shoved into my application classes companion object and accessed that from everywhere.
I managed to inject my repositories into Fragments, Workmanager and Viewmodels. But I do now struggle a bit to understand how they forsaw we should handle this with arbitrary logic classes.
For instance my workmanager worker calls a class that instantiates a list of "jobs" and those need access to the repository. The worker itself actually bearly even does need access to the repositories as all the work is abstracted away from it.
How can I make something like this work
class Job(val someExtraArgINeed: Int) {
#Inject
lateinit var someRepository: SomeRepository // <= this should be injected when the job is instanciated with the constructor that already exists
}
For Activities and so forth we have special annotations #AndroidEntryPoint that makes this work. However, the documentation makes it unclear as to how we should get our instances from any other class that isn't an Android class. I understand that constructor injection is the recommended thing to use. But I do not think it would work here for me without an even more massive refactor required than this already would be.
If I understand your question correctly you can use hilt entry points like this
class CustomClass(applicationContext: Context) {
#EntryPoint
#InstallIn([/*hilt component that provide SomeRepository*/ApplicationComponent]::class)
interface SomeRepositoryEntryPoint {
fun provideSomeRepository(): SomeRepository
}
private val someRepository by lazy {
EntryPointAccessors.fromApplication(applicationContext, SomeRepositoryEntryPoint::class.java).provideSomeRepository()
}
fun doSomething() {
someRepository.doSomething()
}
}

How to start koin from another class instead of Application class

I am creating an app as library. The main app in which library has to be integrated has an Application class, so I can't add Application class in my library app. I have found that koin has to be started from Application class. Can I call startKoin from another class?
You can create your own KoinApplication which does not share the global koin context. You also don't need to initialize this inside an Application class. This instance can then be used in your own implementation of KoinComponent.
You can find a detailed description here:
https://doc.insert-koin.io/#/koin-core/start-koin?id=koin-context-isolation
If you purpose is to inject library dependencies to your app, you can have your library expose set of modules that can be referred from you app and be initialised while starting Koin.
For example:
In library, you have a public module as libraryModule:
val libraryModule = module{
single{
ObjectA()
}
}
Now, when you include your library in your app module, you would be able use it as:
class MyApplication : Application{
override fun onCreate() {
super.onCreate()
initializeKoinDI()
}
private fun initializeKoinDI() {
startKoin {
androidContext(this#MyApplication)
modules(listOf(appModule1, appModule2, libraryModule))
}
}
}

How to mock after setup dagger-android 2.15 when writing espresso tests?

If we just use plain dagger 2. In the application class, we will have a property which holds the AppComponent. Then we can swap it during espresso tests.
But when I setup my project using dagger-android 2.15. Things becomes more implicit if adopt too much Dagger magic. The code is more clean, but makes testing a little bit hard.
This is the application class:
class App : DaggerApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent
.builder()
.create(this)
.build()
}
}
This is the HomeActivity
class HomeActivity : DaggerAppCompatActivity() {
#Inject
lateinit var userPreference: UserPreference
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
if (!this.userPreference.memberRegistered) {
goToActivity(EntryActivity::class.java)
}
}
}
Take this code for example. How to mock that injected userPreference.memberRegistered Which could be a HTTP call underneath?
For those who is interested in this, I got a blog with step by step detail for this:
Basically, the idea is:
You still generate instances for injection in #Module
But We’ll create new #Component A only for testing
This #Component will have a method to get that #Module
During tests, we swap the #Component that the application use with our component A
Then things are easy:
Without DaggerMock
In the #Module, instead of return real instance, you just return mockito mock.
With DaggerMock
You declare the type you want to swap and mock it
You can then use the mock.
No need to change the #Module
It inspires by #AutonomousApps 's solution, but the differences are now you don't need to write the #Component, #Module for each test class.
After trying several approaches, this is the only one that worked for me.
I wrote a blog post that explains how to do this just yesterday: https://dev.to/autonomousapps/the-daggerandroid-missing-documentation-33kj
I don't intend to repeat the entire post for this answer (it's hundreds of words and lines of code to properly set up a test harness with Dagger), but to attempt to summarize:
Add a custom application class in the debug source set (I assume it would also work in the androidTest source set, but I have not tried this).
You also need to reference this application in a AndroidManifest.xml in the same source set.
Create a "Test component" in your androidTest class that extends from your production top-level component and build it.
Use that test component to inject your application, which means you've just replaced your entire Dagger dependency graph with a new one you've defined just for the test suite.
Profit.

How to inject test overrides into the default dependency graph?

I would like to inject mocked overrides into my Android instrumentation tests using Kodein. I don't know which is the optimal approach to do this. Here's what I have in mind:
My app uses a KodeinAware application class. The served Kodein instance holds all dependencies required by my app.
In my tests I would like to inject mocked overrides for specific dependencies to test behavior of the app in various situations.
Overrides should be different for each test, and should be injected before/while the test runs.
Is the configurable Kodein extension sensible in this situation, or is there a simpler, better suited approach (and if so, which)?
If your test is given a Kodein instance (meaning that it can use a different Kodein object than the one held by your Application), then the recommended approach is to create a new Kodein object that extends the one of the app and overrides all necessary bindings.
val testKodein = Kodein {
extend(appKodein())
bind<MyManager>(overrides = true) with singleton { mock<MyManager>() }
}
The configurable Kodein option is recommended only if you're using a static "one true Kodein". Using it prevents the possibility to run you're tests in parallel (because they all access the same Kodein instance), and forces you to clear the ConfigurableKodein between each tests and re-declare every time different overrides.
I am now using the ConfigurableKodein inside my custom App class.
class App : Application(), KodeinAware {
override val kodein = ConfigurableKodein()
override fun onCreate() {
super.onCreate()
// A function is used to create a Kodein module with all app deps.
kodein.addImport(appDependencies(this))
}
}
// Helper for accessing the App from any context.
fun Context.asApp() = this.applicationContext as App
Inside my AppTestRunner class, I declare the configuration to be mutable. That way I can reset it's configuration between each and every test.
class AppTestRunner : AndroidJUnitRunner() {
override fun callApplicationOnCreate(app: Application) {
app.asApp().kodein.mutable = true
super.callApplicationOnCreate(app)
}
}
I have created a JUnit rule that reset the dependency graph before every test.
class ResetKodeinRule : ExternalResource() {
override fun before() {
val app = InstrumentationRegistry.getInstrumentation().targetContext.asApp()
app.kodein.clear()
app.kodein.addImport(appDependencies(app))
}
}
In my tests I can now retrieve the App.kodein instance and inject mocks that override dependencies of the original graph. The only thing that needs to be guaranteed is that the tested activity is launched after configuring mocks, or behavior is not predictable.

Categories

Resources