Dagger - должны ли мы создавать каждый компонент и модуль для каждого действия / фрагмента

85

Давно работаю с dagger2. И я запутался, создавая собственный компонент / модуль для каждого Activity / Fragment. Пожалуйста, помогите мне прояснить это:

Например, у нас есть приложение, в котором около 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. Создать ActivityScope:

    @Scope
    @Documented
    @Retention(value=RUNTIME)
    public @interface ActivityScope {
    }
    
  3. Создайте компонент и модуль для каждого действия. Обычно я помещаю их как статические классы в класс Activity:

    @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.
    

Это очень простые примеры, показывающие, как я бы это реализовал.

Но мой друг только что дал мне другую реализацию:

  1. Создайте PresenterModule, который предоставит всем докладчикам:

    @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. Создайте AppModule и 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, ....)
    
    }
    

Его объяснение таково: ему не нужно создавать компоненты и модули для каждого вида деятельности. Я думаю, что идея моих друзей совершенно не хороша, но, пожалуйста, поправьте меня, если я ошибаюсь. Вот причины:

  1. Очень много утечек памяти :

    • Приложение создаст 50 докладчиков, даже если у пользователя открыто только 2 действия.
    • После того, как пользователь закроет действие, его ведущий останется
  2. Что произойдет, если я захочу создать два экземпляра одного Activity? (как он может создать двух докладчиков)

  3. Инициализация приложения займет много времени (потому что ему нужно создать много презентаторов, объектов, ...)

Извините за длинный пост, но, пожалуйста, помогите мне прояснить это для меня и моего друга, я не могу его убедить. Будем очень признательны за ваши комментарии.

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

Отредактируйте после демонстрации.

Во-первых, спасибо за ответ @pandawarrior. Я должен был создать демо, прежде чем задавать этот вопрос. Я надеюсь, что мой вывод может помочь кому-то другому.

  1. То, что сделал мой друг, не вызывает утечки памяти, если он не помещает какую-либо область действия в методы Provides. (Например, @Singleton или @UserScope, ...)
  2. Мы можем создать много презентаторов, если у Provides-метода нет Scope. (Итак, мой второй пункт тоже неверен)
  3. Dagger будет создавать презентаторов только тогда, когда они нужны. (Итак, инициализация приложения не займет много времени, меня смутила Lazy Injection)

Итак, все причины, которые я сказал выше, в основном ошибочны. Но это не означает, что мы должны следовать идее моего друга по двум причинам:

  1. Это плохо для архитектуры источника, когда он помещает всех презентаторов в модуль / компонент. (Это нарушает принцип разделения интерфейса , возможно, принцип единой ответственности ).

  2. Когда мы создаем Scope Component, мы будем знать, когда он создан, а когда уничтожен, что является огромным преимуществом для предотвращения утечек памяти. Итак, для каждого действия мы должны создать компонент с @ActivityScope. Давайте представим, с реализацией моих друзей, что мы забыли добавить Scope в Provider-method => произойдут утечки памяти.

На мой взгляд, с небольшим приложением (всего несколько экранов без многих зависимостей или с аналогичными зависимостями) мы могли бы применить идею моих друзей, но, конечно, это не рекомендуется.

Предпочитаю читать дальше: Что определяет жизненный цикл компонента (графа объектов) в Dagger 2? Объем деятельности Dagger2, сколько модулей / компонентов мне нужно?

И еще одно замечание: если вы хотите увидеть, когда объект был уничтожен, вы можете вызвать методы метода вместе, и сборщик мусора запустится немедленно:

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

Если вы используете только один из этих методов, сборщик мусора запустится позже, и вы можете получить неверные результаты.

Мистер Майк
источник

Ответы:

85

Объявление отдельного модуля для каждого из них Activity- не лучшая идея. Объявление отдельного компонента для каждогоActivityЕще хуже . Причина этого очень проста - вам действительно не нужны все эти модули / компоненты (как вы уже сами убедились).

Однако наличие только одного компонента, привязанного к Applicationжизненному циклу, и его использование для внедрения во все Activities- тоже не оптимальное решение (это подход вашего друга). Это не оптимально, потому что:

  1. Он ограничивает вас только одной областью ( @Singletonили настраиваемой)
  2. Единственная область, в которой вы ограничены, делает внедренные объекты «одиночными объектами приложения», поэтому ошибки в области видимости или неправильное использование объектов с областью видимости могут легко вызвать глобальные утечки памяти.
  3. Вы также захотите использовать Dagger2 для внедрения Services, но для этого Servicesмогут потребоваться другие объекты Activities(например Services, презентаторы не нужны, их нет и FragmentManagerт. Д.). Используя один компонент, вы теряете гибкость определения различных графов объектов для разных компонентов.

Таким образом, компонент для каждого Activity- это излишество, но отдельный компонент для всего приложения недостаточно гибок. Оптимальное решение находится между этими крайностями (как обычно).

Я использую следующий подход:

  1. Единый компонент «приложения», который предоставляет «глобальные» объекты (например, объекты, которые содержат глобальное состояние, которое совместно используется всеми компонентами в приложении). Создан в Application.
  2. Подкомпонент «Контроллер» компонента «Приложение», который предоставляет объекты, которые требуются всем «контроллерам», обращенным к пользователю (в моей архитектуре это Activitiesи Fragments). Создано в каждом ActivityиFragment .
  3. Подкомпонент «Сервис» компонента «Приложение», который предоставляет необходимые всем объекты Services. Создан в каждом Service.

Ниже приведен пример того, как можно реализовать тот же подход.


Редактировать июль 2017 г.

Я опубликовал видеоурок, в котором показано, как структурировать код внедрения зависимостей Dagger в приложении Android : Android Dagger for Professionals Tutorial .


Редактировать февраль 2018

Я опубликовал полный курс по внедрению зависимостей в Android .

В этом курсе я объясняю теорию внедрения зависимостей и показываю, как она возникает естественным образом в приложении Android. Затем я демонстрирую, как конструкции Dagger вписываются в общую схему внедрения зависимостей.

Если вы пройдете этот курс, вы поймете, почему идея иметь отдельное определение модуля / компонента для каждого действия / фрагмента в корне ошибочна в самом фундаментальном смысле.

Такой подход приводит к тому, что структура уровня представления из «Функционального» набора классов зеркально отражается в структуре «Конструкционного» набора классов, таким образом связывая их вместе. Это идет вразрез с основной целью внедрения зависимостей, которая состоит в том, чтобы не пересекать «Конструктивный» и «Функциональный» наборы классов.


Область применения:

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

Объем контроллера:

@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)
}

А потом в 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;
    }
}

Дополнительная информация о внедрении зависимостей:

Демистификация Dagger 2 Scopes

Внедрение зависимостей в Android

Василий
источник
1
Спасибо @vasiliy за то, что поделился своим мнением. Именно так я бы использовал это и в настоящее время следую стратегии. В случае шаблона MVP упомянутый ControllerModuleсоздаст новый, Presenterа затем презентатор будет введен в файл Activityили Fragment. Есть твердое мнение за или против?
Вахиб Уль Хак,
@Vasiliy, я прочитал всю вашу статью и обнаружил, что, возможно, вы не рассматривали интеракторов и презентаторов в механизме. Будет ли ControllerModule обеспечивать все зависимости взаимодействующих и презентаторов ? Дайте небольшую подсказку на случай, если я что-то пропустил.
iamcrypticcoder
@ mahbub.kuet, если я понимаю, что вы имеете в виду под "взаимодействующими" и "ведущими", ControllerComponentдолжен их ввести. Проводите ли вы их внутри ControllerModuleили вводите дополнительный модуль, зависит от вас. В реальных приложениях я советую использовать многокомпонентный подход вместо того, чтобы помещать все в один модуль. Вот пример ApplicationComponent, но контроллер будет таким же: github.com/techyourchance/idocare-android/tree/master/app/src/…
Василий
2
@ Mr.Hyde, в общем да, но тогда вам придется явно объявлять во ApplicationComponentвсех зависимостях, которые ControllerComponentможно использовать. Также количество методов сгенерированного кода будет выше. Я пока не нашел веской причины использовать зависимые компоненты.
Василий
1
Я использую этот подход во всех своих проектах сегодня, и я явно не использую ничего из dagger.androidпакета, потому что считаю это плохо мотивированным. Таким образом, этот пример все еще очень актуален и по-прежнему является лучшим способом сделать DI в Android IMHO.
Василий
15

Некоторые из лучших примеров организации ваших компонентов, модулей и пакетов можно найти в репозитории Google Android Architecture Blueprints на Github здесь .

Если вы изучите исходный код там, вы увидите, что есть один компонент с областью действия приложения (с жизненным циклом, равным продолжительности всего приложения), а затем отдельные компоненты с областью действия для действия и фрагмента, соответствующие данной функциональности в проект. Например, есть следующие пакеты:

addedittask
taskdetail
tasks

Внутри каждого пакета есть модуль, компонент, презентатор и т.д. Например, внутри taskdetailесть следующие классы:

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

Преимущество такой организации (вместо группирования всех действий в один компонент или модуль) состоит в том, что вы можете воспользоваться модификаторами доступности Java и выполнить пункт 13 Эффективного Java. Другими словами, функционально сгруппированные классы будут в одном и том же package, и вы можете воспользоваться модификаторамиprotected и package-private модификаторами доступности, чтобы предотвратить непреднамеренное использование ваших классов.

Дэвид Роусон
источник
1
это тоже мой предпочтительный подход. Мне не нравятся действия / фрагменты, имеющие доступ к тому, что им не разрешено.
Жоао Соуза
3

Первый вариант создает компонент с подзаданной областью для каждого действия, где действие может создавать компоненты с подобластью, которые предоставляют только зависимость (презентатора) для этого конкретного действия.

Второй вариант создает один @Singletonкомпонент, который может предоставлять презентаторов как зависимости с незаданной областью, то есть при доступе к ним вы каждый раз создаете новый экземпляр презентатора. (Нет, он не создает новый экземпляр, пока вы его не запросите).


Технически ни один из подходов не хуже другого. Первый подход разделяет докладчиков не по функциям, а по уровням.

Я использовал оба, они оба работают и оба имеют смысл.

Единственный недостаток первого решения (если вы используете @Component(dependencies={...}вместо @Subcomponent) заключается в том, что вам нужно убедиться, что это не Activity, который создает свой собственный модуль внутри, потому что тогда вы не можете заменить реализации метода модуля на mocks. Опять же, если вы используете инъекцию конструктора вместо инъекции поля, вы можете просто создать класс напрямую с помощью конструктора, напрямую давая ему макеты.

EpicPandaForce
источник
1

Используйте Provider<"your component's name">вместо простых компонентов реализацию, чтобы избежать утечек памяти и создания множества бесполезных компонентов. Поэтому ваши компоненты будут созданы ленивым методом при вызове метода get (), поскольку вместо этого вы предоставляете не экземпляр компонента, а просто поставщик. Таким образом, ваш презентатор будет применен, если был вызван .get () провайдера. Прочтите о Провайдере здесь и примените его. ( Официальная документация Dagger )


И еще один отличный способ - использовать множественное связывание. В соответствии с ним вы должны привязать своих докладчиков к карте и создавать их через провайдеров, когда вам нужно. ( вот документы о мультибиндинге )

Константин Левицкий
источник
-5

Ваш друг прав, вам действительно не нужно создавать компоненты и модули для каждой деятельности. Предполагается, что Dagger поможет вам уменьшить беспорядочный код и сделать ваши действия Android более чистыми, делегируя экземпляры классов модулям вместо того, чтобы создавать их экземпляры в методе onCreate Activity.

Обычно мы делаем так

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.
}

}

Вы делаете это вместо

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

Так что писать слишком много вещей, чтобы победить цель кинжала, нет? Я скорее создаю своих докладчиков в Activity, если мне нужно создавать модули и компоненты для каждого Activity.

Что касается ваших вопросов о:

1- Утечка памяти:

Нет, если вы не добавите @Singletonаннотацию к докладчикам, которых вы предоставляете. Dagger будет создавать объект только тогда, когда вы выполняете @Injectоперацию в целевом классе`. Других докладчиков в вашем сценарии не будет. Вы можете попробовать использовать журнал, чтобы узнать, созданы они или нет.

@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- Вы вводите дважды и регистрируете их хэш-код

//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. Нет, объекты будут созданы только при входе @Injectв действия, а не при инициализации приложения.

Лью Джун Тунг
источник
1
Спасибо за комментарий, то, что вы сказали, не является неправильным, но я думаю, что это не лучший ответ, пожалуйста, посмотрите мое сообщение редактирования. Таким образом, не удалось отметить его как принятый.
Мистер Майк,
@EpicPandaForce: Эх, но вы должны где-нибудь создать его экземпляр. Что-то должно будет нарушить принцип инверсии зависимостей.
Дэвид Лю