Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dagger 2.11: Local Singleton when using @ContributesAndroidInjector

I have a working Dagger setup using the @ContributesAndroidInjector annotation (https://google.github.io/dagger/android.html).

        Component Application
       /                     \
Subcomponent DrawerActivity   Subcomponent SubActivity
      |
Subcomponent DrawerFragment

In SubActivity and DrawerActivity I am using the same repository instance which is marked as @Singleton.

@Singleton
@Component(modules = {
        AndroidInjectionModule.class,
        AppModule.class,
        ActivityBuilderModule.class
})
public interface AppComponent {
    @Component.Builder
    interface Builder{
        @BindsInstance Builder application(Application application);

        AppComponent build();
    }
    void inject(App app);
}

@Module
public abstract class ActivityBuilderModule {
    @PerActivity
    @ContributesAndroidInjector(modules = {DrawerActivityModule.class, 
    FragmentBuilderModule.class})
    abstract DrawerActivity bindDrawerActivity();

    @PerActivity
    @ContributesAndroidInjector(modules = {DrawerActivityModule.class})
    abstract SubActivity bindSubActivity();
}

@Module
public abstract class FragmentBuilderModule {
    @PerFragment
    @ContributesAndroidInjector(modules = DrawerFragmentModule.class)
    abstract DrawerFragment provideDrawerFragment();
}


@Singleton
public class Repository{
    private SomeClass mSomeClass;

    @Inject
    public VehicleRepositoryImpl(SomeClass someClass) {
        mSomeClass = someClass;
    }
}


public class App extends Application implements HasActivityInjector{
    @Inject
    DispatchingAndroidInjector<Activity> dispatchingAndroidInjector;

    @Override
    public void onCreate() {
        super.onCreate();
        if (BuildConfig.DEBUG) {
            Timber.plant(new Timber.DebugTree());
        }
        AppComponent component = DaggerAppComponent.builder().application(this)
                .build();
        component.inject(this);
    }

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

public class DrawerActivity extends AppCompatActivity implements HasSupportFragmentInjector{
    @Inject
    DispatchingAndroidInjector<Fragment> fragmentDispatchingAndroidInjector;

    @Override
    public AndroidInjector<Fragment> supportFragmentInjector() {
        return fragmentDispatchingAndroidInjector;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
    }
}

public class DrawerFragment extends Fragment {
    @Inject
    ViewModelFactory mViewModelFactory; //repository gets injected into factory

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        AndroidSupportInjection.inject(this);
        super.onCreate(savedInstanceState);
    }
}

public class SubActivity extends AppCompatActivity{
    @Inject
    Repository mRepository;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
    }
}

I have now the need to add a user management. This requires a LoginActivity. I don't want the repository to be avaiblable in LoginActivity. So I'd like to create a local singleton scope "UserScope" for DrawerActivity/Subactivity/DrawerFragement

              Component Application
           /                          \
   @UserScope                       @LoginScope 
   Subcomponent DrawerActivity      SubComponent LoginActivity
      |                        \
Subcomponent DrawerFragment   Subcomponent SubActivity

How can I achieve this still using the new @ContributesAndroidInjector annotation?

I want it to work like in this blogpost: http://frogermcs.github.io/building-userscope-with-dagger2/

like image 308
piveloper Avatar asked Nov 13 '17 18:11

piveloper


2 Answers

I solved my problem by doing it like in this repo:

https://github.com/ragdroid/Dahaka

Many thanks to its contributor!

Update 1: Code example added.

This graph gives a rough idea of the code example.

                 Component Application
           /                               \
   @UserScope                          @LoginScope 
   Subcomponent UserComponent          SubComponent LoginActivity
      |                      \
Subcomponent DrawerActivity   Subcomponent SubActivity
      |
SubComponent DrawerFragment

Code Example (If somethings missing please let me know in the comments):

1. Dagger Setup

AppComponent is the root of Dagger graph:

@Singleton
@Component(modules = {
        AndroidSupportInjectionModule.class,
        AppModule.class,
        AppBindingModule.class 
})
public interface AppComponent extends AndroidInjector<DaggerApplication> {
    @Component.Builder
    interface Builder{
        @BindsInstance Builder application(Application application);

        AppComponent build();
    }

    void inject(MyApp app);

    UserComponent.Builder userBuilder();

    UserManager getUserManager();
}

Module which binds its SubComponents:

@Module(subcomponents = UserComponent.class)
public abstract class AppBindingModule {

    @ContributesAndroidInjector(modules = LoginModule.class)
    @LoginScope
    abstract LoginActivity loginActivity();

}    

UserComponent holds instances of classes that are used only when user is logged in. All classes provided in UserModule.class are available as "LocalSingletons" in further subcomponents like activities and fragment components.

@UserScope
@Subcomponent(modules = {
        UserBindingModule.class,
        UserModule.class,
        AndroidSupportInjectionModule.class
})
public interface UserComponent extends AndroidInjector<DaggerApplication> {
    void inject(UserManager userManager);

    @Subcomponent.Builder
    interface Builder{
        UserComponent build();
    }
}

UserBindingModule defines which activity-subcomponents belong to UserComponent.

@Module
public abstract class UserBindingModule {
    @ContributesAndroidInjector(modules = {DrawerBindingModule.class, AndroidSupportInjectionModule.class})
    abstract DrawerActivity bindDrawerActivity();

    @ContributesAndroidInjector
    abstract SubActivity bindSubActivity();
}

DrawerBindingModule defines which fragment-subcomponents belong to DrawerActivityComponent.

@Module
public abstract class DrawerBindingModule {
    @DrawerFragmentScope
    @ContributesAndroidInjector(modules = DrawerFragmentModule.class)
    abstract DrawerFragment provideDrawerFragment();
}

The UserManager handles user login/logout and all further activity injections.

@Singleton
public class UserManager implements HasActivityInjector {
    private final UserComponent.Builder userComponentBuilder;
    @Inject
    DispatchingAndroidInjector<Activity> activityInjector;

    private UserComponent userComponent;

    @Inject
    public UserManager(UserComponent.Builder builder) {
        this.userComponentBuilder = builder;
    }

    public void logIn(){
        createUserSession();
    }

    private void createUserSession() {
        userComponent = userComponentBuilder.build();
        userComponent.inject(this);
    }

    public boolean isLoggedIn() {
        return userComponent != null;
    }

    public void logOut() {
        userComponent = null;
    }

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

2. App+Activity+Fragment classes

public class MyApp extends Application implements HasActivityInjector{
    @Inject
    DispatchingAndroidInjector<Activity> dispatchingAndroidInjector;

    @Inject
    UserManager mUserManager;

    @Override
    public void onCreate() {
        super.onCreate();
        if (BuildConfig.DEBUG) {
            Timber.plant(new Timber.DebugTree());
        }
        AppComponent component = DaggerAppComponent.builder().application(this)
                .build();
        component.inject(this);
    }

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


public class LoginActivity extends AppCompatActivity {
    Intent mOpenDrawerActivity;
    private ActivityLoginBinding binding;

    @Inject
    UserManager mUserManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
        ...
    }

    void openDrawerActivity(){
        mUserManager.logIn();
        mOpenDrawerActivity = new Intent(this, DrawerActivity.class);
        startActivity(mOpenDrawerActivity);
        finish();
    }
}


public class DrawerActivity extends BaseUserActivity implements HasSupportFragmentInjector{
    @Override
    protected void onCreate(Bundle savedInstanceState) {         
        super.onCreate(savedInstanceState);
        ...
    }        

    private void onLogout(){
        logoutUser();
    }  
}


public abstract class BaseUserActivity extends BaseActivity {
    @Inject
    UserManager userManager;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (!userManager.isLoggedIn()) {
            finishView();
        }
    }

    @Override
    protected void androidInject() {
        AndroidInjection.inject(this);
    }

    protected void logoutUser() {
        userManager.logOut();
        finishView();
    }    
}


public abstract class BaseActivity extends AppCompatActivity implements HasSupportFragmentInjector {
    @Inject
    DispatchingAndroidInjector<Fragment> injector;

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

    protected void androidInject() {
        AndroidInjection.inject(this);
    }

    public void finishView() {
        startActivity(new Intent(this, LoginActivity.class));
        finish();
    }

    @Override
    public AndroidInjector<Fragment> supportFragmentInjector() {
        return injector;
    }
}
like image 161
piveloper Avatar answered Nov 20 '22 11:11

piveloper


  1. What is a scope and what it based on? The scope is based on some android object with it's lifecycle. E.g. Application(there are default scope with annotation @Singleton already available), Activity, BroadcastReceiver, Fragment, Service, ContentProvider (this list I took from DaggerApplication). So depends on what object it will be based you'll get reusable dependency inside lifecycle of this object (it will be local singleton for it, e.g. for activity/fragment)
  2. How to make my interactor/repository to be local singleton? First of all you should choose the object with lifecycle that will be base for our scope. For instance lets choose fragment with two child fragments inside it. And I want to use the same instance of CommonInteractor inside of those two child fragments. We should create standalone Subcomponent for parent fragment. We can use @ContributesAndroidInjector for generating @Subcomponent. This subcomponent should be scoped so that's why we put @FragmentScope (created annotation by ourselves) on it. Two child fragments will belong to this subcomponent and that is the reason why we creating standalone module for child fragments and add it into generated Subcomponent by adding argument modules to @ContributesAndroidInjector of parent fragment subcomponent.

@Module(includes = [AndroidInjectionModule::class])
abstract class AppBindingModule {
    //there are a lot of other android stuff binding here
    //activities, fragments, etc.

    @FragmentScope
    @ContributesAndroidInjector(modules = [ParentFragmentModule::class])
    abstract fun bindParentFragment(): ParentFragment
}

@Module
abstract class ParentFragmentModule {
    //we should not annotate this by any scope annotation
    @ContributesAndroidInjector
    abstract fun bindFirstChildFragment(): FirstChildFragment

    @ContributesAndroidInjector
    abstract fun bindSecondChildFragment(): SecondChildFragment
}

@FragmentScope
class CommonInteractor @Inject constructor() {
    //we can inject this interactor into presenters of those two child fragments
    //it will be the same instance for both presenters
}

class FirstChildPresenter @Inject constructor(
    private val commonInteractor: CommonInteractor
) : Presenter<FirstView>()

class SecondChildPresenter @Inject constructor(
    private val commonInteractor: CommonInteractor
) : Presenter<SecondView>()

class FirstChildFragment: Fragment(), FirstView {
    @Inject
    lateinit var firstChildPresenter: FirstChildPresenter
}

class SecondChildFragment: Fragment(), SecondView {
    @Inject
    lateinit var secondChildPresenter: SecondChildPresenter
}

I omit some details like describing AppComponent and adding AppBindingModule into it and showing how to inject with AndroidSupportInjection.inject(this) cause it is out of the topic. But if clarification is necessary feel free to ask questions in comment.

like image 44
Yazon2006 Avatar answered Nov 20 '22 11:11

Yazon2006