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.
- Utilizza
- 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.
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:
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:
L'istanza di
LoginViewModel
rimarrà in memoria al termine del flusso.Vuoi un'istanza di
LoginViewModel
diversa per ogni flusso di accesso. Ad esempio, se l'utente si disconnette, vuoi un'istanza diversa diLoginViewModel
, 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:
Creazione di un nuovo modulo Dagger (ad es.
SubcomponentsModule
) per trasmettere la classe del sottocomponente all'attributosubcomponents
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 { }
Aggiunta del nuovo modulo (ad es.
SubcomponentsModule
) aApplicationComponent
: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ù inserireLoginActivity
perché questa responsabilità ora appartiene aLoginComponent
, quindi puoi rimuovere il metodoinject()
daApplicationComponent
.I consumatori di
ApplicationComponent
devono sapere come creare istanze diLoginComponent
. 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: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.
Analizziamo le parti del grafico:
NetworkModule
(e quindiLoginRetrofitService
) è incluso inApplicationComponent
perché lo hai specificato nel componente.UserRepository
rimane inApplicationComponent
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 diApplicationComponent
, anche le sue dipendenze (ovveroUserLocalDataSource
eUserRemoteDataSource
) devono essere presenti in questo componente per poter fornire istanze diUserRepository
.LoginViewModel
è incluso inLoginComponent
perché è richiesto solo dalle classi inserite daLoginComponent
.LoginViewModel
non è incluso inApplicationComponent
perché nessuna dipendenza inApplicationComponent
richiedeLoginViewModel
.Allo stesso modo, se non avessi impostato l'ambito
UserRepository
inApplicationComponent
, Dagger avrebbe incluso automaticamenteUserRepository
e le sue dipendenze come parte diLoginComponent
perché questa è attualmente l'unica posizione in cui viene utilizzatoUserRepository
.
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 diApplicationComponent
, mentreLoginActivity
diLoginComponent
.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:
- Esegui il refactoring dei moduli ed estrai il modulo comune nel componente.
- 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.