0

In my efforts to follow the good and official advice for injecting and avoiding cumbersome code (which I had) from the authors themselves, I ran into a wall when trying to use the support library.

According to the article:

AppCompat users should continue to implement AndroidInjector.Factory<? extends Activity> and not <? extends AppCompatActivity> (or FragmentActivity).

I'm sticking to an MVP architecture where views are always Fragments and I don't want to involve my Activity in any DI business, but I wonder if it's necessary for this to work but so far I haven't been able to. If I skip the whole support thing, the app crashes at runtime because the instance of the fragment is support (in case it's not obvious). Then I went into the task of trying to try to implement HasSupportFragmentInjector instead of HasFragmentInjector with a whole bunch of changes due to compile errors my mind has forgotten for the sake of my mental health. After a while I come to a point of thinking how can a non-support Activity host a support fragment. Ah! Those tricky wildcards. But no matter how I've tried to follow the advice, I can't come up with a way without an EmptyModule that I also would need to setup in the Activity so it would be visible to the fragment by dagger and its (really, for me still, magic). Why I haven't tried it? I might as well have, but I'm tired of hopeless changes and I need help at this point.

AppModule.kt

@Singleton
@dagger.Module
class AppModule(val application: Application) {
    @Provides @Singleton fun application(): Application = application
    ...
}

AppComponent.java

@ApplicationScope
@Singleton
@Component(modules = {
        AndroidSupportInjectionModule.class,
        ...
        FooFragmentModule.class,
})
public interface AppComponent {
    Application app();
    ...
    void inject(MyApp app);
}

MyApp.java

public class MyApp extends Application implements HasActivityInjector {

    private AppComponent component;
    public AppComponent someWayToReturnAppComponent() {
        ...
    }

    @Inject DispatchingAndroidInjector<Activity> dispatchingActivityInjector;

    @Override
    public void onCreate() {
        component = DaggerAppComponent.builder()
                .appModule(new AppModule(this))
                // more app-scoped modules
                .build();

        component.inject(this);
    }


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

}

MainActivity.java

public abstract class MainActivity extends AppCompatActivity implements HasSupportFragmentInjector {

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

        setContentView(getLayout()); // inflate the fragment via XML here
    }

    @Inject DispatchingAndroidInjector<Fragment> dispatchingFragmentInjector;

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

FooFragmentComponent.java

@Subcomponent
public interface FooFragmentComponent extends AndroidInjector<FooFragment> {

    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<FooFragment> {}

}

FooFragmentModule.kt

@dagger.Module(subcomponents = {FooFragmentComponent.class})
public abstract class FooFragmentModule {

    @Binds
    @IntoMap
    @FragmentKey(FooFragment.class)
    abstract AndroidInjector.Factory<? extends Fragment> bindFragmentInjectorFactory(FooFragmentComponent.Builder builder);

    @ActivityScope
    abstract FooFragment contributeFooFragmentInjector();

    @Provides
    static FooPresenter presenter() {
        return new FooPresenter();
    }
}

FooFragment

public class FooFragment extends Fragment implements SomeView {

    @Inject FooPresenter presenter;

}

OK. At this point, and going back to

AppCompat users should continue to implement AndroidInjector.Factory<? extends Activity>

I've had no need (and willingly opposing) to use it, only for the fragment. Do I really need to setup a module and component for it or am I missing something?

EDIT

After following EpicPandaForce's advice of using AndroidSupportInjectionModule, Dagger now complains that

FragmentKey methods should bind dagger.android.AndroidInjector.Factory<? extends android.app.Fragment>, not dagger.android.AndroidInjector.Factory<? extends android.support.v4.app.Fragment>.

Chisko
  • 3,092
  • 6
  • 27
  • 45

1 Answers1

2

As @EpicPandaForce mentioned in the comments, you need to use AndroidSupportInjectionModule for support Fragments. You'll also need to use the FragmentKey in dagger.android.support, not the one in dagger.android. That should get you past the problem in your edit.

To your broader point, support Fragments do not extend base Fragments (which are deprecated anyway in API 28 and beyond). This paints them in contrast to AppCompatActivity and its superclass, the support library's FragmentActivity, which both extend the framework Activity as introduced in Android API level 1. Thus, whether you're using support Fragments or built-in Fragments, you might not have a parent AppCompatActivity, but you'll always have an Activity of some sort. This is important because Android reserves the right to instantiate your Fragment using its necessary public no-arg constructor, which means that the Fragment can only self-inject using things that it can find inside onAttach (i.e. its parent fragments, its Activity, or its Application).

dagger.android is unconcerned whether your Activity is an AppCompatActivity because it does not use the Activity other than looking for its own injector. You can see that in the AndroidSupportInjection.findHasFragmentInjector private method, which checks (in order) the hierarchy of parent fragments, then the Activity, then the Application. Consequently, even though practically speaking Support Fragments will only function properly on support Activities, dagger.android can bind its keys based on the superclass Activity because there's no reason to differentiate them and set up two separate maps (Activity vs AppCompatActivity). Even if there were a separation like that, you could bind AppCompatActivity injectors into your Activity map, and everything would get terribly confusing.

You should also take from that search order that if you do not have Activity-scoped bindings, you do not need to create an Activity-scoped component; you can have your Application implement HasSupportFragmentInjector, install your FooFragmentModule directly into AppComponent, and remove HasSupportFragmentInjector from your MainActivity. This is atypical only because most apps have some sense of Activity state or controllers that should be injectable (even just injecting the Activity instance itself, or its Context or Resources). If you only have your @ActivityScope annotation because you're trying to make this work, you can skip that step entirely and only use an Application component and several Fragment subcomponents. However, I think it is very likely that you will eventually need @ActivityScope, so creating a Component for it early-on is pretty reasonable.

Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • I can't believe I got defeated by an import and didn't check that. Thanks for this amazing explanation, but is Dagger's documentation just the opposite of its amazing essence or it's just me? – Chisko Jul 18 '18 at 02:00
  • @Chisko Glad you found the answer helpful! I'm no technical writer or Dagger developer, and I don't mean to disparage my colleagues, but there does seem to be more technical-level documentation than practical explanations or decision making. It's also easy to bump up against verbose but difficult-to-understand error messages. One of my more popular write-ups [explains `@Reusable` vs `@Singleton`](https://stackoverflow.com/q/39136042/1426891), which may be a familiar experience in that the official docs are technically correct but (as quoted in the question) pretty opaque. – Jeff Bowman Jul 18 '18 at 02:22
  • I've been going through more articles and now I understand a bit more about Dagger. But what is now shocking me is that the (for now) only dependency I'm trying to inject is null. Taking a look at your answer again, I now wonder why I need to create an activity component (or fragment) when the whole idea of the article is to get rid of the lifecycle boilerplate (which is itself the component creation, according to it). I tried adding an `inject(FooFragment)` method to the subcomponent and... – Chisko Jul 24 '18 at 03:26
  • playing around with `@ActivityScope abstract FooFragment conntributeFooFragmentInjector();` and using several annotations on `@Provides static FooPresenter presenter() { return new FooPresenter();}` like @Reusable as you suggested but it's always null. It would be great to know a process to make `@Inject` work. – Chisko Jul 24 '18 at 03:29
  • @Chisko Though I'd be glad to help, if you're calling `AndroidInjection.inject(this)` or `AndroidSupportInjection.inject(this)` from `onAttach` as in the `dagger.android` docs, then there might not be a universal process. I'd recommend asking a new question, so you have the freedom to paste in all the details to make a self-contained example that we can help to debug. – Jeff Bowman Jul 25 '18 at 19:44
  • 1
    I was missing `AndroidInjection.inject(this);` from the fragments and a little trick for an activities module that I hope will integrate all activities in one file. But now it all works. Thanks so much! – Chisko Aug 03 '18 at 07:15