Using Dagger with Espresso - android

I'm planning to create Espresso tests on my app multi-module, and I'm about to create the first Espresso test, but what I'm seeing is that on my app I do not have an AppComponent where I can fake it. Since I want to add the test on my feature-module, I'll create the TestApp, TestRunner there from now.
What I have on my feature-module is a FeatureComponent that is injected via ComponentFactory from the App, so what I thought is to create a class like this :
#Component (
dependencies = [ MoreComponents::class],
modules = [ DataSourceModule::class ]
)
interface FeatureOneComponent {
fun activityOneSubComponent(): FeatureOneActivity.Component.Factory
fun activityTwoSubComponent(): FeatureTwoActivity.Component.Factory
#Component.Factory
interface Factory {
fun create(
dependencies
):FeatureOneComponent
}
}
interface FeatureOneProvider {
fun getFeatureOneComponent(): FeatureOneComponent
}
///ACTIVITY
class FeatureOneActivity : AppCompatActivity() {
//this comes from Subcomponent is what I want to MOCK
#Inject lateinit var presenter
//these comes from the factory and I have it mocked
#Inject lateinit var manager
override fun onCreate(){
(applicationContext as FeatureOneProvider).getFeatureOneComponent().activityOneSubComponent().create(this).inject(this)
}
}
#Subcomponent(modules = [ActivityOneModule::class]) <--- THIS I WANT TO MOCK
interface Component {
fun inject(activity: FeatureOneActivity)
#SubComponent.Factory
interface Factory {
fun create(#BindsInstance activity: FeatureOneActivity): Component
}
}
#Module
interface ActivityOneModule {
#Binds
fun bindPresenter(impl: PresenterImpl): Contract.Presenter
}
TEST
class MyTestApp : Application(), FeatureOneProvider {
override fun getFeatureOneComponent(): FeatureOneComponent {
return DaggerMockFeatureOneComponent.create()
}
}
#Component(
modules = [MockFeatureOneModules::class]
)
interface MockFeatureOneComponent : FeatureOneComponent {
//I NEED TO MOCK THE SUBCOMPONENT WITH `MockSubcomponent`
}
#Component
object MockFeatureOneModules {
#Provides
fun providesManager() : MyManager = mock(MyManager)
}
//I want to use this module to replace the subcomponent of my activity
#Module
object MockSubcomponent() {
#Provides
fun providesFakePresenter() : FeatureOneContract.Presenter = mock { FeatureOneContract.Presenter::class.java }
}
To better understand the problem
When I run my test and I put a debugger point I see everything is mocked but the Presenter, and that's because the presenter is in
#Subcomponent(modules = [ActivityOneModule::class]
interface Component {
fun inject(activity: FeatureOneActivity)
#SubComponent.Factory
interface Factory {
fun create(#BindsInstance activity: FeatureOneActivity): Component
}
}
#Module
interface ActivityOneModule {
#Binds
fun bindPresenter(impl: PresenterImpl): Contract.Presenter
}
And in my test component I don't have access to "override" this subcomponent so everything is mocked but this subcomponent and I need this mocked.

I don't know if that's the best idea but if I did not misunderstand you you want this Presenter to return a mock {}. The changes you could do are :
In your TestComponent change interface to abstract class
Duplicate your subcomponent and extends from the real one
#Component(
modules = [MockFeatureOneModules::class]
)
abstract class MockFeatureOneComponent : FeatureOneComponent {
abstract fun subcomponent() : MockComponent.FactoryMock
override fun activityOneSubComponent(): FeatureOneActivity.Component.Factory {
return subcomponent()
}
#Subcomponent
interface MockComponent : FeatureOneActivity.Component {
#Subcomponent.Factory
interface FactoryMock : FeatureOneActivity.Component.Factory {
override fun create....
}
}
}
And that's it, it should work.

Your example code is pretty complex to understand and the actual problem.
But what I understand you want to setup expresso test for your feature module and you need to setup dagger component for it.
So, I can give you some guidelines and example code so that you can follow and setup your dagger architecture for your espresso test very simply.
First of all, you need setup/create your App for espresso test like this:
class MyTestApplication : MyApplication() {
//Call this from MyApplication onCreate()
override fun initDaggerGraph() {
component = DaggerTestAppComponent.builder()
.application(this)
.appLifecycle(appLifecycle)
.build()
component.inject(this)
}
}
Then create your Test app component like this:
//Add all of your dependent modules in this TestAppModule
#Component(modules = [TestAppModule::class])
interface TestAppComponent : AppComponent {
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): Builder
#BindsInstance
fun appLifecycle(appLifecycle: AppLifecycle): Builder
fun build(): TestAppComponent
}
fun inject(activityTest: SomeActivityTest) //Your activity where to inject
}
Also, make sure to initialize your component in your Test activity class when launching the activity like this:
val component = MyApplication.instance.component as TestAppComponent
component.inject(this)
Now you have done all the setup and your dependency should resolve as well as your espresso test should work.

So as far as i get it, you have multiple modules, components and subcomponents, but since most of their names don't quite match up in the code you posted, nor you posted the error log, i have to guess whats going wrong where.
Instead, how about something like this.
public interface SomeComponent {
WhateverClass1 provideWhatever1();
WhateverClass2 provideWhatever2();
WhateverRepository1 provideWhateverRepository1();
SomeOtherComponent getSomeOtherComponent();
// and so on and on
}
and then have your production component look something like this:
#SomeComponentSingleton
#Component(modules = { ... },
dependencies = { ... })
public interface SomeProductionScopedComponent extends SomeComponent {
#Component.Builder
interface Builder {
// you know best what needs to go here
SomeProductionScopedComponent build();
}
}
and this
#SomeComponentSingleton
#Component(dependencies = SomeComponent.class, modules =
{ ... })
public interface TestSomeComponent {
#Component.Builder
interface Builder {
Builder bindCoreComponent(SomeComponent coreComponent);
#BindsInstance
Builder bindContext(Context context);
TestSomeComponent build();
}
}
then, given that you're somewhat manually instantiating the components with these Builders, as long as they depend on the interface (SomeComponent), you should be able to bind a ProductionSomeComponent or a TestSomeComponent into your module.
Makes sense or gives some hint?

Related

How can I return a component from Application to module?

Trying to understand the following example and do something similar.
In my application there is a module, in which I want to use Dagger.
To do this I need an Application class in which I initialize and store AppComponent
Judging from the documentation I need to create an interface with a component from my module:
interface PasscodeSetupComponentProvider {
fun providePasscodeSetupComponent(): PasscodeComponent
}
Then I will implement this interface for my Application class:
open class FenturyApplication : PasscodeSetupComponentProvider {
lateinit var appComponent: AppComponent
override fun onCreate() {
super.onCreate()
appComponent = DaggerAppComponent.builder()
.appModule(AppModule(applicationContext))
.build()
}
override fun providePasscodeSetupComponent(): PasscodeComponent {
return appComponent.passcodeComponent
}
}
But judging by the example from the documentation I don't understand what should be in my interface Appcomponent namely passcodeComponent.
In the example, it looks like this:
class MyApplication: Application(), LoginComponentProvider {
// Reference to the application graph that is used across the whole app
val appComponent = DaggerApplicationComponent.create()
override fun provideLoginComponent(): LoginComponent {
return appComponent.loginComponent().create()
}
}
I added the following code to my AppComponent:
#Component(modules = [AppModule::class])
#Singleton
interface AppComponent {
val applicationContext: Context
fun passcodeComponent(): PasscodeComponent
}
And if I understood correctly, then in the fragment that is in my module, I can write the following:
lateinit var passcodeComponent: PasscodeComponent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val context = activity?.applicationContext ?: return
passcodeComponent = (context as PasscodeSetupComponentProvider).providePasscodeSetupComponent()
passcodeComponent.inject(this)
After that I hope I can use dagger in my module.
You're missing a crucial part of how AppComponent is written. You can read the complete code in this link.
Please note how Google is declaring the LoginComponent (as Subcomponent) and how do they declare it inside AppComponent in order do return a new instance of LoginComponent.
AppComponent (copied from the link I posted)
#Singleton
#Component(modules = [NetworkModule::class, SubcomponentsModule::class])
interface ApplicationComponent {
// This function exposes the LoginComponent Factory out of the graph so consumers
// can use it to obtain new instances of LoginComponent
fun loginComponent(): LoginComponent.Factory
}
And the code for LoginComponent
#Subcomponent
interface LoginComponent {
// Factory that is used to create instances of this subcomponent
#Subcomponent.Factory
interface Factory {
fun create(): LoginComponent
}
fun inject(loginActivity: LoginActivity)
}

(DAGGER-ANDROID) Can not use #Inject on an Espresso Test and can not use mockWebServer

I'm trying to create Espresso tests and using a mockWebServer the thing is when I try to create my mockWebServer it calls the real api call and I want to intercept it and mock the response.
My dagger organisation is :
My App
open class App : Application(), HasAndroidInjector {
lateinit var application: Application
#Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
override fun androidInjector(): AndroidInjector<Any> = androidInjector
override fun onCreate() {
super.onCreate()
DaggerAppComponent.factory()
.create(this)
.inject(this)
this.application = this
}
}
Then MyAppComponent
#Singleton
#Component(
modules = [
AndroidInjectionModule::class,
AppModule::class,
RetrofitModule::class,
RoomModule::class,
AppFeaturesModule::class
]
)
interface AppComponent : AndroidInjector<App> {
#Component.Factory
interface Factory {
fun create(#BindsInstance application: App): AppComponent
}
}
Then I've created this TestApp
class TestApp : App() {
override fun androidInjector(): AndroidInjector<Any> = androidInjector
override fun onCreate() {
DaggerTestAppComponent.factory()
.create(this)
.inject(this)
}
}
And this is my TestAppComponent
#Singleton
#Component(
modules = [
AndroidInjectionModule::class,
AppModule::class,
TestRetrofitModule::class,
AppFeaturesModule::class,
RoomModule::class]
)
interface TestAppComponent : AppComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance application: App): TestAppComponent
}
}
Note: Here I've created a new module, called TestRetrofitModule where the BASE_URL is "http://localhost:8080", I don't know if I need something else.
Also I've created the TestRunner
class TestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, TestApp::class.java.name, context)
}
}
And put it on the testInstrumentationRunner
Problem 1
I can not use
#Inject
lateinit var okHttpClient: OkHttpClient
because it says that it's not initialised.
Problem 2 (Solved thanks Skizo)
My mockWebServer is not dispatching the responses even-though is not pointing the real api call, is pointing the one that I've put to the TestRetrofitModule, the thing is that I have to link that mockWebServer and Retrofit.
The setup you posted looks correct. As for App not being provided, you probably need to bind it in your component, since right now you're binding TestApp only. So you need to replace
fun create(#BindsInstance application: TestApp): TestAppComponent
with
fun create(#BindsInstance application: App): TestAppComponent
I had the same problem with mockWebServer recently, what you need to do is to put a breakpoint and see what's the error, in my case I put it on my BaseRepository where I was doing the call, and found that the exception was :
java.net.UnknownServiceException: CLEARTEXT communication to localhost not permitted by network security policy
What I did to solve the problem is add this on my manifest.xml
android:usesCleartextTraffic="true"
But you may have to use other approaches you can take a look on android-8-cleartext-http-traffic-not-permitted.
When I try to do something similar, I don't create two types of application-components, just one. I provide them with different inputs, based on whether it's for the actual App or for the TestApp. No need for TestAppComponent at all. E.g.
open class App : Application(), HasAndroidInjector {
lateinit var application: Application
#Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
override fun androidInjector(): AndroidInjector<Any> = androidInjector
override fun onCreate() {
super.onCreate()
DaggerAppComponent.factory()
.create(this, createRetrofitModule())
.inject(this)
this.application = this
}
protected fun createRetrofitModule() = RetrofitModule(BuildConfig.BASE_URL)
}
class TestApp : App() {
override fun createRetrofitModule() = RetrofitModule("http://localhost:8080")
}
#Module
class RetrofitModule(private val baseUrl: String) {
...
provide your Retrofit and OkHttpClients here and use the 'baseUrl'.
...
}
(not sure if this 'compiles' or not; I usually use the builder() pattern on a dagger-component, not the factory() pattern, but you get the idea).
The pattern here is to provide your app-component or its modules with inputs for the 'edge-of-the-world', stuff that needs to be configured differently based on the context in which the app would run (contexts such as build-flavors, app running on consumer device vs running in instrumentation mode, etc). Examples are BuildConfig values (such as base-urls for networking), interface-implementations to real or fake hardware, interfaces to 3rd party libs, etc.
How about a dagger module for your Test Class with a ContributeAndroidInjector in there and do Inject on a #Before method.
Your TestAppComponent:
#Component(modules = [AndroidInjectionModule::class, TestAppModule::class])
interface TestAppComponent {
fun inject(app: TestApp)
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: TestApp): Builder
fun build(): TestAppComponent
}
}
TestAppModule like:
#Module
interface TestAppModule {
#ContributesAndroidInjector(modules = [Provider::class])
fun activity(): MainActivity
#Module
object Provider {
#Provides
#JvmStatic
fun provideString(): String = "This is test."
}
// Your other dependencies here
}
And #Before method of Test Class you must be do:
#Before
fun setUp() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
val app = instrumentation.targetContext.applicationContext as TestApp
DaggerTestAppComponent.builder().application(app).build().inject(app)
// Some things other
}
An important thing, you must be have (on build.gradle module app):
kaptAndroidTest "com.google.dagger:dagger-compiler:$version_dagger"
kaptAndroidTest "com.google.dagger:dagger-android-processor:$version"
Now, when you launch an Activity like MainActivity, Dagger will inject dependencies from your TestAppModule instead of AppModule before.
Moreover, If you want to #Inject to Test Class, you can add:
fun inject(testClass: TestClass) // On Your TestAppComponent
And then, you can call:
DaggerTestAppComponent.builder().application(app).build().inject(this) // This is on your TestClass
to Inject some dependencies to your TestClass.
Hope this can help you!!
I am presuming that you are trying to inject OkHttpClient:
#Inject
lateinit var okHttpClient: OkHttpClient
in your TestApp class, and it fails. In order to make it work, you will need to add an inject method in your TestAppComponent, to inject the overriden TestApp, so that it becomes:
#Singleton
#Component(
modules = [
AndroidInjectionModule::class,
AppModule::class,
TestRetrofitModule::class,
AppFeaturesModule::class,
RoomModule::class]
)
interface TestAppComponent : AppComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance application: App): TestAppComponent
}
fun inject(testApp: TestApp)
}
The reason why this is required, is because Dagger is type based, and uses the type of each class to be injected/provided to determine how to generate the code at compile-time. In your case, when you try to inject the TestApp, dagger will inject its superclass (the App class), because it only know that it has to inject the App class. If you have a look at the AndroidInjector interface (that you use in your AppComponent), you will see that it is declared like:
public interface AndroidInjector<T> {
void inject(T instance)
....
}
This means that it will generate a method:
fun inject(app App)
in the AppComponent. And this is why #Inject works in your App class, but does not work in your TestApp class, unless you explicitly provided it in the TestAppComponent.

Override Dagger2 Component / Module created with #Component.Factory

I'm trying to find the correct way to override a Component or Module within a Espresso test while using latest Dagger2 #Component.Factory features.
I have the following component definition:
#Component(
modules = [SomeModule::class],
dependencies = [SessionComponent::class]
)
interface MainComponent {
#Component.Factory
interface Factory {
fun create(sessionComponent: SessionComponent): MainComponent
}
}
and SomeModule definition it's:
#Module
object SomeModule {
#Provides
#JvmStatic
fun some(): Some = SomeImpl()
}
Some it's an interface and SomeImpl will be it's implementation.
Now, to inject in our activity, we just do:
DaggerMainComponent.factory().create(sessionComponent).inject(this)
We resolve the sessionComponent with and extension function declared in our CustomApp: Application
val Context.sessionComponent: SessionComponent
get() = App.sessionComponent(this)
Now as per recommendations in the official guide: https://dagger.dev/testing we should try to replace MainComponent with a TestMainComponent which will have a TestSomeModule with our test implementation.
My initial thoughts were in this direction:
Remove the injection from the activity and replaced it by an Injector object that will do it. Also, modify the create() method and pass the Module:
DaggerMainComponent.factory().create(sessionComponent).inject(this)
It's moved to:
object Injector {
#VisibleForTesting
var someModule: SomeModule = SomeModule()
fun inject(activity: Activity) {
when (activity) {
is MainActivity -> {
DaggerMainComponent.factory().create(App.sessionComponent(activity), someModule).inject(activity)
}
}
}
}
And the new Factory definition:
#Component.Factory
interface Factory {
fun create(sessionComponent: SessionComponent, someModule: SomeModule): MainComponent
}
Then, after the onCreate() in the activity we just invoke Injector.inject(this)
The idea behind this was that in a UI Test we could do:
#Before
fun setup() {
Injector.someModule = TestSomeModule
}
and change the implementation that way. It fails thought since SomeModule it's a singleton object and we cannot extend from it.
This lead me to the following approach:
The app has release and debug buildTypes. Within the release and debug folder I have the real implementation of the Injector (with MainComponent referencing SomeModule).
Then in the androidTest folder I have another Injector object but with the following:
object Injector {
fun inject(activity: Activity) {
when (activity) {
is MainActivity -> {
DaggerTestMainComponent.factory().create(App.sessionComponent(activity)).inject(activity)
}
}
}
}
and TestMainComponent it's
#Component(
modules = [TestSomeModule::class],
dependencies = [SessionComponent::class]
)
interface TestMainComponent {
#Component.Factory
interface Factory {
fun create(sessionComponent: SessionComponent): TestMainComponent
}
}
#Module
object TestSomeModule {
#Provides
#JvmStatic
fun greeter(): Some = TestSomeImpl()
}
This approach works and gradle replaces the Injector object from the debug folder with the one in the androidTest folder, however, I'm wondering if there's a better approach to avoid having to use two Injector objects for this purpose, or maybe just update the Module used with the test one.
Any advice it's welcome!

How to inject Mocked Presenter of Activity in Instrumentation testing with Espresso

I have been trying this for a week. And I have crawled every article available but their implementations or examples fall short or stop at the steps of Espresso Tests.
My Android Application follows MVP architecture (And is in Java)
Scenario: [Giving just one example]
I have a HomeActivity which gets a HomePresenter using Dagger2. (Provides method in the HomeModule exposed through a void inject(HomeActivity activity) in the HomeComponent.
In my espressoTest for HomeActivity I would like to inject a mockpresent.
I Have not exposed this dependencies inside an AppModule through an AppComponent. which most examples on the net do (So they just create a new testApplication and then do the needfull)
I do not want to use the productFlavours way of injecting or providing mockclasses as it doesnt give me control over the Mockito.when methods.
So basically. I would like to inject a mockpresenter wherein i can do whatever Mockito.when()s on it for the sake of my unit tests in espresso.
My Code is below.
HomeComponent
#HomeScope
#Component(modules = HomeModule.class,dependencies = AppComponent.class)
public interface HomeComponent {
void inject(HomeActivity activity);
}
HomeModule
#Module
public class HomeModule {
private final IHomeContract.View view;
public HomeModule(IHomeContract.View view) {
this.view = view;
}
#Provides
#HomeScope
public IHomeContract.Presenter presenter(FlowsRepository flowsRepository, UserRepository userRepository, LoanRepository loanRepository) {
return new HomePresenter(view, flowsRepository, userRepository, loanRepository);
}
}
AppComponent
#Component(modules = {AppModule.class,RepositoryModule.class})
#AppScope
public interface AppComponent {
void inject(App app);
FlowsRepository flowRepository();
LoanRepository loanRepository();
UserRepository userRepository();
}
AppModule
#Module
public class AppModule {
private Context appContext;
public AppModule(#NonNull Context context) {
this.appContext = context;
}
#Provides
#AppScope
public Context context() {
return appContext;
}
}
App
component = DaggerAppComponent.builder()
.appModule(new AppModule(this))
.build();
component.inject(this);
HomeActivity
HomeComponent component = DaggerHomeComponent.builder()
.appComponent(((App) getApplication()).getComponent())
.homeModule(new HomeModule(this))
.build();
Once again. In my tests (espresso) i would like to inject a mockedHomePresenter the set by Mockito. So I can just unit test my views.
The key point in resolving the problem is to have such a Dagger Module that provides a mock Presenter in HomeActivity's instrumented test instead of the "real" one.
For this the following 2 extra actions need to be done (you might also want to see an example).
Delegate instantiation of HomeActivity's Component to some abstraction.
Substitute the implementation of the abstraction in instrumented tests to provide mocks.
I'll use Kotlin in the example below.
Define the delegate interface:
interface HomeComponentBuilder {
fun build(view: IHomeContract.View): HomeComponent
}
Move the HomeComponent initialisation from HomeActivity to the delegate implementation:
class HomeComponentBuilderImpl constructor(private val app: App) : HomeComponentBuilder {
override fun build(view: IHomeContract.View): HomeComponent =
DaggerHomeComponent.builder()
.homeModule(HomeModule(view))
.build()
}
Make the delegate be in application "scope" so that you could interchange its implementation for instrumented tests:
interface App {
val homeComponentBuilder: HomeComponentBuilder
...
}
App implementation should now contain
class AppImpl : Application(), App {
override val homeComponentBuilder: HomeComponentBuilder by lazy {
HomeComponentBuilderImpl(this#AppImpl)
}
...
}
Component initialisation in HomeActivity looks as follows:
(application as App)
.homeComponentBuilder
.build(this)
.inject(this)
For instrumented testing create TestHomeComponent that extends HomeComponent:
#HomeScope
#Component(modules = [TestHomeModule::class])
interface TestHomeComponent : HomeComponent
where TestHomeModule provides a mock Presenter
#Module
class TestHomeModule {
#Provides
fun providePresenter(): IHomeContract.Presenter = mock()
}
What's left to do is to make a test delegate implementation
class TestHomeComponentBuilderImpl : HomeComponentBuilder {
override fun build(view: IHomeContract.View): HomeComponent =
DaggerTestHomeComponent.builder()
.testTestHomeModule(TestHomeModule())
.build()
}
and initialise it in TestAppImpl
class TestAppImpl : Application(), App {
override val homeComponentBuilder: HomeComponentBuilder by lazy {
TestHomeComponentBuilderImpl()
}
...
}
The rest is standard. Create a custom AndroidJUnitRunner that uses TestAppImpl:
class TestAppRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application = Instrumentation.newApplication(TestAppImpl::class.java, context)
}
and add it to app module build.gradle
defaultConfig {
testInstrumentationRunner "your.package.TestAppRunner"
...
}
Usage example:
#RunWith(AndroidJUnit4::class)
class HomeActivityTest {
private lateinit var mockPresenter: IHomeContract.Presenter
#get:Rule
val activityRule = ActivityTestRule(HomeActivity::class.java)
#Before
fun setUp() {
mockPresenter = activityRule.activity.presenter
}
#Test
fun activity_onCreate_presenter_should_onViewCreated() {
verify(mockPresenter).someMethod()
}
}
So. Your problem is that you need to create a Module that provides a mock presenter for testing instead of the "real" one.
There is quite a good article on this here: Testing with Dagger

dagger2, how can i call #BindsInstance denoted method of SubComponent..?

please help me! I have a trouble in use of dagger 2.
I want to bind some dependency in runtime not in compile time inside MainActivity by using #Subcomponent.Builder and #BindsInstance
I have an ApplicationComponent and it has a Builder and its #BindsInstance looks working fine. I can use like below
DaggerApplicationComponent
.builder()
.application(this)
.build()
.inject(this)
but some trouble came from MainActivity...
below are snippets of codes
[ApplicationComponent]
#Singleton
#Component(modules = [ApplicationModule::class])
internal interface ApplicationComponent : AndroidInjector<MyApplication> {
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): Builder
fun build(): ApplicationComponent
}
}
[ApplicationModule]
#Module(
includes = [
AndroidInjectionModule::class,
AndroidSupportInjectionModule::class,
ActivityInjectionModule::class
],
subcomponents = [
MainComponent::class
]
)
internal abstract class ApplicationModule {
#PerActivity
#ContributesAndroidInjector(modules = [SplashModule::class])
abstract fun splashActivity(): SplashActivity
#Binds
#IntoMap
#ActivityKey(MainActivity::class)
abstract fun mainActivity(builder: MainComponent.Builder): AndroidInjector.Factory<out Activity>
}
[MainComponent]
#PerActivity
#Subcomponent(modules = [MainModule::class])
internal interface MainComponent : AndroidInjector<MainActivity> {
#Subcomponent.Builder
abstract class Builder : AndroidInjector.Builder<MainActivity>() {
#BindsInstance
abstract fun testClass(mainTestClass: MainTestClass): Builder
}
}
[MainActivity]
internal class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// This works find without runtime injection
// AndroidInjection.inject(this)
/**
*I want to bind some dependency(in this case, MainTestClass) in runtime like below.
* so that I can use MainTestClass inside MainModule to inject this to other classes.
* but, for some reason,
* DaggerMainComponent IS NOT GENERATED AUTOMATICALLY...
*/
DaggerMainComponent.builder()
.testClass(MainTestClass())
.build()
.inject(this);
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
startActivity(Intent(this, SplashActivity::class.java))
}
}
The problem is that I cannot access DaggerMainComponent because the Dagger doesn't generate it automatically.
I am searching for lots of websites to solve this but failed.
Is there any way to make it?
I have found a way to achieve what you want, I think. Apologies for not using your particular example, but it was easier to paste in the code I have and know that works from my own IDE. I've added comments on the critical lines. Here's the code:
Singleton component
#Singleton
#Component(modules = [
AndroidSupportInjectionModule::class,
RuntimeBindingModule::class // my addition!
])
interface MainApplicationComponent {
fun inject(app: MainApplication)
// my addition!
fun runtimeBuilder(): RuntimeBindingActivitySubcomponent.Builder
#Component.Builder
interface Builder {
fun build(): MainApplicationComponent
#BindsInstance fun app(app: Context): Builder
}
}
This binding code is essentially identical to yours.
#Subcomponent
interface RuntimeBindingSubcomponent : AndroidInjector<RuntimeBindingActivity> {
#Subcomponent.Builder
abstract class Builder : AndroidInjector.Builder<RuntimeBindingActivity>() {
#BindsInstance abstract fun bindInt(intVal: Int): Builder
}
}
#Module(subcomponents = [RuntimeBindingSubcomponent::class])
abstract class RuntimeBindingActivityModule {
#Binds #IntoMap #ActivityKey(RuntimeBindingActivity::class)
abstract fun bindInjectorFactory(
builder: RuntimeBindingActivitySubcomponent.Builder
): AndroidInjector.Factory<out Activity>
}
MainApplication
open class MainApplication : Application(), HasActivityInjector {
// This needs to be accessible to your Activities
lateinit var component: MainApplication.MainApplicationComponent
override fun onCreate() {
super.onCreate()
initDagger()
}
private fun initDagger() {
component = DaggerMainApplicationComponent.builder()
.app(this)
.build()
component.inject(this)
}
}
RuntimeBindingActivity
class RuntimeBindingActivity : AppCompatActivity() {
// I had to use #set:Inject because this is a primitive and we can't use lateinit
// on primitives. But for your case,
// `#Inject lateinit var mainTestClass: MainTestClass` would be fine
#set:Inject var intVal: Int = -1
override fun onCreate(savedInstanceState: Bundle?) {
// And this is how you can get runtime binding
val subComponent = (application as MainApplication).component.runtimeBuilder()
with(subComponent) {
seedInstance(this#RuntimeBindingActivity)
bindInt(10) // runtime binding
build()
}.inject(this)
Log.d("RuntimeBindingActivity", "intVal = $intVal")
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_runtime_binding)
}
}
It is critically important to note that the subcomponent which you generate in this way doesn't get magically stored somewhere by dagger. If you want your late-bound instance to be available for injecting into other classes controlled by your #PerActivity scope, you need to manually manage the lifecycle of this subcomponent. Store it somewhere (maybe in your custom Application class), and then you also must set its reference to null when your activity is destroyed, or you'll be leaking that activity.

Categories

Resources