5

I am exploring the new dagger.android from Dagger 2.11. I hope not to have to create custom scope annotation like @PerActivity. So far I was able to do the following:

1) Define Application scope Singletons and injecting them into activities.

2) Define Activity scope non-Singleton dependencies and injecting them into their activities using @ContributesAndroidInjector

What I cannot figure out is how to have an Application scope Singleton and Activity scope non-Singletons using it.

In the example below, I would like my Activity scope MyActivityDependencyA and MyActivityDependencyB to have access to a Singleton MyActivityService

The setup below results in:

Error:(24, 3) error: com.example.di.BuildersModule_BindMyActivity.MyActivitySubcomponent (unscoped) may not reference scoped bindings: @Singleton @Provides com.example.MyActivityService com.example.MyActivitySingletonsModule.provideMyActivityService()

Here is my setup. Note, I defined separate MyActivitySingletonsModule and MyActivityModule since I could not mix Singleton and non-Singleton dependencies in the same Module file.

@Module
public abstract class BuildersModule {
    @ContributesAndroidInjector(modules = {MyActivitySingletonsModule.class, MyActivityModule.class})
    abstract MyActivity bindMyActivity();
    }
}

and

@Module
public abstract class MyActivityModule {
    @Provides
    MyActivityDependencyA provideMyActivityDependencyA(MyActivityService myActivityService){
       return new MyActivityDependencyA(myActivityService);
    }
    @Provides
    MyActivityDependencyB provideMyActivityDependencyB(MyActivityService myActivityService) {
        return new MyActivityDependencyB(myActivityService);
    }
}

and

@Module
public abstract class MyActivitySingletonsModule {
    @Singleton
    @Provides
    MyActivityService provideMyActivityService() {
        return new MyActivityService();
    }
}

and

@Singleton
 @Component(modules = {
    AndroidSupportInjectionModule.class,
    AppModule.class,
    BuildersModule.class})

public interface AppComponent {
    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder application(App application);
        AppComponent build();
    }
    void inject(App app);
}

Is it even possible to do what I am trying to do without defining custom scope annotations?

Thanks in advance!

David Rawson
  • 20,912
  • 7
  • 88
  • 124
liminal
  • 1,144
  • 2
  • 13
  • 24

2 Answers2

12

Why avoid custom scopes? Custom scopes are still required for the new dagger.android dependency injection framework introduced in Dagger 2.10+.

"My understanding is @ContributesAndroidInjector removes the need for custom annotation and I was able to prove it by using non-singletons defined in the activity scope without any issues."

@ContributesAndroidInjector (available in v2.11) does not remove the need for custom scopes. It merely replaces the need to declare @Subcomponent classes that does not make use of @Subcomponent.Builder to inject dependencies required by the component at runtime. Take a look at the below snippet from the official dagger.android user guide about @ContributesAndroidInjector;

"Pro-tip: If your subcomponent and its builder have no other methods or supertypes than the ones mentioned in step #2, you can use @ContributesAndroidInjector to generate them for you. Instead of steps 2 and 3, add an abstract module method that returns your activity, annotate it with @ContributesAndroidInjector, and specify the modules you want to install into the subcomponent. If the subcomponent needs scopes, apply the scope annotations to the method as well."

@ActivityScope
@ContributesAndroidInjector(modules = { /* modules to install into the subcomponent */ })
abstract YourActivity contributeYourActivityInjector();

The key here is "If the subcomponent needs scopes, apply the scope annotations to the method as well."

Take a look at the following code for an overview of how to use @Singleton, @PerActivity, @PerFragment, and @PerChildFragment custom scopes with the new dagger.android injection framework.

// Could also extend DaggerApplication instead of implementing HasActivityInjector
// App.java
public class App extends Application implements HasActivityInjector {

    @Inject
    AppDependency appDependency;

    @Inject
    DispatchingAndroidInjector<Activity> activityInjector;

    @Override
    public void onCreate() {
        super.onCreate();
        DaggerAppComponent.create().inject(this);
    }

    @Override
    public AndroidInjector<Activity> activityInjector() {
        return activityInjector;
    }
}

// AppModule.java
@Module(includes = AndroidInjectionModule.class)
abstract class AppModule {
    @PerActivity
    @ContributesAndroidInjector(modules = MainActivityModule.class)
    abstract MainActivity mainActivityInjector();
}

// AppComponent.java
@Singleton
@Component(modules = AppModule.class)
interface AppComponent {
    void inject(App app);
}

// Could also extend DaggerActivity instead of implementing HasFragmentInjector
// MainActivity.java
public final class MainActivity extends Activity implements HasFragmentInjector {

    @Inject
    AppDependency appDependency; // same object from App

    @Inject
    ActivityDependency activityDependency;

    @Inject
    DispatchingAndroidInjector<Fragment> fragmentInjector;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);

        if (savedInstanceState == null) {
            addFragment(R.id.fragment_container, new MainFragment());
        }
    }

    @Override
    public final AndroidInjector<Fragment> fragmentInjector() {
        return fragmentInjector;
    }
}

// MainActivityModule.java
@Module
public abstract class MainActivityModule {
    @PerFragment
    @ContributesAndroidInjector(modules = MainFragmentModule.class)
    abstract MainFragment mainFragmentInjector();
}

// Could also extend DaggerFragment instead of implementing HasFragmentInjector
// MainFragment.java
public final class MainFragment extends Fragment implements HasFragmentInjector {

    @Inject
    AppDependency appDependency; // same object from App

    @Inject
    ActivityDependency activityDependency; // same object from MainActivity

    @Inject
    FragmentDependency fragmentDepency; 

    @Inject
    DispatchingAndroidInjector<Fragment> childFragmentInjector;

    @SuppressWarnings("deprecation")
    @Override
    public void onAttach(Activity activity) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            // Perform injection here before M, L (API 22) and below because onAttach(Context)
            // is not yet available at L.
            AndroidInjection.inject(this);
        }
        super.onAttach(activity);
    }

    @Override
    public void onAttach(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            // Perform injection here for M (API 23) due to deprecation of onAttach(Activity).
            AndroidInjection.inject(this);
        }
        super.onAttach(context);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.main_fragment, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        if (savedInstanceState == null) {
            addChildFragment(R.id.child_fragment_container, new MainChildFragment());
        }
    }

    @Override
    public final AndroidInjector<Fragment> fragmentInjector() {
        return childFragmentInjector;
    }
}

// MainFragmentModule.java
@Module
public abstract class MainFragmentModule {
    @PerChildFragment
    @ContributesAndroidInjector(modules = MainChildFragmentModule.class)
    abstract MainChildFragment mainChildFragmentInjector();
}

// MainChildFragment.java
public final class MainChildFragment extends Fragment {

    @Inject
    AppDependency appDependency; // same object from App

    @Inject
    ActivityDependency activityDependency; // same object from MainActivity

    @Inject
    FragmentDependency fragmentDepency; // same object from MainFragment

    @Inject
    ChildFragmentDependency childFragmentDepency;

    @Override
    public void onAttach(Context context) {
        AndroidInjection.inject(this);
        super.onAttach(context);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.main_child_fragment, container, false);
    }
}

// MainChildFragmentModule.java
@Module
public abstract class MainChildFragmentModule {
}

// PerActivity.java
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerActivity {
}

// PerFragment.java
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerFragment {
}

// PerChildFragment.java
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerChildFragment {
}

// AppDependency.java
@Singleton
public final class AppDependency {
    @Inject
    AppDependency() {
    }
}

// ActivityDependency.java
@PerActivity
public final class ActivityDependency {
    @Inject
    ActivityDependency() {
    }
}

// FragmentDependency.java
@PerFragment
public final class FragmentDependency {
    @Inject
    FragmentDependency() {
    }
} 

// ChildFragmentDependency.java
@PerChildFragment
public final class ChildFragmentDependency {
    @Inject
    ChildFragmentDependency() {
    }
}

For a complete dagger.android 2.11 setup guide using @ContributesAndroidInjector and custom scopes mentioned above, read this article.

2

There are some problems here: firstly, ActivitySingleton doesn't make much sense. A dependency is either a singleton (per app, or app scoped) or not.

If it is not a singleton it could be activity scoped (per activity). This would mean it lived and died with the Activity i.e., that its lifecycle was congruent with that of the Activity itself and hence it would be destroyed with the onDestroy of the Activity.

That doesn't mean that everything that is injected inside an Activity must be @PerActivity. You can still inject @Singleton dependencies there (like per app OkHttpClient for instance). However, these @Singleton dependencies will not be bound in the module set that composes a @PerActivity component. Instead, they will be bound in the module set for parent components and obtained through the component hierarchy (dependent components or sub-components).

These means that your ActivitySingletonsModule is incorrect, see the comments in the code below:

@Module
public abstract class MyActivitySingletonsModule {
    //@Singleton
    //^^ remove the annotation here if you want to use the
    //in your ActivityComponent

    //If you need this as a per-app singleton, then include 
    //this module at the AppComponent level
    @Provides
    MyActivityService provideMyActivityService() {
        return new MyActivityService();
    }
}

I do not understand the reluctance to define a custom scope. These are extremely lightweight and can improve readability. Here is the single line of code you would need to create a @PerActivity scope.

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

I suspect the concept of scopes is unclear and this is leading to the reluctance. Admittedly, it can be rather confusing. However there are some really good canonical answers that help clarify. I would suggest this question as a start:

Dagger2 Custom Scopes : How do custom-scopes (@ActivityScope) actually work?

David Rawson
  • 20,912
  • 7
  • 88
  • 124
  • David, thanks for your answer. I've written and used custom Dagger annotations before so the concept is pretty clear to me. My understanding is @ContributesAndroidInjector removes the need for custom annotation and I was able to prove it by using non-singletons defined in the activity scope without any issues. However, why is having 1 instance of a class (definition of Singleton) and that class being specifically designed for a specific activity does not make much sense to you? – liminal Jul 21 '17 at 11:26
  • Thanks for acknowledging the answer. In the context of an Android app, "Singleton" usually means "one per app". Your "per activity" dependencies will get one instance per the Activity you inject them in. This doesn't make them singletons. – David Rawson Jul 21 '17 at 11:29
  • To address your statement that my "per activity" dependencies will get one instance per the Activity I inject them in, previously when I removed the @Singleton annotation when defining `MyActivityService`, `MyActivityDependencyA` and `MyActivityDependencyB` each got distinct instances of `MyActivityService`. I still think that there are valid scenarios when you'd need to have only a single instance of an Activity-scoped class for the rest of the dependencies in that Activity to interact with (maybe that activity needs to have its own data cache, authentication service, etc.). – liminal Jul 21 '17 at 14:17
  • 1
    @liminal are you injecting in `onCreate()`? if you inject a dependency in `onCreate()` for a given Activity (`Activity1`) and the dependency is `@PerActivity` then you will only get one instance for _that_ activity. If you inject that dependency again for `Activity2` you will get another instance. If you want one instance to span both Activity you need to make it app-scoped. – David Rawson Jul 21 '17 at 21:07
  • 1
    David, I am injecting `myActivityDependencyA` and `myActivityDependencyB` in onCreate() of `MyActivity`. Then I call `myActivityDependencyA.getGreeting()` and `myActivityDependencyB.getGreeting()`. in both cases, `getGreeting()` is delegated to `MyActivityService`. There I print value of `this` and it's different so this tells me 2 instances of the service were created `MyActivityService@7a4df92` and `MyActivityService@a3ddd63` respectively – liminal Jul 21 '17 at 22:03