0

I am trying to follow this template for an MVP/Dagger2/RxJava project.

I cannot get an injection of the Activity context into my presenter, every other injection passes through as I know that a subcomponent has open access to all parent provided logic.

The application component is built in the Application class and then accessed in the base presenter activity to then inject the relevant dependencies of the presenter and the rest. The config persistent component is primarily utilised for saving presenter state.

What defeats the purpose of DI is if I just manually pass the context from the activity to the presenter.

I have tried to add scoping to all components and modules to make sure the dependencies can be properly accessed from the graph, however this has not worked.

I am trying to use constructor injection of the context, I actually receive the context in the activity that the presenter communicates with but the presenter does not, an error is thrown. So I am wondering why the activity has access to the activity context but the presenter doesn't.

Any guidance would be appreciated.

Error

Error:(13, 8) error: [<packageName>.injection.component.ActivityComponent.inject(<packageName>.login.LoginActivity)] android.app.Activity cannot be provided without an @Inject constructor or from an @Provides-annotated method.
android.app.Activity is injected at
<packageName>.login.presenter.LoginActivityPresenter.<init>(activity, …)
<packageName>.login.presenter.LoginActivityPresenter is injected at
<packageName>.login.LoginActivity.presenter
<packageName>.login.LoginActivity is injected at
<packageName>.injection.component.ActivityComponent.inject(loginActivity)
A binding with matching key exists in component: <packageName>.injection.component.ActivityComponent

My components/modules are shown below:

Application Component

@Singleton
@Component(modules = {ApplicationModule.class, BusModule.class, PrefsModule.class, NetModule.class})
public interface ApplicationComponent {
    @ApplicationContext Context context();
    Application application();
    EventBus bus();
    SharedPreferences prefs();
    Gson gson();
}

Application Module

@Module
public class ApplicationModule {
    protected final Application app;

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

    @Provides
    Application providesApplication() {
        return app;
    }

    @Provides
    @ApplicationContext
    Context providesContext(){
        return app;
    }
}

Config Persistent Component

@ConfigPersistent
@Component(dependencies = ApplicationComponent.class)
public interface ConfigPersistentComponent {

  ActivityComponent plus(ActivityModule activityModule);

}

Activity Component

@PerActivity
@Subcomponent(modules = ActivityModule.class)
public interface ActivityComponent {

  void inject(LoginActivity loginActivity);

}

Activity Module

@Module
public class ActivityModule {

  private Activity activity;

  public ActivityModule(Activity activity) {
    this.activity = activity;
  }

  @Provides
  @PerActivity
  Activity providesActivity() {
    return activity;
  }

}

Application class

public class App extends Application {

  private ApplicationComponent applicationComponent;

  @Override public void onCreate() {
    super.onCreate();
    Timber.plant(new Timber.DebugTree());
  }

  public static App get(Context context) {
    return (App) context.getApplicationContext();
  }

  public ApplicationComponent getComponent() {
    if (applicationComponent == null) {
      applicationComponent = DaggerApplicationComponent.builder()
              .applicationModule(new ApplicationModule(this))
              .busModule(new BusModule())
              .netModule(new NetModule())
              .prefsModule(new PrefsModule())
              .build();
    }
    return applicationComponent;
  }

}

MainPresenter

@ConfigPersistent
public class LoginActivityPresenter extends BasePresenter<LoginContract.View> {

  Context context;
  Gson gson;

  @Inject
  public LoginActivityPresenter(Activity activity, Gson gson) {
    this.context = activity;
    this.gson = gson;
  }

  @Override public void attachView(LoginContract.View view) {
    super.attachView(view);
    Timber.d("onAttach");
  }

  @Override public void detachView() {
    super.detachView();
    Timber.d("onDettach");
    disposableSubscriber.dispose();
  }
}

Base Presenter Activity

public abstract class BasePresenterActivity extends AppCompatActivity {

    private static final String KEY_ACTIVITY_ID = "KEY_ACTIVITY_ID";
    private static final AtomicLong NEXT_ID = new AtomicLong(0);
    private static final Map<Long, ConfigPersistentComponent> sComponentsMap = new HashMap<>();

    private ActivityComponent activityComponent;
    private long activityId;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create the ActivityComponent and reuses cached ConfigPersistentComponent if this is
        // being called after a configuration change.
        activityId = savedInstanceState != null ? savedInstanceState.getLong(KEY_ACTIVITY_ID) : NEXT_ID.getAndIncrement();
        ConfigPersistentComponent configPersistentComponent;
        if (!sComponentsMap.containsKey(activityId)) {
            Timber.i("Creating new ConfigPersistentComponent id=%d", activityId);
            configPersistentComponent = DaggerConfigPersistentComponent.builder()
                    .applicationComponent(App.get(this).getComponent())
                    .build();
            sComponentsMap.put(activityId, configPersistentComponent);
        } else {
            Timber.i("Reusing ConfigPersistentComponent id=%d", activityId);
            configPersistentComponent = sComponentsMap.get(activityId);
        }
        activityComponent = configPersistentComponent.plus(new ActivityModule(this));
    }

Main Activity

public class LoginActivity extends BasePresenterActivity implements LoginContract.View {

  @Inject
  EventBus bus;
  @Inject
  SharedPreferences prefs;
  @Inject
  Activity activity;
  @Inject
  LoginActivityPresenter presenter;

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    activityComponent().inject(this);
    setContentView(R.layout.activity_login);
    presenter.attachView(this);
  }

  @Override protected void onDestroy() {
    super.onDestroy();
    presenter.detachView();
  }
}

/********** EDIT **********/

To help save state for the presenter and still allow an activity context to be passed in, my solution is posted below. Any feedback is welcome.

     @ActivityContext
    public class LoginActivityPresenter extends BasePresenter<LoginContract.View> {

      Context context;

      @Inject
      LoginStateHolder loginStateHolder;

      @Inject
      public LoginActivityPresenter(Context context, Gson gson) {
        this.context = activity;
        this.gson = gson;
      }
}

@ConfigPersistent
public class LoginStateHolder {

  String title;

  @Inject
  public LoginStateHolder(Context context) {
    title = "Save me";
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public String getTitle() {
    return title;
  }
}

/*********** EDIT - 21_5_17 *********/

Exception:

Error:(13, 8) error: [<packagename>.injection.component.ActivityComponent.inject(<packagename>.login.ui.activity.LoginActivity)] android.app.Activity cannot be provided without an @Provides-annotated method.
android.app.Activity is injected at
<packagename>.login.presenter.LoginActivityPresenter.<init>(activity, …)
<packagename>.login.presenter.LoginActivityPresenter is injected at
<packagename>.login.ui.activity.LoginActivity.presenter
<packagename>.login.ui.activity.LoginActivity is injected at
<packagename>.injection.component.ActivityComponent.inject(loginActivity)

Login Activity

public class LoginActivity extends BasePresenterActivity implements LoginContract.View {

  @Inject
  EventBus bus;
  @Inject
  SharedPreferences prefs;
  @Inject
  LoginActivityPresenter presenter;

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    activityComponent().inject(this);
    setContentView(R.layout.activity_login);
    ButterKnife.bind(this);
    presenter.attachView(this);
    presenter.setupValidation(username, companyId, password);

}

Login Activity Presenter

@ActivityContext
public class LoginActivityPresenter extends BasePresenter<LoginContract.View> {

  @Inject LoginPresenterStorage loginPresenterStorage;

  Gson gson;

  @Inject
  public LoginActivityPresenter(Activity activity, Gson gson) {
    this.context = activity;
    this.gson = gson;
  }
}

Login Presenter Storage

@ConfigPersistent
public class LoginPresenterStorage {

  private String test = "";

    @Inject
  public LoginPresenterStorage(Activity activity) {
    test = "I didn't die";
  }

  public String getTest() {
    return test;
  }
}

App Component

@Singleton
@Component(modules = {ApplicationModule.class, BusModule.class, PrefsModule.class, NetModule.class})
public interface ApplicationComponent {

    @ApplicationContext Context context();
    Application application();
    EventBus bus();
    SharedPreferences prefs();
    Gson gson();

}

Activity Component

@ActivityContext
@Subcomponent(modules = ActivityModule.class)
public interface ActivityComponent {

  void inject(LoginActivity loginActivity);

}

ConfigPersistentComponent

@ConfigPersistent
@Component(dependencies = ApplicationComponent.class)
public interface ConfigPersistentComponent {

  ActivityComponent plus(ActivityModule activityModule);

}

Activity Module

@Module
public class ActivityModule {

  private Activity activity;

  public ActivityModule(Activity activity) {
    this.activity = activity;
  }

  @Provides
  @ActivityContext
  Activity providesActivity() {
    return activity;
  }

}

Edit

I made the mistake of using the same activity context with the activity module. Therefore, I coudln't inject the activity into the presenter. Changing the activity module to the original @peractivity scope and following the answer below will make the activity context injectable.

TheSunny
  • 371
  • 4
  • 14

1 Answers1

2
@ConfigPersistent
public class LoginActivityPresenter extends BasePresenter<LoginContract.View> {

By marking LoginActivityPresenter with @ConfigPersistent scope, you are telling Dagger "manage this class's instance in ConfigPersistentComponent" (i.e. always return the same instance from a given ConfigPersistentComponent instance), which means that it shouldn't have access to anything from a narrower scope like @ActivityScope. After all, ConfigPersistentComponent will outlive ActivityComponent, so injecting the LoginPresenter with an Activity doesn't make sense: The way you have it now, you'd get the same LoginPresenter instance with a different Activity.

The message "android.app.Activity cannot be provided without an @Inject constructor" comes from the generation of ConfigPersistentComponent, which doesn't have an Activity binding. Of course, your ActivityComponent does, but that's not where it's trying to store LoginPresenter with its current annotations.

Switch that declaration to @ActivityScope, and all will be well: You'll get a different LoginActivityPresenter for each Activity you create, and you'll also have access to everything in @Singleton scope (ApplicationComponent) and @ConfigPersistent scope (ConfigPersistentComponent).

@ActivityScope
public class LoginActivityPresenter extends BasePresenter<LoginContract.View> {
Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • Hi Jeff, That works now but I have an issue with saving the presenter for configuration changes, the original example without the activity injection stores the presenter fine for re-use and saves state. However, by using a narrower scope the presenter is created every time for the same activity. Have I approached this wrong, I was just wondering whether I even need an activity context for my presenter especially if it has to outlive the activity. I could always remove the context when I detach the presenter. Maybe I should use the application context instead, what's your thoughts on this? – TheSunny Apr 15 '17 at 11:57
  • 1
    You'll need to decide the roles of your instances here. Pieces that store state beyond the life of an Activity shouldn't inject the Activity, so if you remove that Context reference you may be good to go. On the other hand, presenters often have Activity-specific implementations, so maybe you do need some closely-collaborating Activity-scoped presenter as well—or maybe your LoginActivityPresenter stays activity-scoped and you create a ConfigPersistent holder for its persistent data. Either way is fine, you just need to decide which suits your needs best. – Jeff Bowman Apr 15 '17 at 15:37
  • How do I go about creating a ConfigPersistent holder to be injected into the Presenter? The ConfigPersistentComponent is generic component that is reused in the base activity to allow reuse, I would need a specific instance of the holder object to restore state as I would have to do this for every presenter. How would I go about this through DI? I've only been using Dagger for a week and from what I know I don't think I can inject a wider scope object into a narrower scope, any guidance or examples would be awesome, cheers – TheSunny Apr 16 '17 at 11:12
  • Hi Jeff, I posted my solution to saving state as an edit above, any feedback is appreciated. – TheSunny Apr 16 '17 at 12:12
  • 1
    @TheSunny Injecting a wider scope (`@Singleton` or `@ConfigPersistent`) into a narrower scope (`@ActivityScope`, unscoped) is absolutely allowed and common; you could easily write a `@ConfigPersistent` PresenterStorage or `@ConfigPersistent` LoginPresenterStorage that you could use for this. (The one you can't or shouldn't do is inject a narrow scope into a wide scope—a _scope-widening injection_—which would cause an Activity-specific binding to outlive the Activity it injects. That's what Dagger prevented you for doing in the original question.) – Jeff Bowman Apr 16 '17 at 15:39
  • Hi Jeff, I'm currently trying to run my code and I thought I had fixed the issue, it's not building now throwing an error of 'Context cannot be provided without an @Provides method'. I have updated the question with my current code. I have setup the login presenter storage but cannot get an Activity Context. – TheSunny May 21 '17 at 16:43
  • I don't see you bind `Context` or `@ActivityContext Context` anywhere, just `@ApplicationContext Context`. If it's missing, Dagger will give you that message. Can you show me where you have it? – Jeff Bowman May 21 '17 at 16:47
  • I was just switching between Context and Activity when testing as I was getting the Application object through when wanting a local context. Has been changed to Activity now. – TheSunny May 21 '17 at 17:05
  • any feedback I can get to fix the problem? – TheSunny May 23 '17 at 14:47
  • 1
    @TheSunny You still don't have a binding for `Context` or `@ActivityContext Context`. If you're no longer asking for a Context, then you haven't shown me your most recent error. This is going far beyond the scope of a comment box. – Jeff Bowman May 23 '17 at 17:24