Uso di Dagger nelle app per Android

La pagina Nozioni di base su Dagger spiega come Dagger può aiutarti ad automatizzare l'inserimento delle dipendenze nella tua app. Con Dagger non è necessario scrivere codice boilerplate noioso e soggetto a errori.

Riepilogo delle best practice

  • Utilizza l'inserimento del costruttore con @Inject per aggiungere tipi al grafico Dagger quando è possibile. Se non è corretta:
    • Utilizza @Binds per indicare a Dagger quale implementazione dovrebbe avere un'interfaccia.
    • Usa @Provides per indicare a Dagger come fornire corsi che non appartengono al tuo progetto.
  • I moduli devono essere dichiarati una sola volta in un componente.
  • Assegna un nome alle annotazioni dell'ambito in base alla durata in cui viene utilizzata l'annotazione. Tra gli esempi vi sono @ApplicationScope, @LoggedUserScope e @ActivityScope.

Aggiunta di dipendenze

Per utilizzare Dagger nel tuo progetto, aggiungi queste dipendenze all'applicazione nel file build.gradle. Puoi trovare la versione più recente 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'
}

Pugnale in Android

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

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

Figura 1. Grafico delle dipendenze del codice di esempio

In Android, solitamente crei un grafico Dagger che si trova nella classe dell'applicazione perché vuoi che un'istanza del grafico sia in memoria finché l'app è in esecuzione. In questo modo, il grafico viene collegato al ciclo di vita dell'app. In alcuni casi, è consigliabile anche che il contesto dell'applicazione sia disponibile nel grafico. Per questo motivo, è necessario che il grafico sia nella classe Application. Un vantaggio di questo approccio è che il grafico è disponibile per altre classi di framework Android. Inoltre, semplifica i test consentendoti di utilizzare una classe Application personalizzata nei test.

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

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 framework Android, come attività e frammenti, sono infondate dal sistema, Dagger non può crearle per te. Per le attività specifiche, qualsiasi codice di inizializzazione deve essere inserito nel metodo onCreate(). Ciò significa che non puoi utilizzare l'annotazione @Inject nel costruttore della classe (iniezione del costruttore) come negli esempi precedenti. Devi invece utilizzare l'inserimento dei campi.

Invece di creare le dipendenze richieste da un'attività nel metodo onCreate(), vuoi che Dagger le completi per te. Per l'inserimento dei campi, applichi invece l'annotazione @Inject ai campi che vuoi ottenere 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 ViewModel dei componenti dell'architettura Android; è semplicemente una classe normale che agisce come ViewModel. Per ulteriori informazioni su come inserire queste classi, controlla il codice nell'implementazione ufficiale di Android Blueprints Dagger, nel ramo dev-dagger.

Una delle considerazioni relative a Dagger è che i campi inseriti non possono essere privati. Devono avere almeno la visibilità dei pacchetti privati come nel codice precedente.

Inserimento di attività

Dagger deve sapere che LoginActivity deve accedere al grafico per fornire il valore ViewModel di cui ha bisogno. Nella pagina Nozioni di base su Dagger, hai utilizzato l'interfaccia @Component per recuperare oggetti dal grafico esponendo funzioni con il tipo restituito di ciò che vuoi ottenere dal grafico. In questo caso, devi comunicare a Dagger la presenza di un oggetto (LoginActivity in questo caso) che richiede l'inserimento di una dipendenza. Per farlo, devi esporre 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 indica a Dagger che LoginActivity vuole accedere al grafico e richiede l'inserimento. Dagger deve soddisfare tutte le dipendenze richieste da LoginActivity (LoginViewModel con le proprie dipendenze). Se sono presenti più classi che richiedono l'inserimento, devi dichiararle in modo specifico nel componente con il tipo esatto. Ad esempio, se LoginActivity e RegistrationActivity richiedono l'inserimento, avresti due metodi inject() anziché uno generico che copre entrambi i casi. Un metodo inject() generico non indica a Dagger cosa deve essere fornito. Le funzioni nell'interfaccia possono avere qualsiasi nome, ma la chiamata inject() quando ricevono l'oggetto da inserire come parametro è una convenzione in Dagger.

Per inserire un oggetto nell'attività, devi utilizzare appComponent definito nella classe Application e chiamare il metodo inject(), passando un'istanza dell'attività che richiede l'inserimento.

Quando utilizzi le attività, inserisci Dagger nel 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 i frammenti che potrebbero richiedere l'accesso alle associazioni di attività.

Quando utilizzi i frammenti, inserisci Dagger nel metodo onAttach() del frammento. In questo caso, puoi farlo 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 il resto delle dipendenze per creare il 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 Dagger

Per questo esempio stai utilizzando la libreria di networking Retrofit. UserRemoteDataSource ha una dipendenza da LoginRetrofitService. Tuttavia, il modo per creare un'istanza di LoginRetrofitService è diverso da quello che hai utilizzato finora. Non è un'istanza di classe, ma il risultato della chiamata a Retrofit.Builder() e del trasferimento di parametri diversi per configurare il servizio di accesso.

Oltre all'annotazione @Inject, esiste un altro modo per indicare a Dagger come fornire un'istanza di una classe: le informazioni all'interno dei moduli Dagger. Un modulo Dagger è una classe annotata con @Module. Qui puoi definire le dipendenze 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 oggetti. Come puoi vedere, hai chiamato la classe NetworkModule per raggruppare la logica di fornitura degli oggetti relativi al networking. Se l'applicazione si espande, puoi anche aggiungere come fornire un OkHttpClient qui o come configurare Gson o Moshi.

Le dipendenze di un metodo @Provides sono i parametri di tale metodo. Per il metodo precedente, è possibile fornire LoginRetrofitService senza dipendenze perché il metodo non ha parametri. Se avessi dichiarato OkHttpClient come parametro, Dagger dovrebbe fornire un'istanza OkHttpClient dal grafico per soddisfare le dipendenze di LoginRetrofitService. Ecco alcuni esempi:

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 sia a conoscenza di questo modulo, devi aggiungerlo all'interfaccia @Component nel seguente 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 consiste nell'utilizzare l'iniezione del costruttore (ovvero con l'annotazione @Inject sul costruttore della classe). A volte non è possibile e occorre utilizzare i moduli Dagger. Ad esempio, vuoi che Dagger utilizzi il risultato di un calcolo per determinare come creare un'istanza di un oggetto. Ogni volta che deve fornire un'istanza di quel tipo, Dagger esegue il codice all'interno del metodo @Provides.

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

Diagramma del grafico delle dipendenze LoginActivity

Figura 2. Rappresentazione del grafico con l'inserimento di LoginActivity da parte di Dagger

Il punto di accesso al grafico è LoginActivity. Poiché LoginActivity inserisce LoginViewModel, Dagger crea un grafico che sa come fornire un'istanza di LoginViewModel e in modo ricorsivo, delle sue dipendenze. Dagger sa come farlo grazie all'annotazione @Inject nel costruttore delle classi.

All'interno dell'elemento ApplicationComponent generato da Dagger è disponibile un metodo di tipo fabbrica per recuperare istanze di tutte le classi che sa fornire. In questo esempio, Dagger delega all'elemento NetworkModule incluso in ApplicationComponent per ottenere un'istanza di LoginRetrofitService.

Ambiti Dagger

Gli ambiti sono stati menzionati nella pagina Nozioni di base su Dagger come modo per avere un'istanza univoca di un tipo in un componente. Questo è il significato dell'ambito di un tipo nel ciclo di vita del componente.

Poiché potresti voler utilizzare UserRepository in altre funzionalità dell'app e non creare un nuovo oggetto ogni volta che ne hai bisogno, puoi designarlo come istanza univoca per l'intera app. Lo stesso vale per LoginRetrofitService: può essere costoso da creare e vuoi anche che venga riutilizzata un'istanza unica di quell'oggetto. La creazione di un'istanza di UserRemoteDataSource non è così costosa, quindi non è necessario inserirla nell'ambito del ciclo di vita del componente.

@Singleton è l'unica annotazione dell'ambito fornita con il pacchetto javax.inject. Puoi utilizzarlo 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. Finché il componente con ambito è in memoria, anche l'oggetto creato è in memoria. Poiché l'app ApplicationComponent viene creata all'avvio dell'app (nella classe Application), viene eliminata con l'eliminazione. Di conseguenza, l'istanza univoca di UserRepository rimane sempre in memoria fino all'eliminazione dell'applicazione.

Sottocomponenti Pugnale

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

  1. L'istanza di LoginViewModel rimarrà in memoria al termine del flusso.

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

Per limitare l'ambito di LoginViewModel al ciclo di vita di LoginActivity, devi creare un nuovo componente (un nuovo sottografico) 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. Questa operazione elimina la responsabilità di inserire LoginActivity dalla classe 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. Per comunicare a Dagger che vuoi che un nuovo componente utilizzi parte di un altro componente, utilizza i sottocomponenti Dagger. Il nuovo componente deve essere un sottocomponente del componente contenente risorse condivise.

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

Per creare istanze dei sottocomponenti, è necessaria un'istanza del componente padre. Di conseguenza, l'ambito degli oggetti forniti dal componente padre al sottocomponente è sempre quello del componente principale.

Nell'esempio, devi definire LoginComponent come sottocomponente di ApplicationComponent. Per farlo, annota LoginComponent con @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 inoltre definire una fabbrica di componenti secondari all'interno di LoginComponent per far sì che ApplicationComponent sappia 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 comunicare a Dagger che LoginComponent è un sottocomponente di ApplicationComponent, devi indicarlo in questo modo:

  1. Creazione di un nuovo modulo Dagger (ad es. SubcomponentsModule) per trasmettere la 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 es. 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 perché questa responsabilità ora appartiene a LoginComponent, quindi puoi rimuovere il metodo inject() da ApplicationComponent.

    I consumatori di ApplicationComponent devono sapere come creare istanze di LoginComponent. Il componente padre deve aggiungere un metodo nella sua interfaccia per consentire ai consumatori di creare istanze del sottocomponente da un'istanza del componente padre:

  3. Esponi la fabbrica che crea istanze di LoginComponent nell'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 sottocomponenti

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

Qual è il ciclo di vita di LoginComponent? Uno dei motivi per cui hai richiesto LoginComponent è che hai dovuto condividere la stessa istanza di LoginViewModel tra frammenti relativi all'accesso. Inoltre, ti conviene creare istanze di LoginViewModel diverse ogni volta che è disponibile un nuovo flusso di accesso. LoginActivity sia la durata giusta per LoginComponent: per ogni nuova attività, sono necessarie una nuova istanza di LoginComponent e frammenti che possano utilizzare l'istanza di LoginComponent.

Poiché LoginComponent è collegato al ciclo di vita di LoginActivity, devi mantenere un riferimento al componente nell'attività nello stesso modo in cui hai mantenuto il riferimento al applicationComponent nella classe Application. In questo modo, i frammenti possono 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;

    ...
}

Nota che la variabile loginComponent non è annotata con @Inject perché non ti aspetti che venga fornita da Dagger.

Puoi utilizzare ApplicationComponent per ottenere un riferimento a LoginComponent e quindi inserire 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 verrà eliminata in modo implicito con l'eliminazione dell'attività.

LoginComponent deve sempre fornire la stessa istanza di LoginViewModel ogni volta che viene richiesto. A questo scopo, crea un ambito di annotazioni personalizzato e aggiungi annotazioni a LoginComponent e LoginViewModel con questo ambito. Tieni presente che non puoi utilizzare l'annotazione @Singleton perché è già stata utilizzata dal componente padre e questo renderebbe l'oggetto un singleton di applicazione (istanza unica per l'intera app). Devi creare un ambito di annotazione diverso.

In questo caso, potresti aver chiamato questo ambito @LoginScope, ma non è una buona pratica. Il nome dell'annotazione dell'ambito non deve essere esplicito allo scopo che soddisfa. Dovrebbe invece essere denominato in base alla sua durata perché le annotazioni possono essere riutilizzate da componenti di pari livello come RegistrationComponent e SettingsComponent. Per questo motivo dovresti chiamarlo @ActivityScope anziché @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 avevi due frammenti che richiedono LoginViewModel, entrambi vengono forniti con la stessa istanza. Ad esempio, se hai un LoginUsernameFragment e un LoginPasswordFragment, questi devono essere inseriti da 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 risiede nell'oggetto LoginActivity. Il codice di esempio per LoginUserNameFragment viene visualizzato nel 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 vale 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 la cui istanza univoca è limitata ai rispettivi componenti.

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

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

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: ApplicationComponent. Se il progetto si ingrandisce, vuoi condividere la stessa istanza in diverse funzionalità (ad es. la registrazione).

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

  3. LoginViewModel è incluso in LoginComponent perché è richiesto solo dalle classi inserite da LoginComponent. LoginViewModel non è incluso in ApplicationComponent perché nessuna dipendenza in ApplicationComponent richiede LoginViewModel.

    Allo stesso modo, se non avessi impostato l'ambito UserRepository in ApplicationComponent, Dagger avrebbe incluso automaticamente UserRepository e le sue dipendenze come parte di LoginComponent perché questa è attualmente l'unica posizione in cui viene utilizzato UserRepository.

A parte l'ambito degli oggetti per un ciclo di vita diverso, la creazione di sottocomponenti è una buona pratica per incapsulare parti diverse dell'applicazione l'una dall'altra.

La strutturazione dell'app in modo da creare diversi sottografi di Dagger a seconda del flusso dell'app contribuisce a rendere un'applicazione più efficiente e scalabile in termini 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 la sua durata. In questo caso, la classe Application ha il compito di ApplicationComponent, mentre LoginActivity di LoginComponent.

  • Utilizza la definizione dell'ambito solo quando appropriato. L'uso eccessivo dell'ambito può avere un effetto negativo sulle prestazioni di runtime dell'app: l'oggetto è in memoria purché il componente sia in memoria e il recupero di un oggetto con ambito sia più costoso. Quando Dagger fornisce l'oggetto, utilizza il blocco DoubleCheck anziché un provider di tipo di fabbrica.

Test di un progetto che utilizza Dagger

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

Test delle unità

Non è necessario utilizzare Dagger per i test delle unità. Quando testi una classe che utilizza l'inserimento dei costruttori, non è necessario utilizzare Dagger per creare un'istanza di quella classe. Puoi chiamare direttamente il suo costruttore che trasmette in dipendenze false o fittizie direttamente come faresti se non fossero 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 elemento TestApplicationComponent destinato ai test. La produzione e i test utilizzano una configurazione dei componenti diversa.

Ciò richiede una progettazione più iniziale dei moduli nell'applicazione. Il componente di test estende il componente di produzione e installa un insieme diverso 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 un'implementazione falsa del NetworkModule originale. Qui puoi fornire istanze false o simulazioni di qualsiasi cosa tu voglia 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, dovresti utilizzare un TestApplication che crea un TestApplicationComponent invece di un 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();
}

Quindi, questa applicazione di test viene utilizzata in un TestRunner personalizzato che utilizzerai per eseguire i test di strumentazione. Per ulteriori informazioni, consulta la pagina relativa all'utilizzo di Dagger nel codelab della tua app Android.

Utilizzo dei moduli Dagger

I moduli Dagger sono un modo per incapsulare il modo in cui fornire oggetti in modo semantico. Puoi includere moduli nei componenti, ma anche all'interno di altri moduli. È una soluzione potente, ma può essere facilmente usata in modo improprio.

Dopo aver aggiunto un modulo a un componente o a un altro modulo, questo è già presente nel grafico Dagger. Dagger è in grado di fornire questi oggetti in quel componente. Prima di aggiungere un modulo, verifica se quel modulo fa già parte del grafico Dagger controllando se è già stato aggiunto al componente o compilando il progetto e verificando se Dagger riesce a trovare le dipendenze richieste per quel modulo.

La buona pratica stabilisce che i moduli devono essere dichiarati una sola volta in un componente (al di fuori dei 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 nel grafico, come mostrato nel seguente snippet di codice:

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 eseguire invece una delle seguenti operazioni:

  1. Esegui il refactoring dei moduli ed estrai il modulo comune nel componente.
  2. Crea un nuovo modulo con gli oggetti condivisi da entrambi i moduli e li estrae nel componente.

Questo refactoring non comporta l'inclusione di molti moduli senza un chiaro senso di organizzazione e rendendo più difficile capire da dove proviene ciascuna dipendenza.

Buona prassi (opzione 1): il modulo 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): le dipendenze comuni da Module1 e Module2 in ModuleX vengono estratte in un nuovo modulo denominato ModuleXCommon incluso nel componente. Successivamente, vengono creati altri due moduli denominati ModuleXWithModule1Dependencies e ModuleXWithModule2Dependencies con le dipendenze specifiche di ciascun modulo. Tutti i moduli vengono dichiarati una sola 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 che viene utilizzato per creare un oggetto in cui alcuni parametri possono essere forniti dal framework DI e altri devono essere trasmessi dall'utente al momento della creazione.

In Android, questo pattern è comune nelle schermate dei dettagli in cui l'ID dell'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'inserimento assistito di Dagger, consulta la documentazione di Dagger.

conclusione

Se non lo hai già fatto, consulta la sezione delle best practice. Per scoprire come utilizzare Dagger in un'app per Android, consulta la pagina relativa all'utilizzo di Dagger in un codelab per app per Android.