How do you organise your Dagger 2 modules and comp

2019-01-16 08:35发布

问题:

Do you have a specific package where you put all the Dagger related classes?

Or do you put them next to the relevant class they inject, e.g. if you have an MainActivityModule and MainActivityComponent, you put them in the same package as your MainActivity.

Also, I've seen quite a few people defining components as inner classes, e.g. an ApplicationComponent that is defined inside the Application class. Do you think this is a good practice?

回答1:

EDIT: Let me start out with the fact that this is close to the truth here, but this is an antipattern as described by Martin Fowler's Data Domain Presentation Layering article HERE (CLICK THE LINK!), which specifies that you shouldn't have a MapperModule and a PresenterModule, you should have a GalleryModule and a SomeFeatureModule which has all the mappers, presenters etc. in it.

The smart route to go about it is to use component dependencies to subscope your original singleton component for each feature you have. This what I described is the "full-stack" layering, separation by features.

The one written down below is the "anti-pattern", where you cut your application's top level modules into "layers". It has numerous disadvantages to do so. Don't do it. But you can read it and learn what not to do.

ORIGINAL TEXT:

Normally, you'd use a single Component like an ApplicationComponent to contain all singleton dependencies that you use throughout the app as long as the entire application exists. You would instantiate this in your Application class, and make this accessible from elsewhere.

Project structure for me currently is:

+ injection
|- components
   |-- ApplicationComponent.java
|- modules
   |- data
      |-- DbMapperModule.java
      |-- ...
   |- domain
      |-- InteractorModule.java
      |-- ...
   |- presentation
      |-- ...
   |- utils
      |-- ...
|- scope
|- subcomponents
   |- data
      |-- ...
   |- domain
      |-- DbMapperComponent.java
      |-- ...
   |- presentation
      |-- ...
   |- utils
      |-- ...
   |-- AppContextComponent.java
   |-- AppDataComponent.java
   |-- AppDomainComponent.java
   |-- AppPresentationComponent.java
   |-- AppUtilsComponent.java

For example, mine is like this:

public enum Injector {
    INSTANCE;
    private ApplicationComponent applicationComponent;

    private Injector() {
    }

    public ApplicationComponent getApplicationComponent() {
        return applicationComponent;
    }

    ApplicationComponent initializeApplicationComponent(CustomApplication customApplication) {
        AppContextModule appContextModule = new AppContextModule(customApplication);
        RealmModule realmModule = new RealmModule(customApplication.getRealmHolder());
        applicationComponent = DaggerApplicationComponent.builder()
                .appContextModule(appContextModule)
                .realmModule(realmModule)
                .build();
        return applicationComponent;
    }
}

And you need an ApplicationComponent that can inject into whatever package-protected fields of whatever class you want to field-inject to.

@Singleton
@Component(modules = {
        AppContextModule.class,
        DbMapperModule.class,
        DbTaskModule.class,
        RealmModule.class,
        RepositoryModule.class,
        InteractorModule.class,
        ManagerModule.class,
        ServiceModule.class,
        PresenterModule.class,
        JobManagerModule.class,
        XmlPersisterModule.class
})
public interface ApplicationComponent
        extends AppContextComponent, AppDataComponent, AppDomainComponent, AppUtilsComponent, AppPresentationComponent {
    void inject(CustomApplication customApplication);

    void inject(DashboardActivity dashboardActivity);

    ...
}

For me, AppContextComponent would be a @Subcomponent, but that is not actually what it means. Those are just a way to create a subscope, and not a way to cut your component into smaller parts. So the interface I inherit is actually just a normal interface with provision methods. Same for the others.

public interface AppContextComponent {
    CustomApplication customApplication();

    Context applicationContext();

    AppConfig appConfig();

    PackageManager packageManager();

    AlarmManager alarmManager();
}

Component dependencies (which allows you to subscope, just like subcomponents) don't allow multiple scoped components, which also means your modules would be unscoped. This is due to how you cannot inherit from multiple scopes, just like you can't inherit from multiple classes in Java.

Unscoped providers make it so that the module does not retain a single instance but a new one on every inject call. To get scoped dependencies, you need to provide the scope on the module provider methods too.

@Module
public class InteractorModule {
    @Provides
    @Singleton
    public LeftNavigationDrawerInteractor leftNavigationDrawerInteractor() {
        return new LeftNavigationDrawerInteractorImpl();
    }

    ...
}

In an application, if you use Singleton components everywhere, you won't need more components, unless you create subscopes. If you want, you can even consider making your modules a complete data provider for your views and presenters.

@Component(dependencies = {ApplicationComponent.class}, modules = {DetailActivityModule.class}) 
@ActivityScope
public interface DetailActivityComponent extends ApplicationComponent {
    DataObject data();

    void inject(DetailActivity detailActivity);
}

@Module
public class DetailActivityModule {
    private String parameter;

    public DetailActivityModule(String parameter) {
        this.parameter = parameter;
    }

    @Provides
    public DataObject data(RealmHolder realmHolder) {
        Realm realm = realmHolder.getRealm();
        return realm.where(DataObject.class).equalTo("parameter", parameter).findFirst();
    }
}

Subscoping allows you to have multiple instances of your presenter, which can then store state. This makes sense in for example Mortar/Flow, where each screen has its own "path", and each path has its own component - to provide the data as a "blueprint".

public class FirstPath
        extends BasePath {
    public static final String TAG = " FirstPath";

    public final int parameter;

    public FirstPath(int parameter) {
        this.parameter = parameter;
    }

    //...

    @Override
    public int getLayout() {
        return R.layout.path_first;
    }

    @Override
    public FirstViewComponent createComponent() {
        FirstPath.FirstViewComponent firstViewComponent = DaggerFirstPath_FirstViewComponent.builder()
                .applicationComponent(InjectorService.obtain())
                .firstViewModule(new FirstPath.FirstViewModule(parameter))
                .build();
        return firstViewComponent;
    }

    @Override
    public String getScopeName() {
        return TAG + "_" + parameter;
    }

    @ViewScope //needed
    @Component(dependencies = {ApplicationComponent.class}, modules = {FirstViewModule.class})
    public interface FirstViewComponent
            extends ApplicationComponent {
        String data();

        FirstViewPresenter firstViewPresenter();

        void inject(FirstView firstView);

        void inject(FirstViewPresenter firstViewPresenter);
    }

    @Module
    public static class FirstViewModule {
        private int parameter;

        public FirstViewModule(int parameter) {
            this.parameter = parameter;
        }

        @Provides
        public String data(Context context) {
            return context.getString(parameter);
        }

        @Provides
        @ViewScope //needed to preserve scope
        public FirstViewPresenter firstViewPresenter() {
            return new FirstViewPresenter();
        }
    }

    public static class FirstViewPresenter
            extends ViewPresenter<FirstView> {
        public static final String TAG = FirstViewPresenter.class.getSimpleName();

        @Inject
        String data;

        public FirstViewPresenter() {
            Log.d(TAG, "First View Presenter created: " + toString());
        }

        @Override
        protected void onEnterScope(MortarScope scope) {
            super.onEnterScope(scope);
            FirstViewComponent firstViewComponent = scope.getService(DaggerService.TAG);
            firstViewComponent.inject(this);
            Log.d(TAG, "Data [" + data + "] and other objects injected to first presenter.");
        }

        @Override
        protected void onSave(Bundle outState) {
            super.onSave(outState);
            FirstView firstView = getView();
            outState.putString("input", firstView.getInput());
        }

        @Override
        protected void onLoad(Bundle savedInstanceState) {
            super.onLoad(savedInstanceState);
            if(!hasView()) {
                return;
            }
            FirstView firstView = getView();
            if(savedInstanceState != null) { //needed check
                firstView.setInput(savedInstanceState.getString("input"));
            }
        }

        public void goToNextActivity() {
            FirstPath firstPath = Path.get(getView().getContext());
            if(firstPath.parameter != R.string.hello_world) {
                Flow.get(getView()).set(new FirstPath(R.string.hello_world));
            } else {
                Flow.get(getView()).set(new SecondPath());
            }
        }
    }
}


回答2:

Do you have a specific package where you put all the Dagger related classes?

Or do you put them next to the relevant class they inject, e.g. if you have an MainActivityModule and MainActivityComponent, you put them in the same package as your MainActivity.

I don't have much experience with that, but I can show you my approach. Maybe some people with more experience can improve that solution or provide their point of view.

I usually organize Dagger 2 classes like that:

- di
|
+-- ApplicationComponent class
|    
+-- modules
   |
   +-- AndroidModule class
   |
   +-- WebServiceModule class
   |
   +-- ...
   |
  • di package contains classes related with Dagger 2 and dependency injection.
  • in most cases Android application usually has one Component - here it's named ApplicationComponent - I haven't created an Android application with many Dagger 2 components yet and I've seen solutions with only one component.
  • modules package contains Dagger 2 modules

I don't create module per Activity. Modules group specific functionality. E.g. elements strongly connected with system like interface for SharedPreferences, EventBus (if you are using something like that), network connectivity, etc. may be located in AndroidModule. If your project has important interfaces for WebService or there's a lot of them, you can group them in WebServiceModule. If your application is for example responsible for analyzing network and has many interfaces for similar tasks related to the network, you can group these interfaces in NetworkModule. When your application is simple, it may happen that you will have only one module. When it's complicated, you can have many modules. In my opinion, you shouldn't have many interfaces in a single module. When there's such situation you may consider splitting them into separate modules. You can also keep some business logic specific for your project in a separate module.

Also, I've seen quite a few people defining components as inner classes, e.g. an ApplicationComponent that is defined inside the Application class. Do you think this is a good practice?

I'm not sure if it's a good or bad practice. I think there's no need to do that. You can create public static get() method inside class extending Application class, which will return instance of Application as a singleton. It's much simpler solution and we should have only one instance of an Application class. If we want to to mock Context in a unit tests, we can accept Context as a parameter and in an application code, pass Application Context or Activity Context depending on the situation.

Please note, it's just my approach and some more experienced developers may organize their projects in a different and better way.