16

I have objects of classes F1 and F2 that I want to inject in a retained Fragment. I also have an object of class A that depends on Activity, and I want it to be injected in that Activity and in a retained Fragment attached to that Activity's Fragment Manager. I write the following code. First, the module for the Activity dependency:

@Module
public class MainActivityModule {
    private Activity mActivity;

    public MainActivityModule(Activity activity) {
        mActivity = activity;
    }

    @Provides
    @ActivityScope
    public A provideA() {
        return new A(mActivity);
    }
}

Then, the corresponding component, that must make the A object available to its dependent components:

@ActivityScope
@Component(modules = {MainActivityModule.class})
public interface MainActivityComponent {
    void inject(MainActivity activity);

    // make the A object available to dependent components
    A getA();
}

I also write the Fragment-related module:

@Module
public class FragmentModule {
    @Provides
    @FragmentScope
    public F1 provideF1() {
        return new F1();
    }

    @Provides
    @FragmentScope
    public F2 provideF2() {
        return new F2();
    }
}

and its corresponding component:

@FragmentScope
@Component(modules = {FragmentModule.class}, dependencies = {MainActivityComponent.class})
public interface FragmentComponent {
    void inject(MyFragment presenter);
}

Finally, I inject the dependency on A in the Activity, where I also need to call specific life cycle methods on it. The Activity also provides a method to get the component so that the Fragment is able to use it when building its own component:

// in MainActivity.onCreate
mActivityComponent = DaggerMainActivityComponent.builder()
        .mainActivityModule(new MainActivityModule(this))
        .build();
mActivityComponent.inject(this);
mA.onCreate();

and I try to inject the dependencies on A, F1, F2 in the Fragment, too:

// in MyFragment.onCreate
FragmentComponent component = DaggerFragmentComponent.builder()
        .fragmentModule(new FragmentModule())
        .mainActivityComponent(((MainActivity) getActivity()).getComponent())
        .build();
component.inject(this);

However, since the Fragment is retained, when the Activity is destroyed and recreated by the system reacting to a configuration change (e.g. a device rotation), the Fragment maintains a reference to the old A instance, while the new Activity has correctly recreated a new A instance to go with it. To work around this problem, I have to create the FragmentComponent and inject dependencies in MyFragment.onActivityCreated rather than MyFragment.onCreate. On the other hand, this implies that F1 and F2 dependencies are recreated every time the activity is destroyed and recreated; but they are Fragment-scoped dependencies, so they should follow the Fragment life cycle instead of the Activity's.

Therefore, my question is as follows: is it possible to have differently-scoped dependencies injected in a retained Fragment? Ideally, F1 and F2 dependencies should be injected in MyFragment.onCreate, while A dependency should be injected in MyFragment.onActivityCreated. I tried using two different components, but it seems not to be possible to perform partial injection. Currently, I ended up adding an explicit reassignment of the Fragment A dependency in MyFragment.onActivityCreated, but that's not really injection, you know. Could this be done in a better way?

Giulio Piancastelli
  • 15,368
  • 5
  • 42
  • 62
  • 1
    Injection does not play well with objects that have lifecycle that is out of your control (like Activities). Usually one fragment can work only with it's parent activity so you don't really benefit from injection. However for unit tests you can provide `setA(A a)` method so you can override the value with mock/test object. Also: why are you using 2 separate components and inject graphs respectively (instead of one application wide component and thus having just one building of the graph on app's start)? – Ognyan Aug 20 '15 at 18:42
  • @Ogre_BGR I know it's hard, that's why I'm here asking for help. The benefit I see is related to trimming the amount of code going into the Activity; it needs to contain the lifecycle-related calls on its `A` object, but since the object is really used by the Fragment, that code should stay in the Fragment, not in the Activity. So it's something related to code organization, not tests. I can't use an application-wide component because `A` (which I don't have full control upon) has a strict dependence on an Activity instance (not just a Context). – Giulio Piancastelli Aug 21 '15 at 09:04
  • u can make an application module and get all the instances from application module – shakil.k Apr 07 '16 at 05:26
  • @GiulioPiancastelli could you resolve your problem? i have same problem – mahdi pishguy Oct 21 '16 at 05:18
  • @mahdipishguy no, I have not found a feasible alternative, so I kept injecting in `onActivityCreated` even if is not ideal. – Giulio Piancastelli Oct 21 '16 at 07:10
  • The way that I see it, Fragments and Activities have separate lifecycles and a module for an activity should only contain bindings that actually NEED an activity in the scope to fulfill, such as a LayoutInflator or other component intimately tied to an activity. Anything else should go in a higher level module provided by a higher level component (eg singleton). FragmentScope and ActivityScope should be subcomponents of it. If your fragment REALLY needs something in the activity scope it should request it directly from the activity it's attached to. – Avi Cherry Jan 06 '17 at 01:35
  • @AviCherry following your suggestion would also mean that if a fragment needs something at application scope the fragment should request it from the application (which in turn must be requested from the activity). The only dependencies injected in a fragment would be those that are "intimately tied" to it: barely any, probably. To me, that means a complete failure of dependency injection, because why on Earth you would need Dagger if you could create and store your singletons in activity and application objects, and just ask them whenever you need to from other objects? – Giulio Piancastelli Jan 07 '17 at 13:12
  • 1
    I think you misunderstand what I was saying. A Fragment component should of course be a subcomponent of a higher level scope and therefore should inject anything it needs from its own or a higher scope. But if you're going to use retained fragments then fragments no longer belong to a single activity and therefore it might not be appropriate to scope them as subcomponents of an activity but as siblings. If you don't use retained fragments then it's a perfect fit to make fragments subcomponents of activities. Otherwise, beware. – Avi Cherry Jan 07 '17 at 23:07
  • @AviCherry yes indeed, sorry, I misunderstood. Now that I get it, I think it's a very interesting point of view. I'll probably think about experimenting with it, even if for my next project I hope to be able to entirely get rid of fragments for the view layer, but still using retained ui-less fragments as targets for asynchronous tasks and such. Thank you for your suggestion. – Giulio Piancastelli Jan 08 '17 at 12:01

1 Answers1

8

Considering your retained fragment lives longer than your activity, I'd wager that the proper way to do this would be to make the FragmentScope contain the ActivityScope, and not vice versa.

Meaning your FragmentComponent would have

@FragmentScope
@Component(modules = {FragmentModule.class})
public interface FragmentComponent {
    void inject(MyFragment presenter);
}

And your Activity component would have

@ActivityScope
@Component(dependencies = {FragmentComponent.class}, modules = {MainActivityModule.class})
public interface MainActivityComponent extends FragmentComponent { //provision methods
    void inject(MainActivity activity);

    // make the A object available to dependent components
    A getA();
}

Which is possible if your Fragment injected classes don't rely on the Activity module as dependencies.

This can be done with something akin to

public class MainActivity extends AppCompatActivity {

    private MainActivityComponent mainActivityComponent;

    private MyFragment myFragment;

    @Override
    public void onCreate(Bundle saveInstanceState) {
         super.onCreate(saveInstanceState);
         setContentView(R.layout.activity_main);

         if(saveInstanceState == null) { // first run
             myFragment = new MyFragment(); //headless retained fragment
             getSupportFragmentManager()
                .beginTransaction()
                .add(myFragment, MyFragment.class.getName()) //TAG
                .commit();
         } else {
             myFragment = (MyFragment)(getSupportFragmentManager()
                               .findFragmentByTag(MyFragment.class.getName()));
         }
    }

    @Override
    public void onPostCreate() {
         mainActivityComponent = DaggerMainActivityComponent.builder()
              .fragmentComponent(myFragment.getComponent())
              .build();
    }
}

And

public class MyFragment extends Fragment {
    public MyFragment() {
         this.setRetainInstance(true);
    }

    private FragmentComponent fragmentComponent;

    @Override
    public void onCreate(Bundle saveInstanceState) {
        super.onCreate(saveInstanceState);
        this.fragmentComponent = DaggerFragmentComponent.create();
    }

    public FragmentComponent getFragmentComponent() {
        return fragmentComponent;
    }
}

EDIT:

public class MyFragment extends Fragment {
    public MyFragment() {
         this.setRetainInstance(true);
         this.fragmentComponent = DaggerFragmentComponent.create();
    }

    private FragmentComponent fragmentComponent;

    public FragmentComponent getFragmentComponent() {
        return fragmentComponent;
    }
}

public class MainActivity extends AppCompatActivity {

    private MainActivityComponent mainActivityComponent;

    private MyFragment myFragment;

    @Inject
    A mA;

    @Override
    public void onCreate(Bundle saveInstanceState) {
         super.onCreate(saveInstanceState);
         setContentView(R.layout.activity_main);

         if(saveInstanceState == null) { // first run
             myFragment = new MyFragment(); //headless retained fragment
             getSupportFragmentManager()
                .beginTransaction()
                .add(myFragment, MyFragment.class.getName()) //TAG
                .commit();
         } else {
             myFragment = (MyFragment)(getSupportFragmentManager().findFragmentByTag(MyFragment.class.getName()));
         }
         mainActivityComponent = DaggerMainActivityComponent.builder()
              .fragmentComponent(myFragment.getComponent())
              .build();
         mainActivityComponent.inject(this);
         mA.onCreate();
    }
}
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • That's a very interesting take, thank you. At the moment (the application is not finished yet) "my `Fragment` injected classes don't rely on the Activity module as dependencies" indeed; however, I still need to invoke `mA.onCreate` in `MainActivity.onCreate`, and I'm not sure I can delay that call until `MainActivity.onPostCreate`. It seems that's just not possible even with your clever workaround, isn't it? Oh, and another question: why do I need to extend `FragmentComponent` with `MainActivityComponent`? – Giulio Piancastelli Aug 27 '15 at 09:39
  • You need to specify what this `mA.onCreate` is that you need to call, because I currently don't see it. `onPostCreate()` is used to make sure the `onCreate()` method of the fragment runs before you create the component for the Activity (`onPostCreate()` is called after every fragment is initialized). I extend the component to get ahold of its provision methods in case it is subclassed. – EpicPandaForce Aug 27 '15 at 10:37
  • What do you mean with "I currently don't see it"? It's in the last-but-one code snippet of the question. I just edited a comment (I wrote `MyActivity` where I meant `MainActivity`) but the call has always been there. – Giulio Piancastelli Aug 27 '15 at 14:26
  • oh. You're right, it's been there all along, I'm not sure why I didn't notice. I think calling it in `onPostCreate()` could work, but if not, then you could just construct the `FragmentComponent` in the constructor of the fragment, and then you don't need to place it over to the `onPostCreate()` – EpicPandaForce Aug 27 '15 at 14:28
  • Thanks. Finally I had a chance to revisit my own code in the light of your solution, and there still is at least one discrepancy: in my case, the retained fragment is not immediately built nor added to the fragment manager; the retained fragment may even never be built if the user does not use the specific feature that the fragment helps implementing. Your solution would force me to create it anyway. I wonder if it's indeed feasible, or if I would end up stumbling upon some kind of anti-pattern either on the Android or on the Dagger side. – Giulio Piancastelli Sep 03 '15 at 12:42
  • @EpicPandaForce Hi man, could you help me on this topic? http://stackoverflow.com/q/40168988/6599627 Thanks – mahdi pishguy Oct 21 '16 at 05:26