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.
- Usa
- 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.
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:
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:
L'istanza di
LoginViewModel
rimarrà in memoria dopo che il flusso è completato.Vuoi un'istanza diversa di
LoginViewModel
per ogni flusso di accesso. Ad esempio, se l'utente si disconnette, vuoi un'istanza diversa diLoginViewModel
, 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:
Crea un nuovo modulo Dagger (ad es.
SubcomponentsModule
) per superare della 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 esempio
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
poiché questa responsabilità ora appartiene aLoginComponent
, quindi puoi rimuovere il metodoinject()
daApplicationComponent
.I consumer di
ApplicationComponent
devono sapere come creare istanze diLoginComponent
. Il componente principale deve aggiungere un metodo nella sua interfaccia per consentire i consumer creano istanze del sottocomponente da un'istanza del componente principale:Esponi la fabbrica che crea istanze di
LoginComponent
nell'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.
Analizziamo le parti del grafico:
NetworkModule
(e quindiLoginRetrofitService
) è incluso inApplicationComponent
perché lo hai specificato nel componente.UserRepository
rimane inApplicationComponent
perché ha come ambito ilApplicationComponent
. Se il progetto cresce, condividete gli stessi all'istanza in diverse funzionalità (ad esempio, registrazione).Poiché
UserRepository
fa parte diApplicationComponent
, le sue dipendenze (ad es.UserLocalDataSource
eUserRemoteDataSource
) devono essere in questo per poter fornire istanze diUserRepository
.LoginViewModel
è incluso inLoginComponent
perché è obbligatorio dai corsi inseriti daLoginComponent
.LoginViewModel
non è incluso inApplicationComponent
perché nessuna dipendenza nelle esigenze diApplicationComponent
LoginViewModel
.Allo stesso modo, se non hai limitato l'ambito
UserRepository
aApplicationComponent
, Dagger avrebbe incluso automaticamenteUserRepository
e le sue dipendenze nell'ambito diLoginComponent
perché al momento è l'unico luogo È in usoUserRepository
.
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 diApplicationComponent
, mentreLoginActivity
si occupa diLoginComponent
.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:
- Esegui il refactoring dei moduli ed estrai il modulo comune al di strumento di authoring.
- 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.