Hançer - Her Etkinlik / Parça için her bileşeni ve modülü oluşturmalı mıyız?


85

Bir süredir dagger2 ile çalışıyorum. Ve her bir Etkinlik / Parça için kendi bileşenini / modülünü yaratırken kafam karıştı. Lütfen bunu açıklamama yardım et:

Örneğin, bir uygulamamız var ve uygulamanın yaklaşık 50 ekranı var. MVP modelini ve DI için Dagger2'yi izleyen kodu uygulayacağız. 50 etkinliğimiz ve 50 sunumcumuz olduğunu varsayalım.

Bence kodu genellikle şu şekilde düzenlemeliyiz:

  1. Uygulama açıkken kullanılacak tüm nesneleri sağlayacak bir AppComponent ve AppModule oluşturun.

    @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 Oluşturun:

    @Scope
    @Documented
    @Retention(value=RUNTIME)
    public @interface ActivityScope {
    }
    
  3. Her Aktivite için Bileşen ve Modül oluşturun. Genellikle bunları Activity sınıfına statik sınıflar olarak koyarım:

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

Bunlar, bunu nasıl uygulayacağımı gösteren çok basit örnekler.

Ama bir arkadaşım bana başka bir uygulama verdi:

  1. Tüm sunucuları sağlayacak PresenterModule oluşturun:

    @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 ve AppComponent oluşturun:

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

Onun açıklaması şudur: Her aktivite için bileşenler ve modüller oluşturmak zorunda değildir. Arkadaşlarımın fikrinin kesinlikle hiç iyi olmadığını düşünüyorum, ama lütfen yanılıyorsam beni düzeltin. İşte nedenleri:

  1. Çok fazla bellek sızıntısı :

    • Uygulama, kullanıcının yalnızca 2 Etkinliği açık olsa bile 50 sunucu oluşturacaktır.
    • Kullanıcı bir Aktiviteyi kapattıktan sonra, sunum yapan kişi kalmaya devam edecektir.
  2. Bir Aktivitenin iki örneğini oluşturmak istersem ne olur? (nasıl iki sunucu yaratabilir)

  3. Uygulamanın başlaması çok zaman alacak (çünkü birçok sunucu, nesne, ... oluşturması gerekiyor)

Uzun bir gönderi için özür dilerim, ama lütfen bunu kendim ve arkadaşım için açıklamama yardım edin, onu ikna edemiyorum. Yorumlarınız çok takdir edilecektir.

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

Demo yaptıktan sonra düzenleyin.

Öncelikle, @pandawarrior cevabı için teşekkürler. Bu soruyu sormadan önce bir Demo oluşturmalıydım. Umarım buradaki sonucum başka birine yardımcı olabilir.

  1. Arkadaşımın yaptığı şey, Provides yöntemlerine herhangi bir Kapsam koymadıkça bellek sızıntılarına neden olmaz. (Örneğin @Singleton veya @UserScope, ...)
  2. Provides yönteminin herhangi bir Kapsamı yoksa, birçok sunucu oluşturabiliriz. (Yani ikinci noktam da yanlış)
  3. Dagger, sunucuları yalnızca ihtiyaç duyulduğunda yaratacaktır. (Yani, uygulamanın başlaması uzun sürmeyecek, Lazy Injection ile kafam karıştı)

Yani yukarıda söylediğim tüm nedenler çoğunlukla yanlış. Ancak bu, iki nedenden ötürü arkadaşımın fikrini takip etmemiz gerektiği anlamına gelmez:

  1. Tüm sunucuları modül / bileşene dahil ettiğinde, kaynağın mimarisi için iyi değildir. ( Arayüz ayrımı ilkesini , belki de Tek Sorumluluk ilkesini de ihlal ediyor ).

  2. Bir Kapsam Bileşeni oluşturduğumuzda, ne zaman oluşturulduğunu ve ne zaman yok edildiğini bileceğiz, bu da bellek sızıntılarını önlemek için büyük bir avantajdır. Bu nedenle, her Aktivite için @ActivityScope ile bir Bileşen oluşturmalıyız. Arkadaşlarımın uygulamasıyla, Provider-method => 'a bir Scope koymayı unuttuğumuzu hayal edelim => bellek sızıntıları olacak.

Benim fikrime göre, küçük bir uygulama ile (çok sayıda bağımlılığı olmayan veya benzer bağımlılıkları olan sadece birkaç ekran) arkadaşlarımın fikrini uygulayabiliriz, ancak elbette tavsiye edilmiyor.

Daha fazlasını okumayı tercih edin: Dagger 2'de bir bileşenin (nesne grafiği) yaşam döngüsünü ne belirler? Dagger2 etkinlik kapsamı, kaç modüle / bileşene ihtiyacım var?

Ve bir not daha: Nesnenin ne zaman yok edildiğini görmek istiyorsanız, yönteminkileri birlikte çağırabilirsiniz ve GC hemen çalışacaktır:

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

Bu yöntemlerden yalnızca birini kullanırsanız, GC daha sonra çalışır ve yanlış sonuçlar alabilirsiniz.

Yanıtlar:


85

Her Activitybiri için ayrı bir modül bildirmek hiç de iyi bir fikir değildir. Her Activitybiri için ayrı bileşen beyan etmek daha da kötüdür. Bunun arkasındaki mantık çok basit - tüm bu modüle / bileşenlere gerçekten ihtiyacınız yok (zaten kendi başınıza gördüğünüz gibi).

Bununla birlikte, Applicationyaşam döngüsüne bağlı tek bir bileşene sahip olmak ve onu hepsine enjeksiyon için kullanmak Activitiesda en uygun çözüm değildir (bu arkadaşınızın yaklaşımıdır). Optimal değil çünkü:

  1. Sizi yalnızca bir kapsamla ( @Singletonveya özel bir kapsamla ) sınırlar
  2. Sınırlandırıldığınız tek kapsam, enjekte edilen nesneleri "uygulama tekilleri" yapar, bu nedenle kapsam belirlemedeki hatalar veya kapsamlı nesnelerin yanlış kullanımı, kolayca genel bellek sızıntılarına neden olabilir
  3. Dagger2'yi de enjekte etmek için kullanmak isteyeceksiniz Services, ancak Servicesfarklı nesneler gerektirebilir Activities(örneğin Services, sunucuya ihtiyaç duymama, sahip olma FragmentManager, vb.) Tek bir bileşen kullanarak, farklı bileşenler için farklı nesne grafikleri tanımlama esnekliğini kaybedersiniz.

Bu nedenle, bileşen başına bir bileşen Activityaşırı bir işlemdir, ancak tüm uygulama için tek bir bileşen yeterince esnek değildir. En uygun çözüm bu uç noktalar arasındadır (genellikle olduğu gibi).

Aşağıdaki yaklaşımı kullanıyorum:

  1. "Global" nesneleri sağlayan tek "uygulama" bileşeni (örneğin, uygulamadaki tüm bileşenler arasında paylaşılan global durumu tutan nesneler). Örneklendi Application.
  2. Tüm kullanıcı bakan "kontrol" gerekli olan nesneleri içerir "Uygulama" bileşeninin "Kontrol" alt bileşeni (ki mimarisinde bunlar Activitiesve Fragments). Her birinde Activityve Fragment.
  3. Herkesin ihtiyaç duyduğu nesneleri sağlayan "uygulama" bileşeninin "hizmet" alt bileşeni Services. Her birinde örneklenmiştir Service.

Aşağıda aynı yaklaşımı nasıl uygulayabileceğinize dair bir örnek verilmiştir.


Temmuz 2017'yi düzenleyin

Android uygulamasında Dagger bağımlılık enjeksiyon kodunun nasıl yapılandırılacağını gösteren bir video eğitimi yayınladım: Android Dagger for Professionals Tutorial .


Şubat 2018'i Düzenle

Android'de bağımlılık ekleme hakkında eksiksiz bir kurs yayınladım .

Bu derste bağımlılık ekleme teorisini açıklıyorum ve Android uygulamasında doğal olarak nasıl ortaya çıktığını gösteriyorum. Ardından, Dagger yapılarının genel bağımlılık enjeksiyon şemasına nasıl uyduğunu göstereceğim.

Bu kursu alırsanız, her bir Etkinlik / Parça için ayrı bir modül / bileşen tanımına sahip olma fikrinin neden temelde en temel şekilde kusurlu olduğunu anlayacaksınız.

Bu tür bir yaklaşım, "İşlevsel" sınıflar kümesinden gelen sunum katmanının yapısının, "Oluşturma" sınıflar kümesinin yapısına yansıtılmasına ve böylece bunların birbirine bağlanmasına neden olur. Bu, "Yapım" ve "İşlevsel" sınıf kümelerini ayrık tutmak olan bağımlılık enjeksiyonunun ana amacına aykırıdır.


Uygulama kapsamı:

@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


1
Thanks @vasiliy for sharing your opinion. This is exactly how I would use it and currently followed strategy. In case of an MVP pattern, the referred ControllerModule will create a new Presenter and then the presenter is injected in the Activity or Fragment. Any solid opinion in favor or against this?
Wahib Ul Haq

@Vasiliy, I read your entire article and I found that maybe you didn't consider interactors and presenters in mechanism. Will ControllerModule provides all dependency of interactors and presenters ? Please give a small hint in case I missed anything.
iamcrypticcoder

@mahbub.kuet, if I understand what you're referring to by "interactors" and "presenters", ControllerComponent should inject them. Whether you wire them inside ControllerModule, or introduce additional module is up to you. In real apps I advice using multi-module per component approach instead of putting everything in a single module. Here is an example for ApplicationComponent, but controller's will be the same: github.com/techyourchance/idocare-android/tree/master/app/src/…
Vasiliy

2
@Mr.Hyde, in general yes, but then you'll have to explicitly declare in ApplicationComponent all dependencies that ControllerComponent can use. Also the method count of the generated code will be higher. I haven't found a good reason to use dependent components yet.
Vasiliy

1
I'm using this approach in all my projects today and I explicitly don't use anything from dagger.android package because I find it to be ill motivated. Therefore, this example is still very much up to date and is still the best way to do DI in Android IMHO.
Vasiliy

15

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.


1
this is also my preferred approach. I don't like activities/fragments having access to things they're not supposed to.
Joao Sousa

3

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.


1

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)


-5

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.


1
Thank for the comment, what you have said are not wrong, but I think it not the best answer, please see my edit post. So, could not mark it as accepted.
Mr Mike

@EpicPandaForce: Eh, but you have to instantiate it somewhere. Something will have to violate the dependency inversion principle.
David Liu
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.