Giulio Piancastelli
Giulio Piancastelli

Reputation: 15808

Dagger 2 on Android: inject same dependency in Activity and retained Fragment

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?

Upvotes: 16

Views: 15339

Answers (1)

EpicPandaForce
EpicPandaForce

Reputation: 81539

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();
    }
}

Upvotes: 8

Related Questions