Sztylet - czy powinniśmy stworzyć każdy komponent i moduł dla każdego Działania / Fragmentu


85

Od jakiegoś czasu pracuję z dagger2. I pomyliłem się, tworząc własny komponent / moduł dla każdego działania / fragmentu. Pomóż mi to wyjaśnić:

Na przykład mamy aplikację, która ma około 50 ekranów. Zaimplementujemy kod według wzorca MVP i Dagger2 dla DI. Załóżmy, że mamy 50 zajęć i 50 prezenterów.

Moim zdaniem zwykle powinniśmy zorganizować kod w ten sposób:

  1. Utwórz AppComponent i AppModule, które udostępnią wszystkie obiekty, które będą używane, gdy aplikacja jest otwarta.

    @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. Utwórz zakres działania:

    @Scope
    @Documented
    @Retention(value=RUNTIME)
    public @interface ActivityScope {
    }
    
  3. Utwórz komponent i moduł dla każdego działania. Zazwyczaj umieszczam je jako klasy statyczne w klasie 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.
    

To tylko bardzo proste przykłady, które pokazują, jak bym to zaimplementował.

Ale mój przyjaciel właśnie dał mi inną implementację:

  1. Utwórz PresenterModule, który zapewni wszystkim prezenterom:

    @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. Utwórz AppModule i 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, ....)
    
    }
    

Jego wyjaśnienie brzmi: nie musi tworzyć komponentów i modułów dla każdej czynności. Myślę, że pomysł moich przyjaciół absolutnie nie jest dobry, ale proszę mnie poprawić, jeśli się mylę. Oto powody:

  1. Wiele wycieków pamięci :

    • Aplikacja utworzy 50 osób prowadzących, nawet jeśli użytkownik ma otwarte tylko 2 działania.
    • Po zamknięciu działania przez użytkownika jego osoba prowadząca pozostanie
  2. Co się stanie, jeśli chcę utworzyć dwa wystąpienia jednego działania? (jak może stworzyć dwóch prezenterów)

  3. Zainicjowanie aplikacji zajmie dużo czasu (ponieważ musi utworzyć wiele osób prowadzących, obiektów itp.)

Przepraszam za długi post, ale pomóż mi to wyjaśnić dla mnie i mojego przyjaciela, nie mogę go przekonać. Twoje komentarze będą bardzo mile widziane.

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

Edytuj po wykonaniu demonstracji.

Po pierwsze, dziękuję za odpowiedź @pandawarrior. Powinienem był stworzyć Demo, zanim zadałem to pytanie. Mam nadzieję, że mój wniosek może pomóc komuś innemu.

  1. To, co zrobił mój przyjaciel, nie powoduje wycieków pamięci, chyba że doda zakres do metod Provides. (Na przykład @Singleton lub @UserScope, ...)
  2. Możemy stworzyć wielu prezenterów, jeśli metoda Provides nie ma żadnego zakresu. (Więc moja druga uwaga też jest błędna)
  3. Sztylet stworzy prezenterów tylko wtedy, gdy będą potrzebni. (Tak więc inicjalizacja aplikacji nie zajmie dużo czasu, byłem zdezorientowany przez Lazy Injection)

Zatem wszystkie powody, które powiedziałem powyżej, są w większości błędne. Ale to nie znaczy, że powinniśmy podążać za moim przyjacielem z dwóch powodów:

  1. Nie jest to dobre dla architektury źródła, kiedy on inicjuje wszystkich prezenterów w module / komponencie. (Narusza to zasadę segregacji interfejsów , być może także zasadę pojedynczej odpowiedzialności ).

  2. Kiedy tworzymy komponent zakresu, będziemy wiedzieć, kiedy zostanie utworzony i kiedy zostanie zniszczony, co jest ogromną korzyścią dla uniknięcia wycieków pamięci. Dlatego dla każdego działania powinniśmy utworzyć komponent z @ActivityScope. Wyobraźmy sobie, że z implementacją moich znajomych zapomnieliśmy umieścić jakiś zakres w metodzie Provider => wystąpią wycieki pamięci.

Moim zdaniem przy małej aplikacji (zaledwie kilka ekranów bez wielu zależności lub z podobnymi zależnościami) moglibyśmy zastosować pomysł znajomych, ale oczywiście nie jest to zalecane.

Wolisz przeczytać więcej na temat: Co decyduje o cyklu życia komponentu (grafu obiektowego) w Dagger 2? Zakres działalności Dagger2, ile modułów / komponentów potrzebuję?

I jeszcze jedna uwaga: jeśli chcesz zobaczyć, kiedy obiekt zostanie zniszczony, możesz wywołać te z metody razem, a GC uruchomi się natychmiast:

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

Jeśli użyjesz tylko jednej z tych metod, GC uruchomi się później i możesz uzyskać nieprawidłowe wyniki.

Odpowiedzi:


85

Zadeklarowanie osobnego modułu dla każdego Activitynie jest dobrym pomysłem. Zadeklarowanie osobnego komponentu dla każdego Activityjest jeszcze gorsze. Przyczyna tego jest bardzo prosta - tak naprawdę nie potrzebujesz wszystkich tych modułów / komponentów (jak już sam się przekonałeś).

Jednak posiadanie tylko jednego składnika, który jest powiązany z Applicationcyklem życia i używanie go do wstrzykiwania do wszystkich, Activitiesrównież nie jest optymalnym rozwiązaniem (takie jest podejście twojego przyjaciela). Nie jest optymalne, ponieważ:

  1. Ogranicza Cię do tylko jednego zakresu ( @Singletonlub niestandardowego)
  2. Jedyny zakres, do którego jesteś ograniczony, sprawia, że ​​wstrzyknięte obiekty są „pojedynczymi aplikacjami”, dlatego błędy w określaniu zakresu lub nieprawidłowe użycie obiektów o określonym zakresie mogą łatwo spowodować globalne wycieki pamięci
  3. Będziesz chciał użyć Dagger2, aby wstrzyknąć Servicesrównież do niego , ale Servicesmoże wymagać innych obiektów niż Activities(np. ServicesNie potrzebujesz prezenterów, nie masz FragmentManageritp.). Używając pojedynczego komponentu, tracisz elastyczność definiowania różnych wykresów obiektów dla różnych komponentów.

Tak więc jeden składnik Activityto przesada, ale pojedynczy składnik dla całej aplikacji nie jest wystarczająco elastyczny. Optymalne rozwiązanie znajduje się pomiędzy tymi skrajnościami (jak to zwykle bywa).

Stosuję następujące podejście:

  1. Pojedynczy komponent „aplikacji” udostępniający obiekty „globalne” (np. Obiekty posiadające stan globalny, który jest współdzielony między wszystkimi komponentami w aplikacji). Utworzono w Application.
  2. Podkomponent „Kontroler” komponentu „aplikacja”, który udostępnia obiekty wymagane przez wszystkie „kontrolery” dostępne dla użytkownika (w mojej architekturze są to Activitiesi Fragments). Utworzone w każdym Activityi Fragment.
  3. Podkomponent „Usługa” komponentu „aplikacja”, który udostępnia obiekty wymagane przez wszystkich Services. Występuje w każdym Service.

Poniżej znajduje się przykład tego, jak można zastosować to samo podejście.


Edytuj lipiec 2017

Opublikowałem samouczek wideo, który pokazuje, jak ustrukturyzować kod iniekcji zależności Dagger w aplikacji na Androida : Samouczek Android Dagger for Professionals .


Edytuj luty 2018

Opublikowałem kompletny kurs o wstrzykiwaniu zależności w systemie Android .

Na tym kursie wyjaśniam teorię wstrzykiwania zależności i pokazuję, jak wygląda ona naturalnie w aplikacji na Androida. Następnie pokazuję, jak konstrukcje Daggera pasują do ogólnego schematu wstrzykiwania zależności.

Jeśli weźmiesz udział w tym kursie, zrozumiesz, dlaczego pomysł posiadania oddzielnej definicji modułu / komponentu dla każdego działania / fragmentu jest w zasadzie błędny w najbardziej fundamentalny sposób.

Takie podejście powoduje, że struktura warstwy prezentacji ze zbioru klas „Funkcjonalnych” zostaje odzwierciedlona w strukturze zbioru klas „Konstrukcja”, łącząc je w ten sposób. Jest to sprzeczne z głównym celem wstrzykiwania zależności, którym jest rozłączenie zestawów klas „Konstrukcja” i „Funkcjonalność”.


Zakres zastosowania:

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

Zakres kontrolera:

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

A potem w 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;
    }
}

Dodatkowe informacje o wstrzykiwaniu zależności:

Dagger 2 Scopes Demystified

Dependency Injection w systemie Android


1
Dzięki @vasiliy za podzielenie się swoją opinią. Dokładnie tak bym go użył i obecnie stosuję strategię. W przypadku wzorca MVP, skierowany ControllerModuleutworzy nowy, Presentera następnie prezenter zostanie wstrzyknięty w Activitylub Fragment. Masz jakąś solidną opinię za lub przeciw?
Wahib Ul Haq

@ Vasiliy, przeczytałem cały twój artykuł i stwierdziłem, że być może nie bierzesz pod uwagę interaktorów i prezenterów w mechanizmie. Czy ControllerModule zapewni wszystkie zależności między interaktorami i prezenterami ? Proszę o małą wskazówkę na wypadek, gdyby coś mi umknęło.
iamcrypticcoder

@ mahbub.kuet, jeśli rozumiem, o czym mówisz przez „interactors” i „prezenter”, ControllerComponentpowinien je wstrzyknąć. Od Ciebie ControllerModulezależy, czy podłączysz je do środka , czy wprowadzisz dodatkowy moduł. W prawdziwych aplikacjach radzę stosować podejście wielomodułowe na komponent zamiast umieszczać wszystko w jednym module. Oto przykład dla ApplicationComponent, ale kontroler będzie taki sam: github.com/techyourchance/idocare-android/tree/master/app/src/…
Wasilij

2
@ Panie Hyde, generalnie tak, ale wtedy będziesz musiał jawnie zadeklarować we ApplicationComponentwszystkich zależnościach, które ControllerComponentmogą używać. Również liczba metod wygenerowanego kodu będzie wyższa. Nie znalazłem jeszcze dobrego powodu, aby używać komponentów zależnych.
Wasilij

1
Używam tego podejścia we wszystkich moich dzisiejszych projektach i wyraźnie nie używam niczego z dagger.androidpakietu, ponieważ uważam, że jest to źle zmotywowane. Dlatego ten przykład jest nadal bardzo aktualny i nadal jest najlepszym sposobem na zrobienie DI w Android IMHO.
Wasilij

15

Niektóre z najlepszych przykładów organizacji komponentów, modułów i pakietów można znaleźć w repozytorium Google Android Architecture Blueprints Github tutaj .

Jeśli przejrzysz tam kod źródłowy, zobaczysz, że istnieje jeden składnik o zakresie aplikacji (z cyklem życia całej aplikacji), a następnie oddzielne składniki o zakresie działania dla działania i fragmentu odpowiadające danej funkcji w projekt. Na przykład istnieją następujące pakiety:

addedittask
taskdetail
tasks

W każdym pakiecie znajduje się moduł, komponent, prezenter itp. Na przykład wewnątrz taskdetailznajdują się następujące klasy:

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

Zaletą takiego zorganizowania (zamiast grupowania wszystkich działań w jednym komponencie lub module) jest to, że można skorzystać z modyfikatorów dostępności Java i wypełnić Efektywny element Java 13. Innymi słowy, funkcjonalnie zgrupowane klasy będą w tym samym pakiet można wykorzystać protectedi package-private modyfikatorów dostępu , aby zapobiec niezamierzonym zwyczaje swoich klasach.


1
jest to również moje preferowane podejście. Nie lubię działań / fragmentów, które mają dostęp do rzeczy, do których nie powinny.
Joao Sousa

3

Pierwsza opcja tworzy komponent o obniżonym zakresie dla każdego działania, przy czym działanie to może tworzyć komponenty o obniżonym zakresie, które zapewniają tylko zależność (prezentera) dla tego konkretnego działania.

Druga opcja tworzy pojedynczy @Singletonkomponent, który jest w stanie dostarczyć prezenterów jako zależności bez zakresu, co oznacza, że ​​kiedy uzyskujesz do nich dostęp, za każdym razem tworzysz nową instancję prezentera. (Nie, nie tworzy nowej instancji, dopóki jej nie zażądasz).


Technicznie żadne podejście nie jest gorsze od drugiego. Pierwsze podejście nie rozdziela prezenterów według funkcji, ale według warstw.

Użyłem obu, oba działają i oba mają sens.

Jedyną wadą pierwszego rozwiązania (jeśli używasz @Component(dependencies={...}zamiast niego @Subcomponent) jest to, że musisz upewnić się, że to nie działanie tworzy wewnętrznie własny moduł, ponieważ wtedy nie możesz zastąpić implementacji metod modułu makietami. Z drugiej strony, jeśli użyjesz iniekcji konstruktora zamiast iniekcji pola, możesz po prostu utworzyć klasę bezpośrednio za pomocą konstruktora, bezpośrednio nadając jej mocks.


1

Używaj Provider<"your component's name">zamiast prostych implementacji komponentów, aby uniknąć wycieków pamięci i tworzenia ton bezużytecznych komponentów. Dlatego twoje komponenty będą tworzone przez leniwość, kiedy wywołasz metodę get (), ponieważ nie podajesz instancji komponentu, a zamiast tego tylko dostawcę. W ten sposób prezenter zostanie zastosowany, jeśli wywołano .get () dostawcy. Przeczytaj o Dostawcy tutaj i zastosuj to. ( Oficjalna dokumentacja sztyletu )


Innym świetnym sposobem jest użycie wielowiązania. Zgodnie z nią powinieneś powiązać swoich prezenterów z mapą i tworzyć je przez dostawców, kiedy tego potrzebujesz. ( tutaj jest dokumentacja o wielokrotnym wiązaniu )


-5

Twój znajomy ma rację, tak naprawdę nie musisz tworzyć komponentów i modułów dla każdej czynności. Dagger ma pomóc ci zredukować niechlujny kod i sprawić, że twoje działania w Androidzie będą czystsze, delegując wystąpienia klas do modułów, zamiast tworzyć je w metodzie onCreate Działania.

Normalnie zrobimy to w ten sposób

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

}

Zamiast tego zrób to

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

Więc pisanie zbyt wielu rzeczy może pokrzyżować cel sztyletu, nie? Raczej tworzę wystąpienia moich prezenterów w działaniach, jeśli muszę tworzyć moduły i komponenty dla każdego działania.

Jeśli chodzi o pytania dotyczące:

1- Wyciek pamięci:

Nie, chyba że @Singletondodasz adnotację do prowadzących, których dostarczasz. Dagger stworzy obiekt tylko wtedy, gdy zrobisz to @Injectw klasie docelowej. Nie utworzy innych prezenterów w Twoim scenariuszu. Możesz spróbować użyć dziennika, aby sprawdzić, czy zostały utworzone, czy nie.

@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- Wstrzykujesz dwa razy i rejestrujesz ich kod skrótu

//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. Nie, obiekty zostaną utworzone tylko wtedy, gdy wejdziesz @Injectdo działań, zamiast inicjalizacji aplikacji.


1
Dziękuję za komentarz, to, co powiedziałeś, nie jest złe, ale myślę, że to nie jest najlepsza odpowiedź, zobacz mój wpis redagujący. Nie można więc oznaczyć tego jako zaakceptowanego.
Pan Mike

@EpicPandaForce: Ech, ale musisz gdzieś go utworzyć. Coś będzie musiało naruszyć zasadę inwersji zależności.
David Liu
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.