Кинджал - чи слід створювати кожен компонент та модуль для кожного виду діяльності / фрагмента


85

Я деякий час працюю з 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. Створити 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. Що станеться, якщо я хочу створити два екземпляри одного Діяльності? (як він може створити двох ведучих)

  3. На ініціалізацію програми знадобиться багато часу (оскільки вона повинна створити багато презентаторів, об’єктів, ...)

Вибачте за довгий пост, але, будь ласка, допоможіть мені пояснити це мені та моєму другові, я не можу переконати його. Ваші коментарі будуть дуже вдячні.

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

Редагувати після демонстрації.

По-перше, дякую за відповідь @pandawarrior. Я повинен був створити демонстраційну програму, перш ніж задати це запитання. Сподіваюсь, мій висновок тут може допомогти комусь іншому.

  1. Те, що зробив мій друг, не спричиняє витоків пам’яті, якщо він не додає жодних областей застосування до методів Provides. (Наприклад, @Singleton, або @UserScope, ...)
  2. Ми можемо створити багато доповідачів, якщо метод Provides не має жодної сфери дії. (Отже, моя друга думка теж невірна)
  3. Dagger створюватиме ведучих лише тоді, коли вони потрібні. (Отже, програма не займе багато часу для ініціалізації, мене спантеличила Ледача ін’єкція)

Отже, усі причини, про які я говорив вище, здебільшого помилкові. Але це не означає, що нам слід наслідувати ідею мого друга з двох причин:

  1. Це не добре для архітектури джерела, коли він запускає всіх доповідачів у модулі / компоненті. (Це порушує принцип розділення інтерфейсу , можливо , також принцип єдиної відповідальності ).

  2. Коли ми створюємо компонент Scope, ми будемо знати, коли він створений і коли він знищений, що є величезною перевагою для уникнення витоків пам'яті. Отже, для кожної діяльності ми повинні створити Компонент із @ActivityScope. Давайте уявимо, з реалізацією моїх друзів, що ми забули ввести деякий обсяг у метод Provider => відбуватимуться витоки пам'яті.

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

Краще читайте далі: Що визначає життєвий цикл компонента (графік об’єктів) у Dagger 2? Сфера діяльності Dagger2, скільки модулів / компонентів мені потрібно?

І ще одне зауваження: якщо ви хочете побачити, коли об’єкт буде знищений, ви можете викликати методи методу, і GC запуститься негайно:

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

Якщо ви використовуєте лише один із цих методів, 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 .


Редагувати лютий 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;
    }
}

Додаткова інформація про введення залежності:

Кинджал 2 Сфери дії Демістифіковані

Ін’єкція залежності в 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 Архітектура Blueprints Github репо тут .

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

addedittask
taskdetail
tasks

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

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

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


1
це також мій кращий підхід. Мені не подобається діяльність / фрагменти, які мають доступ до речей, які вони не повинні робити.
Joao Sousa

3

Перший варіант створює підкопійований компонент для кожної діяльності, де дія може створювати підкопійовані компоненти, які забезпечують лише залежність (презентатор) для цієї конкретної діяльності.

Другий варіант створює єдиний @Singletonкомпонент, який може надавати доповідачам як нерозміщені залежності, тобто, коли ви отримуєте до них доступ, ви кожного разу створюєте новий екземпляр презентатора. (Ні, він не створює новий екземпляр, доки ви його не запитаєте).


Технічно жоден з підходів не гірший за інший. Перший підхід не розділяє доповідачів за ознаками, а за шаром.

Я використовував обидва, вони обидва працюють, і обидва мають сенс.

Єдиним недоліком першого рішення (якщо ви використовуєте @Component(dependencies={...}замість нього @Subcomponent) є те, що вам потрібно переконатися, що це не Activity, яка створює власний модуль внутрішньо, оскільки тоді ви не можете замінити реалізації методів модулів на макети. Знову ж таки, якщо ви використовуєте введення конструктора замість введення поля, ви можете просто створити клас безпосередньо за допомогою конструктора, безпосередньо даючи йому знущання.


1

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


І ще один чудовий спосіб - це використання багатозв’язкового. Відповідно до цього ви повинні прив’язати своїх ведучих до карти і створювати їх через провайдерів, коли вам це потрібно. ( ось документи про багатозв’язування )


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

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

Щодо ваших запитань щодо:

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до активності, замість програми init.


1
Дякую за коментар, сказане Вами не є помилковим, але я думаю, що це не найкраща відповідь, будь ласка, перегляньте мій пост редагування. Отже, не міг позначити це як прийнятий.
Містер Майк

@EpicPandaForce: Е, але ти мусиш десь створити екземпляр. Щось доведеться порушити принцип інверсії залежності.
Девід Лю
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.