匕首-我们是否应该为每个活动/片段创建每个组件和模块

我和 Dagger2共事有一段时间了。我不知道是否应该为每个活动/片段创建一个自己的组件/模块。请帮我澄清一下:

例如,我们有一个应用程序,这个应用程序有大约50个屏幕。 我们将按照 MVP 模式和 Dagger2实现 DI 的代码。假设我们有50个活动和50个演讲者。

在我看来,通常我们应该这样组织代码:

  1. 创建一个 AppComponent 和 AppModule,它将提供在应用程序打开时使用的所有对象。

    @Module
    public class AppModule {
    
    
    private final MyApplicationClass application;
    
    
    public AppModule(MyApplicationClass application) {
    this.application = application;
    }
    
    
    @Provides
    @Singleton
    Context provideApplicationContext() {
    return this.application;
    }
    
    
    //... and many other providers
    
    
    }
    
    
    @Singleton
    @Component( modules = { AppModule.class } )
    public interface AppComponent {
    
    
    Context getAppContext();
    
    
    Activity1Component plus(Activity1Module module);
    Activity2Component plus(Activity2Module module);
    
    
    //... plus 48 methods for 48 other activities. Suppose that we don't have any other Scope (like UserScope after user login, ....)
    
    
    }
    
  2. Create ActivityScope :

    @Scope
    @Documented
    @Retention(value=RUNTIME)
    public @interface ActivityScope {
    }
    
  3. Create Component and Module for each Activity. Usually I will put them as static classes within the Activity class:

    @Module
    public class Activity1Module {
    
    
    public LoginModule() {
    }
    @Provides
    @ActivityScope
    Activity1Presenter provideActivity1Presenter(Context context, /*...some other params*/){
    return new Activity1PresenterImpl(context, /*...some other params*/);
    }
    
    
    }
    
    
    @ActivityScope
    @Subcomponent( modules = { Activity1Module.class } )
    public interface Activity1Component {
    void inject(Activity1 activity); // inject Presenter to the Activity
    }
    
    
    // .... Same with 49 remaining modules and components.
    

Those are just very simple examples to show how I would implement this.

But a friend of mine just gave me another implementation:

  1. Create PresenterModule which will provide all presenters:

    @Module
    public class AppPresenterModule {
    
    
    @Provides
    Activity1Presenter provideActivity1Presentor(Context context, /*...some other params*/){
    return new Activity1PresenterImpl(context, /*...some other params*/);
    }
    
    
    @Provides
    Activity2Presenter provideActivity2Presentor(Context context, /*...some other params*/){
    return new Activity2PresenterImpl(context, /*...some other params*/);
    }
    
    
    //... same with 48 other presenters.
    
    
    }
    
  2. Create AppModule and AppComponent:

    @Module
    public class AppModule {
    
    
    private final MyApplicationClass application;
    
    
    public AppModule(MyApplicationClass application) {
    this.application = application;
    }
    
    
    @Provides
    @Singleton
    Context provideApplicationContext() {
    return this.application;
    }
    
    
    //... and many other provides
    
    
    }
    
    
    @Singleton
    @Component(
    modules = { AppModule.class,  AppPresenterModule.class }
    )
    public interface AppComponent {
    
    
    Context getAppContext();
    
    
    public void inject(Activity1 activity);
    public void inject(Activity2 activity);
    
    
    //... and 48 other methods for 48 other activities. Suppose that we don't have any other Scope (like UserScope after user login, ....)
    
    
    }
    

His explaination is: He doesn't have to create components and modules for each activity. I think my friends idea is absolutely not good at all, but please correct me if I am wrong. Here are the reasons:

  1. A lot of memory leaks :

    • The app will create 50 presenters even if the user has only 2 Activities open.
    • After the user closes an Activity, its presenter will still remain
  2. What happens if I want to create two instances of one Activity ? (how can he create two presenters )

  3. It will take a lot of time for the app to initialize (because it has to create many presenters, objects, ...)

Sorry for a long post, but please help me clarify this for me and my friend, I can't convince him. Your comments will be very appreciated.

/-----------------------------------------------------------------------/

Edit after doing a demo.

First, thanks for @pandawarrior answer. I should have created a Demo before I asked this question. I hope my conclusion here could help someone else.

  1. What my friend has done does not cause memory leaks unless he puts any Scope to the Provides-methods. (For example @Singleton, or @UserScope, ...)
  2. We can create many presenters, if the Provides-method doesn't have any Scope. (So, my second point is wrong, too)
  3. Dagger will create the presenters only when they are needed. (So, the app will not take a long time to initialize, I was confused by Lazy Injection)

So, all the reasons I have said above are mostly wrong. But it does not mean that we should follow my friend idea, for two reasons:

  1. It's not good for the source's architecture, when he inits all presenters in module / component. (It violates Interface segregation principle, maybe Single Responsibility priciple, too).

  2. When we create a Scope Component, we will know when it's created and when it's destroyed which is a huge benefit for avoiding memory leaks. So, for each Activity we should create a Component with an @ActivityScope. Let's imagine, with my friends implementation, that we forgot to put some Scope in the Provider-method => memory leaks will occur.

In my opinion, with a small app (just a few screens without many dependencies or with similar dependencies), we could apply my friends idea, but of course it's not recommended.

Prefer to read more on: What determines the lifecycle of a component (object graph) in Dagger 2? Dagger2 activity scope, how many modules/components do i need?

And one more note: If you want to see when the object are destroyed, you can call those of method together and the GC will run immediately:

    System.runFinalization();
System.gc();

如果你只使用这些方法中的一种,GC 会运行得更晚,你可能会得到错误的结果。

27280 次浏览

Your friend is correct, you don't really have to create components and modules for every activities. Dagger is supposed to help you reduce messy code and makes your Android activities cleaner by delegating class instantiations to the Modules instead of instantiates them in Activities' onCreate method.

Normally we'll do like this

public class MainActivity extends AppCompatActivity {




Presenter1 mPresenter1;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPresenter1 = new Presenter1(); // you instantiate mPresentation1 in onCreate, imagine if there are 5, 10, 20... of objects for you to instantiate.
}


}

You do this instead

public class MainActivity extends AppCompatActivity {


@Inject
Presenter1 mPresenter1; // the Dagger module take cares of instantiation for your


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
injectThisActivity();
}


private void injectThisActivity() {
MainApplication.get(this)
.getMainComponent()
.inject(this);
}}

So writing too many things kind of defeat the purpose of dagger no? I rather instantiate my presenters in Activities if I have to create Modules and Components for every Activities.

As for your questions about:

1- Memory leak:

No, not unless you put a @Singleton annotation to the presenters you providing. Dagger will only create the object whenever you do an @Inject in the target class`. It won't create the other presenters in your scenario. You can try to use Log to see if they are created or not.

@Module
public class AppPresenterModule {


@Provides
@Singleton // <-- this will persists throughout the application, too many of these is not good
Activity1Presenter provideActivity1Presentor(Context context, ...some other params){
Log.d("Activity1Presenter", "Activity1Presenter initiated");
return new Activity1PresenterImpl(context, ...some other params);
}


@Provides // Activity2Presenter will be provided every time you @Inject into the activity
Activity2Presenter provideActivity2Presentor(Context context, ...some other params){
Log.d("Activity2Presenter", "Activity2Presenter initiated");
return new Activity2PresenterImpl(context, ...some other params);
}


.... Same with 48 others presenters.

}

2- You inject twice and log their hash code

//MainActivity.java
@Inject Activity1Presenter mPresentation1
@Inject Activity1Presenter mPresentation2


@Inject Activity2Presenter mPresentation3
@Inject Activity2Presenter mPresentation4
//log will show Presentation2 being initiated twice


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
injectThisActivity();
Log.d("Activity1Presenter1", mPresentation1.hashCode());
Log.d("Activity1Presenter2", mPresentation2.hashCode());
//it will shows that both have same hash, it's a Singleton
Log.d("Activity2Presenter1", mPresentation3.hashCode());
Log.d("Activity2Presenter2", mPresentation4.hashCode());
//it will shows that both have different hash, hence different objects

3. No, the objects will only be created when you @Inject in to the activities, instead of the app init.

Declaring a separate module for each Activity is not a good idea at all. Declaring separate component for each Activity is even worse. The reasoning behind this is very simple - you don't really need all these module/components (as you have already seen by yourself).

However, having just one component that is tied to Application's life-cycle and using it for injection into all Activities is also not the optimal solution (this is your friend's approach). It is not optimal because:

  1. It restricts you to just one scope (@Singleton or a custom one)
  2. The only scope you're restricted to makes the injected objects "application singletons", therefore mistakes in scoping or incorrect usage of scoped objects can easily cause global memory leaks
  3. You'll want to use Dagger2 in order to inject into Services too, but Services can require different objects than Activities (e.g. Services don't need presenters, don't have FragmentManager, etc.). By using a single component you loose the flexibility of defining different object graphs for different components.

So, a component per Activity is an overkill, but single component for the entire application is not flexible enough. The optimal solution is in between these extremes (as it usually is).

I use the following approach:

  1. Single "application" component that provides "global" objects (e.g. objects that hold global state which is shared between all components in the application). Instantiated in Application.
  2. "Controller" subcomponent of "application" component that provides objects which are required by all user-facing "controllers" (in my architecture these are Activities and Fragments). Instantiated in each Activity and Fragment.
  3. "Service" subcomponent of "application" component that provides objects which are required by all Services. Instantiated in each Service.

Following is an example of how you could implement the same approach.


Edit July 2017

I published a video tutorial that shows how to structure Dagger dependency injection code in Android application: Android Dagger for Professionals Tutorial.


Edit February 2018

I published a complete course about dependency injection in Android.

In this course I explain the theory of dependency injection and show how it emerges naturally in Android application. Then I demonstrate how Dagger constructs fit into the general dependency injection scheme.

If you take this course you will understand why the idea of having a separate definition of module/component for each Activity/Fragment is basically flawed in the most fundamental way.

Such an approach causes the structure of presentation layer from "Functional" set of classes to be mirrored into the structure of "Construction" set of classes, thus coupling them together. This goes against the main objective of dependency injection which is to keep the "Construction" and "Functional" sets of classes disjoint.


Application scope:

@ApplicationScope
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {


// Each subcomponent can depend on more than one module
ControllerComponent newControllerComponent(ControllerModule module);
ServiceComponent newServiceComponent(ServiceModule module);


}




@Module
public class ApplicationModule {


private final Application mApplication;


public ApplicationModule(Application application) {
mApplication = application;
}


@Provides
@ApplicationScope
Application applicationContext() {
return mApplication;
}


@Provides
@ApplicationScope
SharedPreferences sharedPreferences() {
return mApplication.getSharedPreferences(Constants.PREFERENCES_FILE, Context.MODE_PRIVATE);
}


@Provides
@ApplicationScope
SettingsManager settingsManager(SharedPreferences sharedPreferences) {
return new SettingsManager(sharedPreferences);
}
}

Controller scope:

@ControllerScope
@Subcomponent(modules = {ControllerModule.class})
public interface ControllerComponent {


void inject(CustomActivity customActivity); // add more activities if needed


void inject(CustomFragment customFragment); // add more fragments if needed


void inject(CustomDialogFragment customDialogFragment); // add more dialogs if needed


}






@Module
public class ControllerModule {


private Activity mActivity;
private FragmentManager mFragmentManager;


public ControllerModule(Activity activity, FragmentManager fragmentManager) {
mActivity = activity;
mFragmentManager = fragmentManager;
}


@Provides
@ControllerScope
Context context() {
return mActivity;
}


@Provides
@ControllerScope
Activity activity() {
return mActivity;
}


@Provides
@ControllerScope
DialogsManager dialogsManager(FragmentManager fragmentManager) {
return new DialogsManager(fragmentManager);
}


// @Provides for presenters can be declared here, or in a standalone PresentersModule (which is better)
}

And then in Activity:

public class CustomActivity extends AppCompatActivity {


@Inject DialogsManager mDialogsManager;


private ControllerComponent mControllerComponent;


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

}


private ControllerComponent getControllerComponent() {
if (mControllerComponent == null) {


mControllerComponent = ((MyApplication)getApplication()).getApplicationComponent()
.newControllerComponent(new ControllerModule(this, getSupportFragmentManager()));
}
        

return mControllerComponent;
}
}

Additional information on dependency injection:

Dagger 2 Scopes Demystified

Dependency Injection in Android

First option creates a subscoped component for each activity, where the activity is able to create subscoped components that only provide the dependency (presenter) for that particular activity.

Second option creates a single @Singleton component that is able to provide the presenters as unscoped dependencies, meaning when you access them, you create a new instance of the presenter each time. (No, it doesn't create a new instance until you request one).


Technically, neither approach is worse than the other. The first approach doesn't separate presenters by feature, but by layer.

I've used both, they both work and both make sense.

The only disadvantage of the first solution (if you're using @Component(dependencies={...} instead of @Subcomponent) is that you need to make sure it's not the Activity that creates its own module internally, because then you cannot replace module method implementations with mocks. Then again, if you use constructor injection instead of field injection, you can just create the class directly with constructor, directly giving it mocks.

Some of the best examples of how to organise your components, modules, and packages can be found in the Google Android Architecture Blueprints Github repo here.

If you examine the source code there, you can see there is one single app-scoped Component (with a lifecycle of the duration of the whole app) and then separate Activity-scoped Components for the Activity and Fragment corresponding to a given functionality in a project. For example, there are the following packages:

addedittask
taskdetail
tasks

Inside each package there is a module, component, presenter etc. For instance, inside taskdetail there are the following classes:

TaskDetailActivity.java
TaskDetailComponent.java
TaskDetailContract.java
TaskDetailFragment.java
TaskDetailPresenter.java
TaskDetailPresenterModule.java

The advantage of organising this way (rather than grouping all of the activities in one component or module) is that you can take advantage of Java accessibility modifiers and fulfil Effective Java item 13. In other words, the functionally grouped classes will be in the same package and you can take advantage of protected and package-private accessibility modifiers to prevent unintended usages of your classes.

Use Provider<"your component's name"> instead of simple components implementation to avoid memory leaks and creating tons of useless components. Therefore your components will be created by lazy when you call get() method since you don't provide an instance of component but just provider instead. Thus your presenter is gonna be applied if .get() of provider was called. Read about Provider here and apply this. (Official Dagger documentation)


And other great way is to use multibinding. In accordance with it you should bind your presenters into map and create them through providers when you need. (here is docs about multibinding)