Dagger - Dovremmo creare ogni componente e modulo per ogni attività / frammento


85

Lavoro con dagger2 da un po '. E mi sono confuso se creare un proprio componente / modulo per ogni attività / frammento. Per favore aiutami a chiarire questo:

Ad esempio, abbiamo un'app e l'app ha circa 50 schermate. Implementeremo il codice seguendo il pattern MVP e Dagger2 per DI. Supponiamo di avere 50 attività e 50 presentatori.

Secondo me, di solito dovremmo organizzare il codice in questo modo:

  1. Crea un AppComponent e un AppModule che forniranno tutti gli oggetti che verranno usati mentre l'app è aperta.

    @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. Crea attività

    @Scope
    @Documented
    @Retention(value=RUNTIME)
    public @interface ActivityScope {
    }
    
  3. Crea componente e modulo per ogni attività. Di solito le inserisco come classi statiche all'interno della classe 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.
    

Questi sono solo esempi molto semplici per mostrare come lo implementerei.

Ma un mio amico mi ha appena dato un'altra implementazione:

  1. Crea PresenterModule che fornirà a tutti i relatori:

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

La sua spiegazione è: non deve creare componenti e moduli per ogni attività. Penso che l'idea dei miei amici non sia assolutamente buona, ma per favore correggimi se sbaglio. Ecco i motivi:

  1. Molte perdite di memoria :

    • L'app creerà 50 relatori anche se l'utente ha solo 2 attività aperte.
    • Dopo che l'utente chiude un'attività, il relatore rimarrà comunque
  2. Cosa succede se voglio creare due istanze di un'attività? (come può creare due presentatori)

  3. Ci vorrà molto tempo per l'inizializzazione dell'app (perché deve creare molti presentatori, oggetti, ...)

Scusa per il post lungo, ma per favore aiutami a chiarire questo per me e il mio amico, non riesco a convincerlo. I tuoi commenti saranno molto apprezzati.

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

Modifica dopo aver fatto una demo.

Innanzitutto, grazie per la risposta di @pandawarrior. Avrei dovuto creare una demo prima di porre questa domanda. Spero che la mia conclusione qui possa aiutare qualcun altro.

  1. Ciò che il mio amico ha fatto non causa perdite di memoria a meno che non inserisca uno Scope nei metodi Provides. (Ad esempio @Singleton o @UserScope, ...)
  2. Possiamo creare molti presentatori, se il metodo Provides non ha alcun ambito. (Quindi, anche il mio secondo punto è sbagliato)
  3. Dagger creerà i presentatori solo quando sono necessari. (Quindi, l'app non impiegherà molto tempo per inizializzarsi, sono stato confuso da Lazy Injection)

Quindi, tutte le ragioni che ho detto sopra sono per lo più sbagliate. Ma ciò non significa che dovremmo seguire l'idea del mio amico, per due motivi:

  1. Non va bene per l'architettura della sorgente, quando inserisce tutti i presentatori in modulo / componente. (Viola il principio di segregazione dell'interfaccia , forse anche il principio di responsabilità unica ).

  2. Quando creiamo un componente Scope, sapremo quando viene creato e quando viene distrutto, il che è un enorme vantaggio per evitare perdite di memoria. Quindi, per ogni attività dovremmo creare un componente con un @ActivityScope. Immaginiamo, con l'implementazione dei miei amici, di aver dimenticato di mettere un po 'di Scope nel metodo Provider => si verificheranno perdite di memoria.

Secondo me, con una piccola app (solo poche schermate senza molte dipendenze o con dipendenze simili), potremmo applicare l'idea dei miei amici, ma ovviamente non è consigliabile.

Preferisco leggere di più su: Cosa determina il ciclo di vita di un componente (grafico a oggetti) in Dagger 2? Ambito di attività di Dagger2, di quanti moduli / componenti ho bisogno?

E un'altra nota: se vuoi vedere quando l'oggetto viene distrutto, puoi chiamare quelli del metodo insieme e il GC verrà eseguito immediatamente:

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

Se utilizzi solo uno di questi metodi, GC verrà eseguito in un secondo momento e potresti ottenere risultati errati.

Risposte:


85

Dichiarare un modulo separato per ciascuno Activitynon è affatto una buona idea. Dichiarare componenti separati per ciascuno Activityè anche peggio. Il ragionamento alla base di questo è molto semplice: non hai davvero bisogno di tutti questi moduli / componenti (come hai già visto da solo).

Tuttavia, anche avere un solo componente legato al Applicationciclo di vita di e utilizzarlo per l'iniezione in tutti Activitiesnon è la soluzione ottimale (questo è l'approccio del tuo amico). Non è ottimale perché:

  1. Ti limita a un solo ambito ( @Singletono uno personalizzato)
  2. L'unico ambito a cui sei limitato rende gli oggetti inseriti "singleton dell'applicazione", quindi errori nella definizione dell'ambito o un utilizzo errato degli oggetti con ambito possono facilmente causare perdite di memoria globale
  3. Ti consigliamo di utilizzare Dagger2 anche per effettuare l'iniezione Services, ma Servicespuò richiedere oggetti diversi da Activities(ad es Services. Non servono presentatori, non ce l'hanno FragmentManager, ecc.). Utilizzando un singolo componente si perde la flessibilità di definire diversi oggetti grafici per diversi componenti.

Quindi, un componente per Activityè eccessivo, ma un singolo componente per l'intera applicazione non è abbastanza flessibile. La soluzione ottimale è tra questi estremi (come di solito è).

Uso il seguente approccio:

  1. Singolo componente "applicazione" che fornisce oggetti "globali" (ad es. Oggetti che mantengono uno stato globale condiviso tra tutti i componenti dell'applicazione). Istanziato in Application.
  2. Sottocomponente "controller" del componente "applicazione" che fornisce gli oggetti richiesti da tutti i "controller" rivolti all'utente (nella mia architettura questi sono Activitiese Fragments). Istanziato in ogni Activitye Fragment.
  3. Sottocomponente "Servizio" del componente "applicazione" che fornisce gli oggetti richiesti da tutti Services. Istanziato in ciascuno Service.

Di seguito è riportato un esempio di come è possibile implementare lo stesso approccio.


Modifica luglio 2017

Ho pubblicato un video tutorial che mostra come strutturare il codice di iniezione delle dipendenze Dagger nell'applicazione Android: Android Dagger for Professionals Tutorial .


Modifica febbraio 2018

Ho pubblicato un corso completo sull'iniezione di dipendenze in Android .

In questo corso spiego la teoria dell'iniezione di dipendenza e mostro come emerge naturalmente nell'applicazione Android. Quindi mostro come i costrutti di Dagger si adattano allo schema di iniezione di dipendenza generale.

Se segui questo corso capirai perché l'idea di avere una definizione separata di modulo / componente per ogni Attività / Frammento è fondamentalmente viziata nel modo più fondamentale.

Un tale approccio fa sì che la struttura del livello di presentazione dell'insieme di classi "Funzionale" venga rispecchiato nella struttura dell'insieme di classi "Costruzione", accoppiandole insieme. Questo va contro l'obiettivo principale dell'iniezione di dipendenza che è mantenere disgiunti gli insiemi di classi "Costruzione" e "Funzionale".


Ambito di applicazione:

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

Ambito del controller:

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

E poi 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;
    }
}

Ulteriori informazioni sull'inserimento delle dipendenze:

Dagger 2 Mirini demistificati

Inserimento di dipendenze in Android


1
Grazie @vasiliy per aver condiviso la tua opinione. Questo è esattamente il modo in cui lo userei e la strategia attualmente seguita. In caso di pattern MVP, il referenziato ControllerModulene creerà uno nuovo Presentere quindi il presentatore verrà iniettato in Activityo Fragment. Qualche opinione solida a favore o contro questo?
Wahib Ul Haq

@ Vasiliy, ho letto tutto il tuo articolo e ho scoperto che forse non hai considerato interattori e presentatori nel meccanismo. ControllerModule fornirà tutte le dipendenze di interattori e relatori ? Si prega di dare un piccolo suggerimento nel caso mi sia perso qualcosa.
iamcrypticcoder

@ mahbub.kuet, se capisco a cosa ti riferisci con "interattori" e "presentatori", ControllerComponentdovresti iniettarli. ControllerModuleSpetta a te decidere se cablarli all'interno o introdurre un modulo aggiuntivo. Nelle app reali consiglio di utilizzare l'approccio multi-modulo per componente invece di mettere tutto in un unico modulo. Ecco un esempio per ApplicationComponent, ma il controller sarà lo stesso: github.com/techyourchance/idocare-android/tree/master/app/src/…
Vasiliy

2
@ Mr.Hyde, in generale sì, ma poi dovrai dichiarare esplicitamente in ApplicationComponenttutte le dipendenze che ControllerComponentpuoi usare. Anche il conteggio del metodo del codice generato sarà maggiore. Non ho ancora trovato un buon motivo per utilizzare componenti dipendenti.
Vasiliy

1
Oggi sto usando questo approccio in tutti i miei progetti e non uso esplicitamente nulla di dagger.androidpackage perché trovo che sia mal motivato. Pertanto, questo esempio è ancora molto aggiornato ed è ancora il modo migliore per fare DI in Android IMHO.
Vasiliy

15

Alcuni dei migliori esempi di come organizzare i componenti, i moduli e i pacchetti possono essere trovati nel repository Github di Google Android Architecture Blueprints qui .

Se esamini il codice sorgente lì, puoi vedere che c'è un singolo componente con ambito app (con un ciclo di vita della durata dell'intera app) e quindi componenti separati con ambito attività per l'attività e il frammento corrispondenti a una determinata funzionalità in un progetto. Ad esempio, ci sono i seguenti pacchetti:

addedittask
taskdetail
tasks

All'interno di ogni pacchetto c'è un modulo, un componente, un presenter ecc. Ad esempio, all'interno taskdetailci sono le seguenti classi:

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

Il vantaggio di organizzare in questo modo (piuttosto che raggruppare tutte le attività in un componente o modulo) è che puoi sfruttare i modificatori di accessibilità Java e soddisfare l'elemento Java effettivo 13. In altre parole, le classi raggruppate funzionalmente saranno nello stesso pacchetto e si può approfittare di protectede package-private modificatori di accessibilità per impedire usi non intenzionali delle vostre classi.


1
questo è anche il mio approccio preferito. Non mi piace che attività / frammenti abbiano accesso a cose che non dovrebbero.
Joao Sousa

3

La prima opzione crea un componente sottoprocesso per ciascuna attività, in cui l'attività è in grado di creare componenti sottoprocesso che forniscono solo la dipendenza (presentatore) per quella particolare attività.

La seconda opzione crea un singolo @Singletoncomponente in grado di fornire i relatori come dipendenze senza ambito, il che significa che quando si accede ad essi, si crea ogni volta una nuova istanza del presentatore. (No, non crea una nuova istanza fino a quando non ne richiedi una).


Tecnicamente, nessuno dei due approcci è peggiore dell'altro. Il primo approccio non separa i relatori per funzionalità, ma per livello.

Li ho usati entrambi, funzionano entrambi ed entrambi hanno un senso.

L'unico svantaggio della prima soluzione (se stai usando al @Component(dependencies={...}posto di @Subcomponent) è che devi assicurarti che non sia l'attività che crea il proprio modulo internamente, perché in questo modo non puoi sostituire le implementazioni del metodo del modulo con i mock. Poi di nuovo, se usi l'iniezione del costruttore invece dell'iniezione del campo, puoi semplicemente creare la classe direttamente con il costruttore, dandogli direttamente dei mock.


1

Utilizzare Provider<"your component's name">invece della semplice implementazione dei componenti per evitare perdite di memoria e creare tonnellate di componenti inutili. Pertanto i tuoi componenti verranno creati da pigro quando chiami il metodo get () poiché non fornisci un'istanza del componente ma solo il provider. Quindi il tuo presentatore verrà applicato se è stato chiamato .get () del provider. Leggi qui il provider e applica questo. ( Documentazione ufficiale di Dagger )


E un altro ottimo modo è usare il multibinding. In base ad esso dovresti associare i tuoi relatori alla mappa e crearli tramite i fornitori quando necessario. ( ecco i documenti sul multibinding )


-5

Il tuo amico ha ragione, non devi davvero creare componenti e moduli per ogni attività. Dagger dovrebbe aiutarti a ridurre il codice disordinato e rendere più pulite le tue attività Android delegando le istanze di classe ai moduli invece di istanziarle nel metodo onCreate delle attività.

Normalmente faremo così

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

}

Fai questo invece

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

Quindi scrivere troppe cose in qualche modo sconfigge lo scopo del pugnale no? Preferisco istanziare i miei relatori in Attività se devo creare moduli e componenti per ogni attività.

Per quanto riguarda le tue domande su:

1- Perdita di memoria:

No, a meno che tu non metta @Singletonun'annotazione ai relatori che fornisci. Dagger creerà l'oggetto solo ogni volta che farai una @Injectnella classe di destinazione`. Non creerà gli altri presentatori nel tuo scenario. Puoi provare a utilizzare Log per vedere se sono stati creati o meno.

@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- Inietti due volte e registri il loro codice hash

//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, gli oggetti verranno creati solo quando si @Injectaccede alle attività, al posto dell'app init.


1
Grazie per il commento, quello che hai detto non è sbagliato, ma penso che non sia la risposta migliore, guarda il mio post di modifica. Quindi, non è stato possibile contrassegnarlo come accettato.
Mr Mike

@EpicPandaForce: Eh, ma devi istanziarlo da qualche parte. Qualcosa dovrà violare il principio di inversione di dipendenza.
David Liu
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.