1

I'm following an MVC pattern for my Android application and have ran into this issue a few times and have had to work around it. When my application is able to create an injected object using the @Inject annotation on a field, that objects @Inject fields are null, usually causing a crash. For instance, I have controller classes that will handle logic and flow. Any Fragments/Activities will callback to their controller to notify of a user interaction / state change. However, the injected instance of Controller is usually null.

I'll give a simple example to illustrate. Below, the Controller is having an injected activity created, then using that to start flow by adding a Fragment. That dependency is handled but the Activities dependency on the controller is not (i.e. null).

Simple Controller class to handle business logic and flow:

public class SomeController {
    @Inject
    SomeActivity someActivity;

    private SomeComponent component;

    private final Application app;

    @Inject
    public SomeController(Application app) {
        this.app = app;
    }

    private void startActivity() {
        component = Dagger_SomeComponent().builder()
             .someModule(app)
             .build();

        someActivity.getFragmentManager().beginTransaction().
            .add(R.id.content, SomeFragment.class, null)
            .commit();
    }

    public void activityStarted() {
        //callback when Activity is ready...
    }
}

The simple activity that handles user interaction and calls back to the controller to perform some business logic:

public class SomeActivity extends Activity {

    @Inject
    SomeController controller;

    private void controllerCallback() {
          //notify controller of something here...
    }
}

Simple Module class for injecting objects into graph:

@Module
public class SomeModule {

    private Application app;

    public SomeModule(Application app) {
         this.app = app;
    }

    @Provides
    @Singleton
    SomeController provideSomeController( return new SomeController(app); )

    @Provides
    SomeActivity provideSomeActivity( return new SomeActivity();)
}

Simple Component class for providing methods to consume objects:

@Component
public interface SomeComponent {

    void addController(SomeController controller);

    SomeController controller();

    SomeActivity activity();
}
fakataha
  • 785
  • 6
  • 31
  • Do you initialise Component? and where do you inject this object? Dagger-2 didn't use reflection mechanism – Konrad Krakowiak Mar 19 '15 at 18:52
  • right, should have added that. edited that in. was trying to trim it down to the essentials so it would be easier to discuss what/why/how I keep hitting this issue. Assume that the Application calls startActivity() on the controller when it wants to start that feature. – fakataha Mar 19 '15 at 20:51
  • forgot to add Activity to module, as well. that's what I get from doing this from memory. So, my understanding is that the Module will provide any injectable fields for SomeActivity, which appears to happen in Controller, but then why is the Activity's injectable field for Controller == null? – fakataha Mar 19 '15 at 21:00
  • What is some Activity is the object which extends Activity – Konrad Krakowiak Mar 19 '15 at 21:37
  • I mean SomeActivity class – Konrad Krakowiak Mar 19 '15 at 21:46
  • yes, in this example, SomeActivity would extend Activity. This is intended as a pseudocode example. – fakataha Mar 19 '15 at 23:01

3 Answers3

1

I believe a few things are missed in your example:

1) Dagger2 Component is a bridge between some modules that provides dependencies and some injection points that "consumes" dependencies. Your Component should have at least one linked module and annotation should looks like:

@Component(modules = SomeModule.class)

2) SomeActivity instantiation in the module with 'new' operator has no sense. Ok, you can create the Activity object, but who will manage it's state, call it's lifecycle methods etc? If you really want to pass a reference of existing Activity as dependency - it's possible, but in a different way. For example, create application level module(s) & component, and a separate activity level module & component, passing existing Activity reference as module constructor argument.

3) Activities are created by Android framework, therefore you should use field injection in activities. Add to your Component a line like:

void inject(SomeActivity activity);

And place the code that creates a Component into Activity. For example to onCreate():

SomeComponent component = Dagger_SomeComponent().builder()
         .someModule(getApplication())
         .build()
         .inject(this);

4) Please try to avoid @Singleton annotations at the beginning. By first make sure that all dependencies you're declared are satisfied and you haven't nulls anymore. Next you can check some existing Dagger2 open source projects to make right scope annotations.

Vlad Kuts
  • 503
  • 5
  • 11
  • 1. Yes, you are correct, I left that out by mistake. I didn't intend for this to be a concrete implementation, more like pseudocode to illustrate the question. Which was, why would an injected objects @Inject fields be null? 2. Agreed, in practice I wouldn't be using injection for the Activities. 3. That breaks the MVC model to give the component creation to Activity instead of the Controller, imo. 4. Not sure what you are aiming for in this statement? Singletons are useful patterns. – fakataha Mar 20 '15 at 22:12
  • 3. It's your choice where you will create the object graph (Component). But Activity is the base block, you cannot create a screen, a dialog etc without Activity. Thus, you can create the object graph in controller, but you also must directly create the controller instance in activity with new operator. So IMO it's better go one step back from MVC purity, create component in activity and use this component to inject dependencies into activity. 4. You may got some troubles with scopes, so I advise to solve issue with nulls by the 1st step, then please use Singleton annotation where you want. – Vlad Kuts Mar 23 '15 at 21:56
  • In one app, I'm using a default 'splash screen' activity to launch the app, since Android requires this as an entry point. The Application class can replace this Activity and control flow by starting Controllers for feature modules. The Controllers create activities using Intents and those call back to the Controller. I find this fairly nice and clean. Problem I have is when I have a class A with an @ Inject constructor -> Controller creates an instance of class B that has dependency on A-> B object's @ Inject field for A is null. I'm sure I'm missing something. :/ – fakataha Mar 23 '15 at 22:51
  • 1
    OK, if I clearly understand your last comment, only annotate a dependency with @ Inject A a in the class B is not enough. If you (your controller) explicitly instantiates B class, you need 1) choose who will inject dependencies - controller or B class constructor; 2) have a built object graph (component) in controller or in B constructor; 3) make an injection with declared in the component method like void inject(B b); – Vlad Kuts Mar 24 '15 at 19:05
  • Yep, think we got it. If you put that in an answer, I'll mark it. Thanks for the help! – fakataha Mar 24 '15 at 19:46
1

I ran into this problem, and resolved it using this accepted answer.

TLDR: I had to add inject() methods in the Component for exact types, not just inject(SomeSuperclass foo)

Basically, I had an Activity that needed injecting:

class SubActivity extends BaseActivity {
    @Inject Type someVar;

    @Override
    protected void onCreate(Bundle bundle) {
       getComponent().inject(this);
    }
}

But my component looked like:

@Component
public interface MyComponent {
    void inject(BaseActivity activity); // This allows SubActivity to compile but messes up injection
}

Solution: Add inject methods for the exact subtype:

@Component
public interface MyComponent {
    void inject(BaseActivity activity);
    void inject(SubActivity activity); //<-- This solved it
}
Community
  • 1
  • 1
bytehala
  • 665
  • 1
  • 10
  • 25
0

In my case, depends-on="bean1" was within property-placeholder in application-context was causing the issue. I used @PostConstruct to make injection work.

Smart Coder
  • 1,435
  • 19
  • 19