Dagger 2 on Android @Singleton annotated class not being injected

12,737

Solution 1

You're making a mistake in that you are using

DaggerBootstrap.create().initialize(this);

in your Activity, as scopes are not shared across multiple component instances. What I recommend is using a custom application class

public class CustomApplication extends Application {
    @Override
    public void onCreate() {
         super.onCreate();
         Bootstrap.INSTANCE.setup();
    }
}

@Component
@Singleton
public interface _Bootstrap {
    void initialize(ActivityA activityA);
    //void initiali...
}

public enum Bootstrap {
    INSTANCE;

    private _Bootstrap bootstrap;

    void setup() {
        bootstrap = Dagger_Bootstrap.create();
    }

    public _Bootstrap getBootstrap() {
        return bootstrap;
    }
}

Then you could call it as

Bootstrap.INSTANCE.getBootstrap().initialize(this);

This way, you share the component across your classes. I personally named Bootstrap as injector, and _Bootstrap as ApplicationComponent, so it looks like this:

Injector.INSTANCE.getApplicationComponent().inject(this);

But that's just my typical setup. Names don't really matter.

EDIT: To your last question, you can solve this by subscoping and component dependencies.

Your library project should be able to see only the library classes, correct? In that case, all you do is

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface LibraryScope {
}

@Component(modules={LibraryModule.class})
@LibraryScope
public interface LibraryComponent {
    LibraryClass libraryClass(); //provision method for `MyManager`
}

@Module
public class LibraryModule {
    @LibraryScope
    @Provides
    public LibraryClass libraryClass() { //in your example, LibraryClass is `MyManager`
        return new LibraryClass(); //this is instantiation of `MyManager`
    }
}

public enum LibraryBootstrap {
    INSTANCE;

    private LibraryComponent libraryComponent;

    static {
        INSTANCE.libraryComponent = DaggerLibraryComponent.create();
    }

    public LibraryComponent getLibraryComponent() {
        return libraryComponent;
    }
}

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ApplicationScope {
}

@Component(dependencies={LibraryComponent.class}, modules={AdditionalAppModule.class})
@ApplicationScope
public interface ApplicationComponent extends LibraryComponent {
    AdditionalAppClass additionalAppClass();

    void inject(InjectableAppClass1 injectableAppClass1);
    void inject(InjectableAppClass2 injectableAppClass2);
    void inject(InjectableAppClass3 injectableAppClass3);
}

@Module
public class AdditionalAppModule {
    @ApplicationScope
    @Provides
    public AdditionalAppClass additionalAppClass() { //something your app shares as a dependency, and not the library
        return new AdditionalAppClass();
    }
}

public enum ApplicationBootstrap {
    INSTANCE;

    private ApplicationComponent applicationComponent;

    void setup() {
        this.applicationComponent = DaggerApplicationComponent.builder()
                                        .libraryComponent(LibraryBootstrap.INSTANCE.getLibraryComponent())
                                        .build();
    }

    public ApplicationComponent getApplicationComponent() {
        return applicationComponent;
    }
}

Then

@Inject
LibraryClass libraryClass; //MyManager myManager;

...
    ApplicationBootstrap.INSTANCE.getApplicationComponent().inject(this);

Solution 2

It's hard to say what your problem was, since you didn't show what your Component looks like and whether you have multiple components etc.

Assuming this logical structure:

/app
   MainComponent
   SomeClass    // where MyManager is to be injected
   MainActivity // where SomeClass is to be injected
/library
   LibraryComponent
   MyManager    // Singleton

Then your classes as listed would inject correctly with the following configuration:

@Singleton
@Component
public interface LibraryComponent {
    MyManager getMyManager();
}

and the app-level component to inject dependencies into the activity:

@ActivityScope
@Component(dependencies = LibraryComponent.class)
public interface MainComponent {
    void inject(MainActivity mainActivity);
}

Note that MainComponent depends on LibraryComponent, but because the latter has singleton scope, you need to define a scope for the other one too, which I was I used the "activity scope" here. (Or you could also just make the MainComponent a singleton and get rid of the LibraryComponent completely if that suits your needs.)

Finally it's all injected into the activity like this:

@Inject
SomeClass someClass;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    DaggerMainComponent.builder()
            .libraryComponent(DaggerLibraryComponent.create())
            .build()
            .inject(this);

    someClass.doSomething();
}

I've put a working sample here on GitHub

Update 1:

If I understand your setup correctly, you've so far only used the @Singleton and @Inject annotations on the two classes listed (MyManager and SomeClass), and there is no other Dagger-related code in your project.

In that case, the reason your MyManager isn't getting injected, is because Dagger doesn't know how to provide/instantiate the dependencies. This is where the "components" come in that I mentioned above. Without any Dagger 2 components (interface or abstract class annotated with @Component), your dependencies won't get injected automatically.

I don't know if you have experience with Dependency Injection concepts, but assuming you don't, I'll step through the minimum basics you'll need to understand to get your MyManager injected into SomeClass:

First: when you use DI, you need to understand the difference between "newables" and "injectables". This blogpost by Misko Hevery explains the details.

This means, you can't new up your SomeClass. This won't work:

mSomeClass = new SomeClass();

Because if you did that (say in an activity or fragment), Dagger will have no idea that you expected a dependency to get injected into SomeClass and it has no opportunity to inject anything.

In order for its dependencies to get injected, you have to instantiate (or inject) SomeClass itself through Dagger too.

In other words, say in your Activity where SomeClass is used, you'll need:

@Inject
SomeClass mSomeClass;

Next, you need a Dagger component to perform the actual injection. To create a component, you create an interface with a method that takes your root object (say MainActivity) as argument, e.g.:

@Singleton
@Component
public interface Bootstrap {
    void initialize(MainActivity activity);
}

Now when you build your project, Dagger 2 generates a class called DaggerBootstrap that implements this interface. You use this generated class to perform the injection, say in your activity's onCreate:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    DaggerBootstrap.create().initialize(this);

    mSomeClass.doSomething();
}

I believe this generated component is the key part you're missing. Full code for above here.

Some useful Dagger 2 resources:

Update 2:

Seems like the final missing piece of the puzzle was that your component provided an inject method for the Activity base class, but not for your actual concrete activity.

Unfortunately Dagger 2 requires an inject method for each activity or other class you want to inject into.

As you mentioned, this will be annoying when you have many different activities in your app. There some possible workarounds for this, search for "dagger 2 inject base class", for example this suggestion by @EpicPandaForce: Dagger 2 base class injections

Also note, as pointed out by @EpicPandaForce in the comments, that in my simplistic example I called DaggerLibraryComponent.create() every time which is probably not what you want, since that component is supposed to provide your singletons, so you're probably better off getting the existing instance from somewhere else such as from your Application instance.

Share:
12,737

Related videos on Youtube

AgentKnopf
Author by

AgentKnopf

Updated on June 04, 2022

Comments

  • AgentKnopf
    AgentKnopf about 2 years

    I am currently trying to integrate Dagger 2 into an Android application. My project setup is as follows:

    • library
    • app (depends on library)

    In my library project I defined a class that I'll later inject into other classes that need it (Activities and regular classes) in the library as well as the app project.

    @Singleton
    public class MyManager{
      @Inject
      public MyManager(){
        //Do some initializing
      }
    }
    

    Now - for instance in my Fragments or Activities or regular classes I'd inject the above Singleton as follows:

    public class SomeClass{
    
      @Inject
      MyManager myManager;
    }
    

    Or so I thought, because in practice myManager is always null. And apparently it's constructor is never called either, so I guess I must be missing something configuration-wise? Or maybe I misunderstood the documentation and it's not meant to work this way at all? The purpose of MyManager class is to be an application-wide accessible component-accumulating entity - that's why I went for the @Singleton.

    UPDATE

    To avoid confusion: I mentioned my having components somewhere in a comment I think - this refers to components in the sense of "component based design" and has nothing to do with dagger. The dagger-based code I have is all listed above - there is nothing else related to dagger in my code.

    When I started adding @Component I had some compiler issues, because my dagger2 was not setup properly - check out this really helpful thread on how to setup dagger2 correctly: https://stackoverflow.com/a/29943394/1041533

    UPDATE 2

    Here is my updated code, based on G. Lombard's suggestions - I changed the code as follows - the original Singleton is in the library project:

    @Singleton
    public class MyManager{
      @Inject
      public MyManager(){
        //Do some initializing
      }
    }
    

    Also in the library project is the bootstrap class:

    @Singleton
    @Component
    public interface Bootstrap {
        void initialize(Activity activity);
    }
    

    Then I use the above Bootstrap class in my activity (in my concrete app, NOT in the library project! I do however also have Classes/Activities in the library that'll access Bootstrap to inject MyManager):

    public class MyActivity extends Activity{
    
        @Inject
        MyManager manager;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            //DONT DO THIS !!! AS EXPLAINED BY EpicPandaForce
            DaggerBootstrap.create().initialize(this);
        }
    }
    

    But even after this line:

            DaggerBootstrap.create().initialize(this);
    

    the manager instance is still null, i.e. not injected.

    I just found this: https://stackoverflow.com/a/29326023/1041533

    Which if I don't misread, implies I need to specify every single class in the Bootstrap class that will use @Inject to have stuff injected. Sadly - this is not an option, as I have more than 40 classes and activities for which I'd have to do that.

    Meaning my Bootstrap interface apparently would have to look something like this:

    @Singleton
    @Component
    public interface Bootstrap {
        void initialize(ActivityA activity);
        void initialize(ActivityB activity);
        void initialize(ActivityC activity);
        void initialize(ActivityD activity);
        void initialize(ActivityE activity);
        void initialize(ActivityF activity);
        //and so on and so forth...
    }
    

    If the above is true, that would not be worth it for my use case. Plus: Seems there is no compile-time check, if I forgot to specify one of my 40+ classes here? It just wont work - i.e. crash the app at runtime.

    • EpicPandaForce
      EpicPandaForce almost 9 years
      Your assumption on how "there is no compile time check" is incorrect unless you specify both superclass and concrete class for the same class (that is when compile check starts failing and you need to be wary of nulls). This is not dagger1.
    • EpicPandaForce
      EpicPandaForce almost 9 years
      Your Bootstrap class is an actual dagger component and it is annotated as such. The name of the method does not matter, it can be called either inject or initializeor whatever as long as its return type is void and the parameter is whatever you want to inject.
    • AgentKnopf
      AgentKnopf almost 9 years
      @EpicPandaForce Thanks a lot for clearing that one up! Your comments were very helpful in understanding how dagger2 works!
    • EpicPandaForce
      EpicPandaForce almost 9 years
      Glad I could help! :) There's only one more problem going on with your setup, and that's that you call DaggerBootstrap.create().initialize(this); but that means you create a new component in your activity (and inject your activity with it) - which means that the components are not shared across your activities, and your singleton class is not actually a singleton! You should create the component only once in your Application subclass, and make it accessible to other classes. What I do is place it in an enum singleton and that singleton stores the component so you can inject anywhere
    • EpicPandaForce
      EpicPandaForce
      Well, they need to call the component's inject method manually on creation. If they actually remember to call inject, then it'll whine about how it needs to be added to the component. I feel like saying that "they forget to inject" is like saying "I marked the view fields with annotations but my view is null" because they forget to call ButterKnife.bind(this); after setContentView(R.layout.something);... They'll figure out that it's kinda required to make it work. My workmate was completely new to dagger (and dependency injection), and he picked it up really quickly.
    • EpicPandaForce
      EpicPandaForce
      Ah. Well you need to call the field injection manually. The compile time check is that if you try to inject a class with the component, then it needs to have a void inject(YourClass yourClass); method defined or otherwise it won't work. Back in the days of Dagger1, you had to provide this in an injects={YourClass.class} kind of array and it actually did throw a runtime exception if you messed up! It was pretty bad. We have about 53 classes defined in our ApplicationComponent for field injection and the provided classes (non-lifecycle) can just call inject in their constructors. Voila!
  • AgentKnopf
    AgentKnopf almost 9 years
    Thank you very much for taking the time to answer this! I'll check it out asap, but I just wanted to let you know that I dont have Components in the Dagger-sense but in the "Component Design" sense - so when I say "components" - then those have nothing to do with dagger ;) - all my dagger related code was in my OP (i.e. nothing missing, it's all there, and that's all I have when it comes to dagger)
  • AgentKnopf
    AgentKnopf almost 9 years
    One thing I noticed - you said "(Or you could also just make the MainComponent a singleton and get rid of the LibraryComponent completely" - but is that not exactly what I did in my OP? MyManager is annotated as an @Singleton - or do you see a mistake in my OP configuration? That MyManager is used in my library project and in my actual app - so I'd have to inject it into classes and activities residing in both projects (library and app) - which does not seem to work they way I set it up in my OP
  • G. Lombard
    G. Lombard almost 9 years
    Thanks for the clarification @Zainodis! So you don't have an interface with the @Component interface in your project? That must be the problem then... I'll update my answer to expand further, hopefully it makes sense! :)
  • AgentKnopf
    AgentKnopf almost 9 years
    Exactly - no @ Component interface :) - I felt i was missing something, though for me it somehow wasn't that obvious from the dagger2 documentation that an @ Singleton needs @ Component to be used somewhere as well - looking forward to your update and thank you very much for taking the time to help :) !
  • G. Lombard
    G. Lombard almost 9 years
    You're welcome, I'm learning this stuff myself, so I'm glad to contribute to the current lack of resources on DI in Android! I hope the update helps!
  • AgentKnopf
    AgentKnopf almost 9 years
    I thank you in advance for that detailed answer - I should have no problem trying that out :) ! It will take a few days though as I had switched to another solution in between and now need to get back to the dagger-compatible version - I'll let you know how it worked out asap !
  • AgentKnopf
    AgentKnopf almost 9 years
    Thanks again for the in-depth explanation! I think I am almost there now :) - one more problem: After building everything seemed ok and compiled but upon deploy I got the gradle build error: cannot find symbol class DaggerBootstrap - which is odd because it is imported and if I click on DaggerBootstrap it shows me the generated class. Could that be an android studio issue? Or maybe a caching issue?
  • G. Lombard
    G. Lombard almost 9 years
    That's weird. Where is the generated file located? For me it's: ./app/build/generated/source/apt/debug/com/codeblast/simples‌​tdagger2example/Dagg‌​erBootstrap.java Do you deploy in Android Studio? If so, what happens if you deploy from command-line with ./gradlew installDebug ?
  • AgentKnopf
    AgentKnopf almost 9 years
    Hello there and I apologize for the late reply. I tried your second suggestion. MyManager class resides in my library, while I inject and use it (or try to ; ) ) in my concrete project/app. Even the compile issue I had resolved itself (I have no idea what went wrong there) - but the overall result is still the same: MyManager is not getting injected. Despite adding that Bootstrap interface and all the rest. Last thing I can think of: I am injecting into a subclass of Activity (lets call it MyActivity) - but in the Bootstrap class I just write: void initialize(Activity a). Is that a problem?
  • AgentKnopf
    AgentKnopf almost 9 years
    P.s. (not enough space for longer comments) because if that is a problem and I need to have an initialize Method for every single class that uses dagger for injecting stuff in my Bootstrap interface, then that would make dagger horribly useless xD - but I really can't imagine that being the case? Having tried all your suggestions I am out of ideas though what else could cause the issue of stuff not getting injected.
  • AgentKnopf
    AgentKnopf almost 9 years
    Updated my code - seems that dagger2 really is not able to inject properly unless I specify the exact class/activity that uses @inject - which means I'd have to register ~40 classes - which is not an option :/
  • G. Lombard
    G. Lombard almost 9 years
    You're right: you need to specify each injection method... I've seen a workaround for that somewhere, if I can find it I'll send a link.
  • AgentKnopf
    AgentKnopf almost 9 years
    That'd be great! Also - if you add the tid-bit about having to specify each and every concrete class that will use @Inject - then I'll mark your answer as accepted. It might not be what I was hoping for, but that's just the way things are and you put a lot of effort into helping me out :)
  • EpicPandaForce
    EpicPandaForce almost 9 years
    .libraryComponent(DaggerLibraryComponent.create()) this will create a new instance of the component each time, and the scope will not be retained across components.
  • AgentKnopf
    AgentKnopf almost 9 years
    Thank you very much for clarifying this vital part about (the issue of) re-creating the Bootstrapper every time!
  • EpicPandaForce
    EpicPandaForce almost 9 years
    No prob - also, an interesting fact, it's supposedly possible to do base class injections, but it requires reflection. I haven't tested it, I just saw it here: reddit.com/r/androiddev/comments/3f0aso/… (although I'm guessing you still have to specify every concrete class in the component, and it just calls the method with the current class parameter)
  • AgentKnopf
    AgentKnopf almost 9 years
    Btw one more additional question: The bootstrap class would be in my library, but that means I cant register concrete classes from my app project (because they don't exist within the scope of the library) - I do however also have to access the MyManager Singleton from classes and activities in the library itself. So the question would be: Can I extend Bootstrap in the app project and add all concrete classes from the app there while having a base bootstrap in the library as well?
  • AgentKnopf
    AgentKnopf almost 9 years
    Addendum: All classes/activities in the library and app must access the same instance of MyManager (I store configuration data in it)
  • AgentKnopf
    AgentKnopf almost 9 years
    Thank you very much for the last part! I will try it out in the next 2-3 days and if it works I will mark your answer as the accepted one, as it'll provide a full, working solution for my concrete problem, which will be helpful for people facing similar issues. Thank you again for helping out!
  • AgentKnopf
    AgentKnopf almost 9 years
    Hey there! I was just implementing your addendum when I started wondering if maybe I expressed myself poorly. Namely I am not sure why that new LibraryClass(); is called? The only class that dagger should create for me is MyManager - but you introduced a new Library Module. I am not sure how that LIbrary class would automatically be the same instance as MyManager? Thanks in advance - maybe I am just not understanding this properly, so if you're sure this works as described in my OP then I'll trust that statement and simply continue with implementing it.
  • EpicPandaForce
    EpicPandaForce almost 9 years
    LibraryClass would be your MyManager, yes. I added three comments in an edit to make that clearer.
  • EpicPandaForce
    EpicPandaForce almost 9 years
    You need to replace the constructor injection with module providers because you're subscoping with component dependencies, and that requires modules and provision methods for your component. All a module does is create a new instance of the things you want to inject.
  • AgentKnopf
    AgentKnopf almost 9 years
    Thank you very much for clarifying :) ! I'll get on it tomorrow and will report back here.
  • AgentKnopf
    AgentKnopf almost 9 years
    Almost done xD - I did stumble over the AdditionalAppModule though - I couldnt find a definition of it in your code - is it basically the same as LibraryModule? I.e. instantiating the MyManager?
  • EpicPandaForce
    EpicPandaForce almost 9 years
    No, that's the stuff you want to use in your Application, but aren't part of the library.
  • AgentKnopf
    AgentKnopf almost 9 years
    Ok - i am done - but when trying it out, MyManager is still null - I created a gist with the relevant code and comments. MyManager in this case is now called ConfigurationManager - despite injecting everything and stuff I still get an NPE, implying that ConfigurationManager is still null and was not injected properly: gist.github.com/AgentKnopf/59288bd78b314830dffe#file-login - any ideas? P.s. I basically copied most of your classes and left them named as you named them - except for ConfigurationManager (which is the original MyManager class)
  • EpicPandaForce
    EpicPandaForce almost 9 years
    Please show your LibraryComponent and your LibraryModule, I think the issue is that the @Inject on constructor won't work for subscoped component dependencies and you have to use the module with the component provision method (so you need a LibraryComponent with ConfigurationManager configurationManager(); and a LibraryModule that returns a new instance of configuration manager)
  • AgentKnopf
    AgentKnopf almost 9 years
    Hey there and thank you for the quick reply ! Appended both components you asked for at the bottom of the gist: gist.github.com/AgentKnopf/59288bd78b314830dffe#file-login
  • EpicPandaForce
    EpicPandaForce almost 9 years
    In that case, once you remove the @Inject from the constructor and the @Singleton annotation in your ConfigurationManager then it should work.
  • AgentKnopf
    AgentKnopf almost 9 years
    While trying the last thing you mentioned I realized I wasn't calling the setup() method in ApplicationBootstrap anywhere... So I changed it to a static initializer like in LibraryBootstrap and it is now working :) !! Thanks a bunch for being so patient and guiding me through this! I left the ConfigurationManager/MyManager annotated as [at]Singleton with the [at]Inject on the constructor - as it does work (since the problem was elsewhere) - thanks again ! P.s. I now marked your answer as the correct one.