Dependency injection in Android Library - android

I'm working on Android library that other apps will use it.
This library will not have any activity, but it will have Fragments, VM, domain etc.
So far on my apps i worked with Dagger2, and i'm not sure how it will work in library.
Anyone have experience with it? or maybe someone can recommend other library to use for that case (koin?)?
Thanks

Koin is far more easy to use. You can also get rid of annotations and their handling. Suppose we have a class name Helper and needs to be access from different locations.
Implementation Steps:
a) Add Dependency in build.gradle(app).
implementation "io.insert-koin:koin-android:3.3.0"
b) Create a class extend it with KoinComponent
class DIComponent : KoinComponent {
// Utils
val helper by inject<Helper>()
}
c) Initialize Koin by passing it modules in App class
class MainApplication : Application(){
override fun onCreate() {
super.onCreate()
startKoin{
androidLogger()
androidContext(this#MainApplication)
modules(appModule)
}
}
private val appModule = module {
single { Helper() }
}
}
d) Now, to use this class in project (activity/fragment)
class MainActivity : AppCompatActivity() {
private val diComponent = DIComponent()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diComponent.helper.yourFunction()
}
}

Related

Koin. Cannot inject to fragment with ScopeActivity

I'm trying to inject some dependency to both activity and fragment using Koin and I expect it to live as long as activity lives, but it turned out a headache for me.
I managed to create a module that resolves MainRouter, inject it into an activity, but it doesn't work for a fragment.
val appModule = module {
scope<MainActivity> {
scoped { MainRouter() }
}
}
MainActivity extends ScopeActivity, MyFragment extends ScopeFragment.
in MainActivity private val router : MainRouter by inject() works fine, but in MyFragment it throws org.koin.core.error.NoBeanDefFoundException: No definition found for class:'com.example.app.MainRouter'. Check your definitions!
Finally I managed to inject, but it doesn't look pretty
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val scopeId = scopeActivity!!.getScopeId()
scope.linkTo(getKoin().getScope(scopeId))
mainRouter = get()
...
I also don't like that scopeActivity can't be accessed in the init method. Does this mean that activity scoped dependencies cannot be resolved in fragment using by inject()?
As I can see in your code, you have to declare a Fragment instance, just declare it as a fragment in your Koin module and use constructor injection. Like below:
val appModule = module {
single { MyService() }
fragment { MyFragment(get()) }
}
Please refer link for more details.

Dagger2 + ActivityInjection + AndroidXTest/Espresso/RoboElectric in library project

I am working on android library module and I want to test the standalone activity in my module. I was following the article https://medium.com/androiddevelopers/write-once-run-everywhere-tests-on-android-88adb2ba20c5 to use roboelectric and androidx test with espresso. I recently introduced dagger 2 to my library project.
With that my Activity looks like this:
class XYZLibBaseActivity : AppCompatActivity(){
#Inject
lateinit var resourceProvider: ResourceProvider
override fun onCreate(savedInstanceState: Bundle?) {
//creating the dagger component
DaggerXYZLibComponent.factory().create(application).inject(this)
super.onCreate(savedInstanceState)
}
}
My component declaration is
#Component(modules = [ResourceProviderModule::class])
interface XYZLibComponent{
#Component.Factory
interface Factory{
fun create(#BindsInstance application: Application):XYZLibComponent
}
fun inject(xyzLibBaseActivity: XYZLibBaseActivity)
}
and dagger module is
#Module
class ResourceProviderModule {
#Provides
fun provideResourceProvider(application: Application): ResourceProvider{
return ResourceProviderImpl(application.applicationContext)
}
}
This works perfectly fine and I don't want the underlying application to use dagger 2.
Now I wan to test my activity without depending on the underlying application or application class. How can I inject mock ResourceProvider in the activity?
One of many options is
create 2 flavors in your gradle config: real and mock
in both flavors, define a boolean buildConfigField flag
In your provideResourceProvider, return a corresponding implementation based on the flag's value

How to use Koin in multiple module?

There are two modules in my android project, app module and lib module.
Both these two modules need Koin for D.I., so I call startKoin in MyApplication class in app module, and IninKointContentProvider in lib module as below.
// app module
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin(this, modules1)
}
}
// lib module
class InitKoinContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
startKoin(context.applicationContext, modules2)
return true
}
}
Then app crashed and shown this message
Caused by: org.koin.error.BeanOverrideException: Try to override definition with Single [class='android.content.Context'], but override is not allowed. Use 'override' option in your definition or module.
I guess startKoin can be called only one time.
The solution I found is merging two koin modules then calling startKoin in MyApplication, but I don't like it. Lib module may be imported by other android project which doesn't use koin, in that case, I think calling startKoin in InitKoinContentProvider is better.
Any solution for this problem?? Thanks!
I found the best solution inspired by #laalto's answer, thanks!
Upgrade to koin 2.0, then use KoinApplication and customized KoinComponent to create a isolated koin context, it can let lib module using koin without any initializing call by app module, still start koin in ContentProvider. The whole code may like below.
// app module
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this#MyApplication)
modules(module{
viewModel { MainViewModel() }
})
}
}
}
class MainActivity: AppCompactActivity() {
private val viewModel: MainViewModel by viewModel()
}
// lib module
internal object MyKoinContext {
lateinit var koinApplication: KoinApplication
}
interface MyKoinComponent : KoinComponent {
override fun getKoin(): Koin {
return MyKoinContext.koinApplication.koin
}
}
class InitKoinContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
MyKoinContext.koinApplication = koinApplication {
androidContext(context.applicationContext)
modules(module{
viewModel { FooViewModel() }
})
}
return true
}
}
class FooActivity: AppCompactActivity(), MyKoinComponent {
private val viewModel: FooViewModel by viewModel()
}
Ref:
https://insert-koin.io/docs/2.0/documentation/reference/index.html#_koin_context_isolation
In your library modules, use loadKoinModules() to load the module-specific koin modules. Docs.
You need to have run startKoin() prior to that, so the init order with content providers can be a little tricky.
To init extra koin modules on other project modules and get no duplicate loading issues (e.g. pressing home, than coming back to the activity), go to your module declaration file:
val myModule = module {
single { MyRepository(get()) }
viewModel { MyViewModel(get()) }
}
private val loadKoinModules by lazy {
loadKoinModules(myModule)
}
fun inject() = loadKoinModules
Then on your view:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
inject()
}
TL;DR
Use single/factory methods with override param set to true when providing your dependencies that are overriding those provided by the modules loaded before.
single<Manager>(override = true) { TestManager() }
I have faced a similar issue when I tried to override one of the dependencies for UI test purposes.
When I setup in Application.onCreate():
startKoin {
module {
single { Printer() }
}
}
and then in before method of test:
loadKoinModules(module {
single<Printer> { TestPrinter() }
})
I get a Runtime exception during the test:
org.koin.core.error.DefinitionOverrideException: Already existing definition or try to override an existing one
And the solution is to show Koin that you are intentionally overriding that dependency by using override param of single function like that:
loadKoinModules(module {
single<Printer>(override = true) { TestPrinter() }
})
By design startKoin is meant to be called from the Application class. You can provide a parameter in the lib whether to call startKoin or not. But I doubt that including such things as Koin in libs is a good practice. What if an application already includes Koin, but of different version?
This way worked perfect for me:
#KoinExperimentalAPI
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
initKoin()
}
private fun initKoin() {
startKoin {
androidLogger()
androidContext(this#MainApplication)
}
loadKoinModules(
myFeatureModule
)
}
}
And define you module in your feature as usual:
val myFeatureModule = module {
factory<...> { ...() }
single { ...() }
viewModel { ...(get() }
}

Koin how to inject outside of Android activity / appcompatactivity

Koin is a new, lightweight library for DI and can be used in Android as well as in standalone kotlin apps.
Usually you inject dependencies like this:
class SplashScreenActivity : Activity() {
val sampleClass : SampleClass by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
with the inject() method.
But what about injecting stuff in places where Activity context is not available i.e. outside of an Activity?
There is the KoinComponent which comes to the rescue. In any class you can simply:
class SampleClass : KoinComponent {
val a : A? by inject()
val b : B? by inject()
}
Extending KoinComponent gives you access to inject() method.
Remember that usually it's enough to inject stuff the usual way:
class SampleClass(val a : A?, val b: B?)
Koin provides a solution for this using the KoinComponent interface. For example, if you need to get some dependencies in your repository then you can simply implement the KoinComponent interface. It gives you access to various Koin features such as get() and inject(). Use KoinComponent only when you can't rewrite the constructor to accept dependencies as constructor parameters.
class MyRepository: Repository(), KoinComponent {
private val myService by inject<MyService>()
}
Constructor injection is better than this approach.
For example, the same thing can be achieved by:
class MyRepository(private val service: MyService): Repository() {
...
}
And you can add the definition for instantiating this class in a koin module:
val serviceModule = module {
...
factory { MyService() }
}
val repositoryModule = module {
...
factory { MyRepository(get<MyService>()) }
}
If you don't want to implement any interfaces then just take a look at how KoinComponent.inject() is implemented and do something similar yourself:
val foo by lazy { KoinPlatformTools.defaultContext().get().get<FooClass>() }

How to initialize/inject generic ViewModel in BaseActivity by Koin injection on Android/Kotlin App

I'm building the architecture of a new Android application using Kotlin and Android Architecture Components (ViewModel, LiveData) and I'm also using Koin as my dependency injection provider.
The problem is that I'm not been able to initialize the ViewModel in a generic way inside my BaseActivity via koin injection. The current code looks like this:
abstract class BaseActivity<ViewModelType : ViewModel> : AppCompatActivity() {
// This does not compile because of the generic type
private val viewModel by lazy {
// Koin implementation to inject ViewModel
getViewModel<ViewModelType>()
}
#CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Fabric.with(this, Crashlytics())
}
/**
* Method needed for Calligraphy library configuration
*/
#CallSuper
override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase))
}
}
I'd like to know if is there a way to do this in Kotlin because I'm pretty sure I would be able to do in Java easily.
Thanks.
The solution was provided by the koin team in version 0.9.0-alpha-11 and the final code looks like this:
open class BaseActivity<out ViewModelType : BaseViewModel>(clazz: KClass<ViewModelType>) :
AppCompatActivity() {
val viewModel: ViewModelType by viewModel(clazz)
fun snackbar(message: String?) {
message?.let { longSnackbar(find(android.R.id.content), it) }
}
fun toast(message: String?) {
message?.let { longToast(message) }
}
}
Here is example of not passing Class and Generic to base implementation
In your base fragment/activity:
abstract class BaseFragment<T : BaseViewModel> : Fragment() {
...
#Suppress("UNCHECKED_CAST")
private val clazz: KClass<T> = ((this.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<T>).kotlin
protected val viewModel: T by viewModel(clazz = clazz)
...
}
It looks ugly, but it works.
you can use a delegate version declaration for your ViewModel and avoid using directly a lazy expression. Try with this:
abstract class BaseActivity<T : ViewModel> : AppCompatActivity() {
val model by viewModel<T>()
}
This will give you a lazy of
getViewModel<T>()
Throw an eye on the quick ref: https://insert-koin.io/docs/1.0/getting-started/android-viewmodel/
Hope it will help.

Categories

Resources