Uso di Dagger nelle app per Android

La pagina Nozioni di base su Dagger ha spiegato come Dagger può aiutarti ad automatizzare la dipendenza iniezione di dati nell'app. Con Dagger, non devi scrivere noiosi e codice boilerplate soggetto a errori.

Riepilogo delle best practice

  • Usa l'inserimento del costruttore con @Inject per aggiungere tipi al Dagger quando è possibile. Quando non lo è:
    • Usa @Binds per indicare a Dagger l'implementazione che dovrebbe avere un'interfaccia.
    • Usa @Provides per spiegare a Dagger come fornire corsi al tuo progetto non è proprietario.
  • Devi dichiarare i moduli una sola volta in un componente.
  • Assegna un nome alle annotazioni dell'ambito in base alla durata in cui l'annotazione. Esempi: @ApplicationScope, @LoggedUserScope, e @ActivityScope.

Aggiunta di dipendenze

Per utilizzare Dagger nel tuo progetto, aggiungi queste dipendenze all'applicazione in il tuo file build.gradle. Puoi trovare l'ultima versione di Dagger in questo progetto GitHub.

Kotlin

plugins {
  id 'kotlin-kapt'
}

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    kapt 'com.google.dagger:dagger-compiler:2.x'
}

Java

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}

Dagger in Android

Considera un'app per Android di esempio con il grafico delle dipendenze della Figura 1.

LoginActivity dipende da LoginViewModel, che dipende da UserRepository,
  che dipende da UserLocalDataSource e UserRemoteDataSource, che a loro volta
  dipende da Retrofit.

Figura 1. Grafico delle dipendenze dell'esempio codice

In Android, di solito crei un grafico Dagger presente nella tua applicazione perché desideri che un'istanza del grafico rimanga in memoria finché sia in esecuzione. In questo modo, il grafico è collegato al ciclo di vita dell'app. In alcuni casi, potresti anche voler rendere disponibile il contesto dell'applicazione nella grafico. A questo scopo, il grafico deve trovarsi anche Application. Un vantaggio è che il grafico è disponibile per altre classi di framework Android. Inoltre, semplifica i test consentendoti di utilizzare un modello Application corso nei test.

Poiché l'interfaccia che genera il grafico è annotata con @Component, puoi chiamarlo ApplicationComponent o ApplicationGraph. Di solito mantieni un'istanza di quel componente nella tua classe Application personalizzata e denominala ogni volta che hai bisogno del grafico dell'applicazione, come mostrato nel seguente codice snippet:

Kotlin

// Definition of the Application graph
@Component
interface ApplicationComponent { ... }

// appComponent lives in the Application class to share its lifecycle
class MyApplication: Application() {
    // Reference to the application graph that is used across the whole app
    val appComponent = DaggerApplicationComponent.create()
}

Java

// Definition of the Application graph
@Component
public interface ApplicationComponent {
}

// appComponent lives in the Application class to share its lifecycle
public class MyApplication extends Application {

    // Reference to the application graph that is used across the whole app
    ApplicationComponent appComponent = DaggerApplicationComponent.create();
}

Poiché alcune classi di framework Android, come attività e frammenti, vengono creata dal sistema, Dagger non può crearle per te. Per le attività in particolare, qualsiasi codice di inizializzazione deve essere inserito nel metodo onCreate(). Ciò significa che non puoi utilizzare l'annotazione @Inject nel costruttore del (instructor injection) come negli esempi precedenti. Invece, devi usare l'iniezione di campo.

Invece di creare le dipendenze richieste da un'attività nell'onCreate() vuoi che Dagger compili automaticamente queste dipendenze. Per campo un'iniezione, viene invece applicata l'annotazione @Inject ai campi che che si desidera ricavare dal grafico Dagger.

Kotlin

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel
}

Java

public class LoginActivity extends Activity {

    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject
    LoginViewModel loginViewModel;
}

Per semplicità, LoginViewModel non è un componente dell'architettura Android ViewModel; è solo una classe normale che agisce come ViewModel. Per maggiori informazioni su come inserire questi corsi, dai un'occhiata al codice nell'implementazione ufficiale di Android Blueprints Dagger, in il ramo dev-dagger.

Una delle considerazioni da fare di Dagger è che i campi inseriti non possono essere privati. Devono disporre almeno della visibilità privata del pacchetto, come nel codice precedente.

Inserimento di attività

Dagger deve sapere che LoginActivity deve accedere al grafico per fornire i ViewModel richiesti. Nella pagina Nozioni di base su Dagger, hai utilizzato l'interfaccia @Component per estrarre oggetti dal grafico esponendo funzioni con il tipo restituito di ciò che si vuole ottenere grafico. In questo caso, devi comunicare a Dagger di un oggetto (LoginActivity in questo caso) che richiede l'inserimento di una dipendenza. Per questo, espone una funzione che prende come parametro l'oggetto che richiede l'inserimento.

Kotlin

@Component
interface ApplicationComponent {
    // This tells Dagger that LoginActivity requests injection so the graph needs to
    // satisfy all the dependencies of the fields that LoginActivity is requesting.
    fun inject(activity: LoginActivity)
}

Java

@Component
public interface ApplicationComponent {
    // This tells Dagger that LoginActivity requests injection so the graph needs to
    // satisfy all the dependencies of the fields that LoginActivity is injecting.
    void inject(LoginActivity loginActivity);
}

Questa funzione comunica a Dagger che LoginActivity vuole accedere al grafico richieste di iniezione. Dagger deve soddisfare tutte le dipendenze Richiede LoginActivity (LoginViewModel con le sue dipendenze). Se hai più corsi che richiedono l'inserimento, devi specificare dichiararli tutti nel componente con il tipo esatto. Ad esempio, se avevi LoginActivity e RegistrationActivity richiedono l'inserimento, avresti due inject() anziché uno generico che copre entrambi i casi. Un generico Il metodo inject() non dice a Dagger cosa deve essere fornito. Le funzioni nell'interfaccia può avere qualsiasi nome, ma viene chiamato inject() quando l'oggetto da inserire come parametro è una convenzione in Dagger.

Per inserire un oggetto nell'attività, utilizza il valore appComponent definito in la tua classe Application e chiama il metodo inject(), passando in un'istanza dell'attività che richiede l'inserimento.

Quando utilizzi le attività, inserisci Dagger il metodo onCreate() dell'attività prima di chiamare super.onCreate() per evitare problemi con il ripristino dei frammenti. Durante la fase di ripristino in super.onCreate(), un'attività collega frammenti che potrebbero voler accedere alle associazioni di attività.

Quando utilizzi i frammenti, inserisci Dagger nel campo onAttach() del frammento . In questo caso, può essere fatto prima o dopo aver chiamato il numero super.onAttach().

Kotlin

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Make Dagger instantiate @Inject fields in LoginActivity
        (applicationContext as MyApplication).appComponent.inject(this)
        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

// @Inject tells Dagger how to create instances of LoginViewModel
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

public class LoginActivity extends Activity {

    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Make Dagger instantiate @Inject fields in LoginActivity
        ((MyApplication) getApplicationContext()).appComponent.inject(this);
        // Now loginViewModel is available

        super.onCreate(savedInstanceState);
    }
}

public class LoginViewModel {

    private final UserRepository userRepository;

    // @Inject tells Dagger how to create instances of LoginViewModel
    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Diciamo a Dagger come fornire le restanti dipendenze per la creazione nel grafico:

Kotlin

class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor(
    private val loginService: LoginRetrofitService
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

public class UserLocalDataSource {

    @Inject
    public UserLocalDataSource() {}
}

public class UserRemoteDataSource {

    private final LoginRetrofitService loginRetrofitService;

    @Inject
    public UserRemoteDataSource(LoginRetrofitService loginRetrofitService) {
        this.loginRetrofitService = loginRetrofitService;
    }
}

Moduli di pugnali

Per questo esempio, utilizzi la libreria di networking Retrofit. UserRemoteDataSource ha una dipendenza da LoginRetrofitService. Tuttavia, per creare un'istanza di LoginRetrofitService è diverso da quello che avete fatto fino a ora. Non è un'istanza in classe, è il risultato chiamando Retrofit.Builder() e passando diversi parametri per configurare il servizio di accesso.

Oltre all'annotazione @Inject, c'è un altro modo per spiegare a Dagger come fornire un'istanza di una classe, ovvero le informazioni all'interno dei moduli Dagger. Un pugnale modulo è una classe annotata con @Module. Qui puoi definire con l'annotazione @Provides.

Kotlin

// @Module informs Dagger that this class is a Dagger Module
@Module
class NetworkModule {

    // @Provides tell Dagger how to create instances of the type that this function
    // returns (i.e. LoginRetrofitService).
    // Function parameters are the dependencies of this type.
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService {
        // Whenever Dagger needs to provide an instance of type LoginRetrofitService,
        // this code (the one inside the @Provides method) is run.
        return Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService::class.java)
    }
}

Java

// @Module informs Dagger that this class is a Dagger Module
@Module
public class NetworkModule {

    // @Provides tell Dagger how to create instances of the type that this function
    // returns (i.e. LoginRetrofitService).
    // Function parameters are the dependencies of this type.
    @Provides
    public LoginRetrofitService provideLoginRetrofitService() {
        // Whenever Dagger needs to provide an instance of type LoginRetrofitService,
        // this code (the one inside the @Provides method) is run.
        return new Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService.class);
    }
}

I moduli sono un modo per incapsulare semanticamente le informazioni su come fornire di oggetti strutturati. Come puoi vedere, hai chiamato la classe NetworkModule per raggruppare la logica di fornire oggetti legati al networking. Se l'applicazione si espande, puoi aggiungi anche come fornire un OkHttpClient qui, o come configurare Gson o Moshi.

Le dipendenze di un metodo @Provides sono i parametri di quel metodo. Per sul metodo precedente, puoi fornire LoginRetrofitService senza dipendenze perché il metodo non ha parametri. Se hai dichiarato OkHttpClient come Dagger deve fornire un'istanza OkHttpClient dal per soddisfare le dipendenze di LoginRetrofitService. Ad esempio:

Kotlin

@Module
class NetworkModule {
    // Hypothetical dependency on LoginRetrofitService
    @Provides
    fun provideLoginRetrofitService(
        okHttpClient: OkHttpClient
    ): LoginRetrofitService { ... }
}

Java

@Module
public class NetworkModule {

    @Provides
    public LoginRetrofitService provideLoginRetrofitService(OkHttpClient okHttpClient) {
        ...
    }
}

Affinché il grafico Dagger sappia di questo modulo, devi aggiungerlo a l'interfaccia @Component in questo modo:

Kotlin

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    ...
}

Java

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
    ...
}

Il modo consigliato per aggiungere tipi al grafico Dagger è utilizzare il costruttore iniezione (ovvero con l'annotazione @Inject sul costruttore della classe). A volte non è possibile e devi usare i moduli Dagger. Un esempio è quando si desidera che Dagger usi il risultato di un calcolo per determinare come per creare un'istanza di un oggetto. Ogni volta che deve fornire un'istanza di questo tipo, Dagger esegue il codice all'interno del metodo @Provides.

Ecco l'aspetto attuale del grafico Dagger nell'esempio:

Diagramma del grafico delle dipendenze dell'attività di accesso

Figura 2. Rappresentazione del grafico con Iniettamento di LoginActivity da parte di Dagger

Il punto di accesso al grafico è LoginActivity. Perché LoginActivity inserisce LoginViewModel, Dagger crea un grafico che sa come fornire un'istanza di LoginViewModel, e ricorsivamente, delle sue dipendenze. Pugnale sa come fare a causa dell'annotazione @Inject nella classe come costruttore.

All'interno del ApplicationComponent generato da Dagger, c'è un campo per ottenere istanze di tutte le classi che sa fornire. In questo esempio, Dagger delegati all'NetworkModule incluso in ApplicationComponent per ottenere un'istanza di LoginRetrofitService.

Ambiti pugnali

Gli ambiti sono stati menzionati nella pagina Nozioni di base su Dagger per avere a disposizione un'istanza univoca di un tipo in un componente. Questo è ciò che si intende per dell'ambito di un tipo al ciclo di vita del componente.

Perché potresti voler usare UserRepository in altre funzionalità dell'app e non vuoi creare un nuovo oggetto ogni volta che ne hai bisogno, puoi specificare come istanza unica per l'intera app. È lo stesso per LoginRetrofitService: può essere costoso creare e vuoi anche un'istanza univoca di quell'oggetto da riutilizzare. Creazione di un'istanza di UserRemoteDataSource non è così costoso, quindi viene scelto come ambito non è necessario.

@Singleton è l'unica annotazione relativa all'ambito fornita con il pacchetto javax.inject. Puoi usarlo per annotare ApplicationComponent e gli oggetti che vuoi riutilizzare nell'intera applicazione.

Kotlin

@Singleton
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    fun inject(activity: LoginActivity)
}

@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Module
class NetworkModule {
    // Way to scope types inside a Dagger Module
    @Singleton
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService { ... }
}

Java

@Singleton
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
    void inject(LoginActivity loginActivity);
}

@Singleton
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

@Module
public class NetworkModule {

    @Singleton
    @Provides
    public LoginRetrofitService provideLoginRetrofitService() { ... }
}

Fai attenzione a non introdurre perdite di memoria quando applichi ambiti agli oggetti. Come finché il componente con ambito è in memoria, l'oggetto creato rimane in memoria . Poiché ApplicationComponent viene creata all'avvio dell'app (nel Application), viene eliminata quando l'app viene eliminata. Di conseguenza, l'istanza univoca di UserRepository rimane sempre in memoria fino l'applicazione viene eliminata.

Sottocomponenti del pugnale

Se il flusso di accesso (gestito da un solo LoginActivity) è costituito da più devi riutilizzare la stessa istanza di LoginViewModel in tutti di grandi dimensioni. @Singleton non può annotare LoginViewModel per riutilizzare l'istanza per i seguenti motivi:

  1. L'istanza di LoginViewModel rimarrà in memoria dopo che il flusso è completato.

  2. Vuoi un'istanza diversa di LoginViewModel per ogni flusso di accesso. Ad esempio, se l'utente si disconnette, vuoi un'istanza diversa di LoginViewModel, anziché la stessa istanza di quando l'utente ha eseguito l'accesso per la prima volta.

Per l'ambito LoginViewModel al ciclo di vita di LoginActivity, devi creare un nuovo componente (un nuovo grafico secondario) per il flusso di accesso e un nuovo ambito.

Creiamo un grafico specifico per il flusso di accesso.

Kotlin

@Component
interface LoginComponent {}

Java

@Component
public interface LoginComponent {
}

Ora LoginActivity dovrebbe ricevere iniezioni da LoginComponent perché ha una configurazione specifica per l'accesso. Ciò elimina la responsabilità di inserire LoginActivity del corso ApplicationComponent.

Kotlin

@Component
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

@Component
public interface LoginComponent {
    void inject(LoginActivity loginActivity);
}

LoginComponent deve essere in grado di accedere agli oggetti da ApplicationComponent perché LoginViewModel dipende da UserRepository. Il modo di dire a Dagger vuoi che un nuovo componente usi parte di un altro componente è con Sottocomponenti Dagger. Il nuovo componente deve essere un sottocomponente del contenente risorse condivise.

I sottocomponenti sono componenti che ereditano ed estendono il grafico degli oggetti di un principale. Di conseguenza, tutti gli oggetti forniti nel componente padre fornito anche nel sottocomponente. In questo modo, un oggetto di un sottocomponente può dipendere da un oggetto fornito dal componente principale.

Per creare istanze dei componenti secondari, hai bisogno di un'istanza del componente di strumento di authoring. Pertanto, gli oggetti forniti dal componente principale il sottocomponente principale riguarda ancora il componente principale.

Nell'esempio, devi definire LoginComponent come sottocomponente di ApplicationComponent. Per farlo, aggiungi a LoginComponent annotazioni @Subcomponent:

Kotlin

// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent
@Subcomponent
interface LoginComponent {

    // This tells Dagger that LoginActivity requests injection from LoginComponent
    // so that this subcomponent graph needs to satisfy all the dependencies of the
    // fields that LoginActivity is injecting
    fun inject(loginActivity: LoginActivity)
}

Java

// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent
@Subcomponent
public interface LoginComponent {

    // This tells Dagger that LoginActivity requests injection from LoginComponent
    // so that this subcomponent graph needs to satisfy all the dependencies of the
    // fields that LoginActivity is injecting
    void inject(LoginActivity loginActivity);
}

Devi anche definire il valore di fabbrica di un sottocomponente all'interno di LoginComponent, in modo che ApplicationComponent sa come creare istanze di LoginComponent.

Kotlin

@Subcomponent
interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    fun inject(loginActivity: LoginActivity)
}

Java

@Subcomponent
public interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    void inject(LoginActivity loginActivity);
}

Per indicare a Dagger che LoginComponent è un sottocomponente di ApplicationComponent, devi indicarlo con:

  1. Crea un nuovo modulo Dagger (ad es. SubcomponentsModule) per superare della classe del sottocomponente all'attributo subcomponents dell'annotazione.

    Kotlin

    // The "subcomponents" attribute in the @Module annotation tells Dagger what
    // Subcomponents are children of the Component this module is included in.
    @Module(subcomponents = LoginComponent::class)
    class SubcomponentsModule {}
    

    Java

    // The "subcomponents" attribute in the @Module annotation tells Dagger what
    // Subcomponents are children of the Component this module is included in.
    @Module(subcomponents = LoginComponent.class)
    public class SubcomponentsModule {
    }
    
  2. Aggiunta del nuovo modulo (ad esempio SubcomponentsModule) a ApplicationComponent:

    Kotlin

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = [NetworkModule::class, SubcomponentsModule::class])
    interface ApplicationComponent {
    }
    

    Java

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = {NetworkModule.class, SubcomponentsModule.class})
    public interface ApplicationComponent {
    }
    

    Tieni presente che ApplicationComponent non deve più inserire LoginActivity poiché questa responsabilità ora appartiene a LoginComponent, quindi puoi rimuovere il metodo inject() da ApplicationComponent.

    I consumer di ApplicationComponent devono sapere come creare istanze di LoginComponent. Il componente principale deve aggiungere un metodo nella sua interfaccia per consentire i consumer creano istanze del sottocomponente da un'istanza del componente principale:

  3. Esponi la fabbrica che crea istanze di LoginComponentnell'ambiente dell'interfaccia:

    Kotlin

    @Singleton
    @Component(modules = [NetworkModule::class, SubcomponentsModule::class])
    interface ApplicationComponent {
    // This function exposes the LoginComponent Factory out of the graph so consumers
    // can use it to obtain new instances of LoginComponent
    fun loginComponent(): LoginComponent.Factory
    }
    

    Java

    @Singleton
    @Component(modules = { NetworkModule.class, SubcomponentsModule.class} )
    public interface ApplicationComponent {
    // This function exposes the LoginComponent Factory out of the graph so consumers
    // can use it to obtain new instances of LoginComponent
    LoginComponent.Factory loginComponent();
    }
    

Assegnazione di ambiti ai componenti secondari

Se crei il progetto, puoi creare istanze di ApplicationComponent e LoginComponent. ApplicationComponent è collegato al ciclo di vita del perché vuoi utilizzare la stessa istanza del grafico, purché l'applicazione è in memoria.

Qual è il ciclo di vita di LoginComponent? Uno dei motivi per cui avevi bisogno LoginComponent è perché devi condividere la stessa istanza del LoginViewModel tra frammenti correlati all'accesso. Ma vuoi anche istanze di LoginViewModel ogni volta che è presente un nuovo flusso di accesso. LoginActivity sia la durata giusta per LoginComponent: per ogni nuova attività, devi disporre una nuova istanza di LoginComponent e frammenti che possono utilizzare l'istanza di LoginComponent.

Poiché LoginComponent è collegato al ciclo di vita LoginActivity, devi un riferimento al componente nell'attività nello stesso modo in cui hai mantenuto riferimento a applicationComponent nella classe Application. In questo modo che possano accedervi.

Kotlin

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent
    ...
}

Java

public class LoginActivity extends Activity {

    // Reference to the Login graph
    LoginComponent loginComponent;

    ...
}

Tieni presente che la variabile loginComponent non è annotata con @Inject perché non prevedi che la variabile ti venga fornita da Dagger.

Puoi utilizzare ApplicationComponent per ottenere un riferimento a LoginComponent e poi inserisci LoginActivity come segue:

Kotlin

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Creation of the login graph using the application graph
        loginComponent = (applicationContext as MyDaggerApplication)
                            .appComponent.loginComponent().create()

        // Make Dagger instantiate @Inject fields in LoginActivity
        loginComponent.inject(this)

        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

Java

public class LoginActivity extends Activity {

    // Reference to the Login graph
    LoginComponent loginComponent;

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Creation of the login graph using the application graph
        loginComponent = ((MyApplication) getApplicationContext())
                                .appComponent.loginComponent().create();

        // Make Dagger instantiate @Inject fields in LoginActivity
        loginComponent.inject(this);

        // Now loginViewModel is available

        super.onCreate(savedInstanceState);
    }
}

LoginComponent viene creato nel metodo onCreate() dell'attività e riceverà e in modo implicito quando l'attività viene eliminata.

LoginComponent deve sempre fornire la stessa istanza di LoginViewModel a ogni richiesta. Puoi farlo creando un'annotazione personalizzata ambito e annota sia LoginComponent che LoginViewModel. Nota che l'annotazione @Singleton non può essere utilizzata perché è già stata utilizzata dal componente padre e rendere l'oggetto un singleton dell'applicazione (istanza unica per l'intera app). Devi creare un'annotazione diversa l'ambito di attività.

In questo caso, avresti potuto chiamare questo ambito @LoginScope, ma non è una buona pratica. Il nome dell'annotazione relativa all'ambito non deve indicare esplicitamente lo scopo dell'annotazione soddisfa. Dovrebbe invece essere denominato in base alla sua durata le annotazioni possono essere riutilizzate da componenti di pari livello come RegistrationComponent e SettingsComponent. Ecco perché dovresti chiamarla @ActivityScope di @LoginScope.

Kotlin

// Definition of a custom scope called ActivityScope
@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope

// Classes annotated with @ActivityScope are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@ActivityScope
@Subcomponent
interface LoginComponent { ... }

// A unique instance of LoginViewModel is provided in Components
// annotated with @ActivityScope
@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

// Definition of a custom scope called ActivityScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {}

// Classes annotated with @ActivityScope are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@ActivityScope
@Subcomponent
public interface LoginComponent { ... }

// A unique instance of LoginViewModel is provided in Components
// annotated with @ActivityScope
@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Ora, se avessi due frammenti che richiedono LoginViewModel, saranno entrambi forniti con la stessa istanza. Ad esempio, se disponi di una LoginUsernameFragment e LoginPasswordFragment devono essere iniettati dal LoginComponent:

Kotlin

@ActivityScope
@Subcomponent
interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment
    // request injection from LoginComponent. The graph needs to satisfy
    // all the dependencies of the fields those classes are injecting
    fun inject(loginActivity: LoginActivity)
    fun inject(usernameFragment: LoginUsernameFragment)
    fun inject(passwordFragment: LoginPasswordFragment)
}

Java

@ActivityScope
@Subcomponent
public interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment
    // request injection from LoginComponent. The graph needs to satisfy
    // all the dependencies of the fields those classes are injecting
    void inject(LoginActivity loginActivity);
    void inject(LoginUsernameFragment loginUsernameFragment);
    void inject(LoginPasswordFragment loginPasswordFragment);
}

I componenti accedono all'istanza del componente che si trova nella LoginActivity oggetto. Il codice di esempio per LoginUserNameFragment è riportato in seguente snippet di codice:

Kotlin

class LoginUsernameFragment: Fragment() {

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        // Obtaining the login graph from LoginActivity and instantiate
        // the @Inject fields with objects from the graph
        (activity as LoginActivity).loginComponent.inject(this)

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Java

public class LoginUsernameFragment extends Fragment {

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        // Obtaining the login graph from LoginActivity and instantiate
        // the @Inject fields with objects from the graph
        ((LoginActivity) getActivity()).loginComponent.inject(this);

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

E lo stesso per LoginPasswordFragment:

Kotlin

class LoginPasswordFragment: Fragment() {

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        (activity as LoginActivity).loginComponent.inject(this)

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Java

public class LoginPasswordFragment extends Fragment {

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        ((LoginActivity) getActivity()).loginComponent.inject(this);

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

La Figura 3 mostra l'aspetto del grafico Dagger con il nuovo sottocomponente. Le classi con un punto bianco (UserRepository, LoginRetrofitService e LoginViewModel) sono quelle con un'istanza univoca con l'ambito dei rispettivi componenti.

Grafico dell'applicazione dopo l'aggiunta dell'ultimo sottocomponente

Figura 3. Rappresentazione del grafico che hai creato per l'app per Android, ad esempio

Analizziamo le parti del grafico:

  1. NetworkModule (e quindi LoginRetrofitService) è incluso in ApplicationComponent perché lo hai specificato nel componente.

  2. UserRepository rimane in ApplicationComponent perché ha come ambito il ApplicationComponent. Se il progetto cresce, condividete gli stessi all'istanza in diverse funzionalità (ad esempio, registrazione).

    Poiché UserRepository fa parte di ApplicationComponent, le sue dipendenze (ad es. UserLocalDataSource e UserRemoteDataSource) devono essere in questo per poter fornire istanze di UserRepository.

  3. LoginViewModel è incluso in LoginComponent perché è obbligatorio dai corsi inseriti da LoginComponent. LoginViewModel non è incluso in ApplicationComponent perché nessuna dipendenza nelle esigenze di ApplicationComponent LoginViewModel.

    Allo stesso modo, se non hai limitato l'ambito UserRepository a ApplicationComponent, Dagger avrebbe incluso automaticamente UserRepository e le sue dipendenze nell'ambito di LoginComponent perché al momento è l'unico luogo È in uso UserRepository.

Oltre a definire l'ambito degli oggetti in base a un diverso ciclo di vita, la creazione di sottocomponenti è è una buona pratica per incapsulare parti diverse dell'applicazione l'uno dall'altro.

Strutturare l'app per creare diversi sottografi Dagger a seconda del flusso della tua app contribuisce a creare un'applicazione più efficiente e scalabile nel di memoria e tempo di avvio.

Best practice per la creazione di un grafico Dagger

Quando crei il grafico Dagger per la tua applicazione:

  • Quando crei un componente, devi considerare quale elemento è responsabile per tutta la durata di quel componente. In questo caso, il corso Application si trova responsabile di ApplicationComponent, mentre LoginActivity si occupa di LoginComponent.

  • Utilizza la definizione dell'ambito solo quando opportuno. Un uso eccessivo della definizione dell'ambito può avere un sulle prestazioni di runtime dell'app: l'oggetto rimane in memoria per il tempo poiché il componente è in memoria e ottenere un oggetto con ambito è più costoso. Quando Dagger fornisce l'oggetto, utilizza il blocco DoubleCheck anziché un di fabbrica.

Test di un progetto che utilizza Dagger

Uno dei vantaggi dell'utilizzo di framework di inserimento delle dipendenze come Dagger è che semplifica il test del codice.

Test delle unità

Non è necessario utilizzare Dagger per i test delle unità. Durante il test di un corso che utilizza costruttore, non devi usare Dagger per creare un'istanza di quella classe. Puoi chiamare direttamente il suo costruttore che passa in dipendenze false o fittizie come faresti se non venissero annotate.

Ad esempio, durante il test di LoginViewModel:

Kotlin

@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

class LoginViewModelTest {

    @Test
    fun `Happy path`() {
        // You don't need Dagger to create an instance of LoginViewModel
        // You can pass a fake or mock UserRepository
        val viewModel = LoginViewModel(fakeUserRepository)
        assertEquals(...)
    }
}

Java

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

public class LoginViewModelTest {

    @Test
    public void happyPath() {
        // You don't need Dagger to create an instance of LoginViewModel
        // You can pass a fake or mock UserRepository
        LoginViewModel viewModel = new LoginViewModel(fakeUserRepository);
        assertEquals(...);
    }
}

Test end-to-end

Per i test di integrazione, è buona norma creare un'istanza TestApplicationComponent pensato per i test. La produzione e i test utilizzano una configurazione dei componenti diversa.

Ciò richiede una progettazione iniziale dei moduli in la tua applicazione. Il componente di test estende il componente di produzione e un altro insieme di moduli.

Kotlin

// TestApplicationComponent extends from ApplicationComponent to have them both
// with the same interface methods. You need to include the modules of the
// component here as well, and you can replace the ones you want to override.
// This sample uses FakeNetworkModule instead of NetworkModule
@Singleton
@Component(modules = [FakeNetworkModule::class, SubcomponentsModule::class])
interface TestApplicationComponent : ApplicationComponent {
}

Java

// TestApplicationComponent extends from ApplicationComponent to have them both
// with the same interface methods. You need to include the modules of the
// Component here as well, and you can replace the ones you want to override.
// This sample uses FakeNetworkModule instead of NetworkModule
@Singleton
@Component(modules = {FakeNetworkModule.class, SubcomponentsModule.class})
public interface TestApplicationComponent extends ApplicationComponent {
}

FakeNetworkModule ha una falsa implementazione del modello NetworkModule originale. Qui puoi fornire istanze false o simulazioni di ciò che vuoi sostituire.

Kotlin

// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService
// that you can use in your tests.
@Module
class FakeNetworkModule {
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService {
        return FakeLoginService()
    }
}

Java

// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService
// that you can use in your tests.
@Module
public class FakeNetworkModule {

    @Provides
    public LoginRetrofitService provideLoginRetrofitService() {
        return new FakeLoginService();
    }
}

Nei test di integrazione o end-to-end, utilizzerai TestApplication che crea TestApplicationComponent invece di ApplicationComponent.

Kotlin

// Your test application needs an instance of the test graph
class MyTestApplication: MyApplication() {
    override val appComponent = DaggerTestApplicationComponent.create()
}

Java

// Your test application needs an instance of the test graph
public class MyTestApplication extends MyApplication {
    ApplicationComponent appComponent = DaggerTestApplicationComponent.create();
}

Questa applicazione di test viene quindi usata in un oggetto TestRunner personalizzato che userai per per eseguire test di strumentazione. Per ulteriori informazioni in merito, consulta la Guida di riferimento Dagger nel codelab delle tue app per Android.

Utilizzo dei moduli Dagger

I moduli Dagger sono un modo per incapsulare come fornire oggetti in una in molti modi diversi. Puoi includere moduli nei componenti, ma anche moduli all'interno di altri moduli. Si tratta di una funzionalità potente, ma può essere facilmente utilizzata in modo improprio.

Dopo aver aggiunto un modulo a un componente o a un altro modulo, già nel grafico Dagger; Dagger può fornire questi oggetti nel componente. Prima di aggiungere un modulo, controlla se fa già parte del grafico Dagger controllando se è già stata aggiunta al componente oppure compilando il progetto per vedere se Dagger riesce a trovare le dipendenze richieste per quel modulo.

È buona prassi che i moduli vengano dichiarati una sola volta in un componente (al di fuori di specifici casi d'uso avanzati di Dagger).

Supponiamo che il grafico sia configurato in questo modo. ApplicationComponent include Module1 e Module2, mentre Module1 include ModuleX.

Kotlin

@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class})
public interface ApplicationComponent { ... }

@Module(includes = {ModuleX.class})
public class Module1 { ... }

@Module
public class Module2 { ... }

Se ora Module2 dipende dai corsi forniti da ModuleX. Una cattiva pratica include ModuleX in Module2 perché ModuleX è incluso due volte in il grafico, come illustrato nello snippet di codice riportato di seguito:

Kotlin

// Bad practice: ModuleX is declared multiple times in this Dagger graph
@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module(includes = [ModuleX::class])
class Module2 { ... }

Java

// Bad practice: ModuleX is declared multiple times in this Dagger graph.
@Component(modules = {Module1.class, Module2.class})
public interface ApplicationComponent { ... }

@Module(includes = ModuleX.class)
public class Module1 { ... }

@Module(includes = ModuleX.class)
public class Module2 { ... }

Devi procedere in uno dei seguenti modi:

  1. Esegui il refactoring dei moduli ed estrai il modulo comune al di strumento di authoring.
  2. Crea un nuovo modulo con gli oggetti che entrambi i moduli condividono ed estraggono al componente.

Se non si esegue il refactoring in questo modo si generano molti moduli che si includono tra loro senza un chiaro senso di organizzazione e rendendo più difficile vedere dove da cui proviene ciascuna dipendenza.

Buone prassi (opzione 1): ModuleX viene dichiarato una volta nel grafico Dagger.

Kotlin

@Component(modules = [Module1::class, Module2::class, ModuleX::class])
interface ApplicationComponent { ... }

@Module
class Module1 { ... }

@Module
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class, ModuleX.class})
public interface ApplicationComponent { ... }

@Module
public class Module1 { ... }

@Module
public class Module2 { ... }

Buona pratica (opzione 2): dipendenze comuni da Module1 e Module2 in ModuleX vengono estratte in un nuovo modulo denominato ModuleXCommon che è incluso nel componente. Quindi altri due moduli denominati ModuleXWithModule1Dependencies e ModuleXWithModule2Dependencies sono e le dipendenze specifiche di ciascun modulo. Tutti i moduli vengono dichiarati una volta nel grafico Dagger.

Kotlin

@Component(modules = [Module1::class, Module2::class, ModuleXCommon::class])
interface ApplicationComponent { ... }

@Module
class ModuleXCommon { ... }

@Module
class ModuleXWithModule1SpecificDependencies { ... }

@Module
class ModuleXWithModule2SpecificDependencies { ... }

@Module(includes = [ModuleXWithModule1SpecificDependencies::class])
class Module1 { ... }

@Module(includes = [ModuleXWithModule2SpecificDependencies::class])
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class, ModuleXCommon.class})
public interface ApplicationComponent { ... }

@Module
public class ModuleXCommon { ... }

@Module
public class ModuleXWithModule1SpecificDependencies { ... }

@Module
public class ModuleXWithModule2SpecificDependencies { ... }

@Module(includes = ModuleXWithModule1SpecificDependencies.class)
public class Module1 { ... }

@Module(includes = ModuleXWithModule2SpecificDependencies.class)
public class Module2 { ... }

Iniezione assistita

L'inserimento assistito è un pattern DI utilizzato per costruire un oggetto in cui alcuni parametri possono essere forniti dal framework DI mentre altri devono essere passati al momento della creazione da parte dell'utente.

In Android, questo pattern è comune nelle schermate dei dettagli in cui l'ID del parametro l'elemento da mostrare è noto solo in fase di runtime, non in fase di compilazione, quando Dagger genera il grafico DI. Per scoprire di più sull'iniezione assistita con Dagger, consulta la documentazione di Dagger.

Conclusione

Se non l'hai già fatto, consulta la sezione delle best practice. A scopri come utilizzare Dagger in un'app Android, leggi l'articolo Utilizzare Dagger in un'app Android codelab.