Reputation: 381
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.
Upvotes: 1
Views: 3450
Reputation: 95634
@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> {
Upvotes: 2