Un progetto con più moduli Gradle è noto come progetto multimodulo.
In un progetto multimodulo fornito come singolo APK senza moduli di funzionalità, è comune avere un modulo app
che può dipendere dalla maggior parte dei moduli del progetto e un modulo base
o core
da cui dipendono in genere gli altri moduli. Il modulo app
in genere contiene la tua classe Application
, mentre il modulo base
contiene tutte le classi comuni condivise in tutti i moduli del progetto.
Il modulo app
è utile per dichiarare il componente dell'applicazione (ad esempio, ApplicationComponent
nell'immagine di seguito) in grado di fornire oggetti di cui altri componenti potrebbero aver bisogno, oltre ai singleton della tua app. Ad esempio, classi come OkHttpClient
, parser JSON, funzioni di accesso per il tuo database o oggetti SharedPreferences
che possono essere definite nel modulo core
saranno fornite dal ApplicationComponent
definito nel modulo app
.
Nel modulo app
, potresti anche avere altri componenti con una durata minore.
Un esempio potrebbe essere un UserComponent
con una configurazione specifica dell'utente (come UserSession
) dopo un accesso.
Nei diversi moduli del progetto, puoi definire almeno un sottocomponente con logica specifica per quel modulo, come mostrato nella Figura 1.
Ad esempio, in un modulo login
potresti avere un ambito LoginComponent
con un'annotazione @ModuleScope
personalizzata che può fornire oggetti comuni a quella funzionalità, ad esempio LoginRepository
. All'interno di questo modulo puoi anche avere
altri componenti che dipendono da un elemento LoginComponent
con un ambito
personalizzato diverso, ad esempio @FeatureScope
per LoginActivityComponent
o
TermsAndConditionsComponent
, in cui puoi definire l'ambito con logiche più specifiche delle funzionalità,
come gli oggetti ViewModel
.
Per altri moduli, ad esempio Registration
, la configurazione sarà simile.
Una regola generale per un progetto multimodulo è che i moduli dello stesso livello non dovrebbero dipendere l'uno dall'altro. In caso affermativo, valuta se quella logica condivisa (le dipendenze tra loro) deve far parte del modulo padre. In tal caso, esegui il refactoring per spostare le classi nel modulo padre. In caso contrario, crea un nuovo modulo che espanda il modulo padre e fai in modo che entrambi i moduli originali estendono il nuovo modulo.
Come best practice, in genere crei un componente in un modulo nei seguenti casi:
Devi eseguire l'inserimento dei campi, come nel caso delle
LoginActivityComponent
.Devi definire l'ambito degli oggetti, come per
LoginComponent
.
Se nessuno di questi casi è applicabile e devi indicare a Dagger come fornire oggetti da quel modulo, crea ed esponi un modulo Dagger con i metodi @Provides
o @Binds
se l'inserimento della creazione non è possibile per queste classi.
Implementazione con i sottocomponenti Dagger
La pagina del documento Utilizzo di Dagger nelle app per Android spiega come creare e utilizzare
sottocomponenti. Tuttavia, non puoi utilizzare lo stesso codice perché i moduli di funzionalità non conoscono il modulo app
. Ad esempio, se pensi a un tipico flusso di accesso e al codice che abbiamo nella pagina precedente, non viene più compilato:
Kotlin
class LoginActivity: Activity() { ... 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) ... } }
Java
public class LoginActivity extends Activity { ... @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); ... } }
Il motivo è che il modulo login
non è a conoscenza di MyApplication
né di appComponent
. Affinché funzioni, devi definire un'interfaccia nel modulo delle funzionalità che fornisca un elemento FeatureComponent
che MyApplication
deve implementare.
Nell'esempio seguente, puoi definire un'interfaccia LoginComponentProvider
che fornisce un valore LoginComponent
nel modulo login
per il flusso di accesso:
Kotlin
interface LoginComponentProvider { fun provideLoginComponent(): LoginComponent }
Java
public interface LoginComponentProvider { public LoginComponent provideLoginComponent(); }
Ora LoginActivity
utilizzerà questa interfaccia anziché lo snippet di codice sopra definito:
Kotlin
class LoginActivity: Activity() { ... override fun onCreate(savedInstanceState: Bundle?) { loginComponent = (applicationContext as LoginComponentProvider) .provideLoginComponent() loginComponent.inject(this) ... } }
Java
public class LoginActivity extends Activity { ... @Override protected void onCreate(Bundle savedInstanceState) { loginComponent = ((LoginComponentProvider) getApplicationContext()) .provideLoginComponent(); loginComponent.inject(this); ... } }
Ora MyApplication
deve implementare questa interfaccia e implementare i
metodi richiesti:
Kotlin
class MyApplication: Application(), LoginComponentProvider { // Reference to the application graph that is used across the whole app val appComponent = DaggerApplicationComponent.create() override fun provideLoginComponent(): LoginComponent { return appComponent.loginComponent().create() } }
Java
public class MyApplication extends Application implements LoginComponentProvider { // Reference to the application graph that is used across the whole app ApplicationComponent appComponent = DaggerApplicationComponent.create(); @Override public LoginComponent provideLoginComponent() { return appComponent.loginComponent.create(); } }
Ecco come puoi utilizzare i sottocomponenti Dagger in un progetto multimodulo. Con i moduli delle caratteristiche, la soluzione è diversa a causa del modo in cui i moduli dipendono l'uno dall'altro.
Dipendenze dei componenti con moduli delle funzionalità
Con i moduli di funzionalità, il modo in cui i moduli dipendono solitamente
l'uno dall'altro viene invertito. Anziché il modulo app
che include i moduli
delle funzionalità, i moduli delle funzionalità dipendono dal modulo app
. Vedi la Figura 2 per una rappresentazione di come sono strutturati i moduli.
In Dagger, i componenti devono conoscere i relativi componenti secondari. Queste informazioni sono incluse in un modulo Dagger aggiunto al componente principale (come il modulo SubcomponentsModule
in Utilizzo di Dagger nelle app Android).
Sfortunatamente, con la dipendenza invertita tra l'app e il modulo delle funzionalità, il sottocomponente non è visibile nel modulo app
perché non si trova nel percorso di build. Ad esempio, un LoginComponent
definito in un
modulo della funzionalità login
non può essere un sottocomponente di
ApplicationComponent
definito nel modulo app
.
Dagger ha un meccanismo chiamato dipendenze dei componenti che puoi utilizzare per risolvere questo problema. Anziché essere un componente secondario del componente principale, quest'ultimo dipende da quest'ultimo. Non c'è alcuna relazione padre-figlio; ora i componenti dipendono dagli altri per ottenere determinate dipendenze. I componenti devono esporre i tipi dal grafico affinché i componenti dipendenti li utilizzino.
Ad esempio, un modulo delle funzionalità chiamato login
vuole creare una LoginComponent
che dipenda da AppComponent
disponibile nel modulo app
Gradle.
Di seguito sono riportate le definizioni dei corsi e di AppComponent
che fanno parte del
modulo Gradle app
:
Kotlin
// UserRepository's dependencies class UserLocalDataSource @Inject constructor() { ... } class UserRemoteDataSource @Inject constructor() { ... } // UserRepository is scoped to AppComponent @Singleton class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } @Singleton @Component interface AppComponent { ... }
Java
// UserRepository's dependencies public class UserLocalDataSource { @Inject public UserLocalDataSource() {} } public class UserRemoteDataSource { @Inject public UserRemoteDataSource() { } } // UserRepository is scoped to AppComponent @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; } } @Singleton @Component public interface ApplicationComponent { ... }
Nel tuo modulo Gradle login
che include il modulo Gradle app
, è presente un'istanza LoginActivity
che richiede l'inserimento di un'istanza LoginViewModel
:
Kotlin
// LoginViewModel depends on UserRepository that is scoped to AppComponent class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
Java
// LoginViewModel depends on UserRepository that is scoped to AppComponent public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } }
LoginViewModel
ha una dipendenza su UserRepository
che è disponibile e ha come ambito AppComponent
. Creiamo un LoginComponent
che dipende da AppComponent
per inserire LoginActivity
:
Kotlin
// Use the dependencies attribute in the Component annotation to specify the // dependencies of this Component @Component(dependencies = [AppComponent::class]) interface LoginComponent { fun inject(activity: LoginActivity) }
Java
// Use the dependencies attribute in the Component annotation to specify the // dependencies of this Component @Component(dependencies = AppComponent.class) public interface LoginComponent { void inject(LoginActivity loginActivity); }
LoginComponent
specifica una dipendenza su AppComponent
aggiungendola al
parametro delle dipendenze dell'annotazione del componente. Poiché LoginActivity
verrà inserito da Dagger, aggiungi il metodo inject()
all'interfaccia.
Quando crei un LoginComponent
, è necessario passare un'istanza di AppComponent
. Utilizza la fabbrica dei componenti per:
Kotlin
@Component(dependencies = [AppComponent::class]) interface LoginComponent { @Component.Factory interface Factory { // Takes an instance of AppComponent when creating // an instance of LoginComponent fun create(appComponent: AppComponent): LoginComponent } fun inject(activity: LoginActivity) }
Java
@Component(dependencies = AppComponent.class) public interface LoginComponent { @Component.Factory interface Factory { // Takes an instance of AppComponent when creating // an instance of LoginComponent LoginComponent create(AppComponent appComponent); } void inject(LoginActivity loginActivity); }
Ora LoginActivity
può creare un'istanza di LoginComponent
e chiamare il metodo inject()
.
Kotlin
class LoginActivity: Activity() { // You want Dagger to provide an instance of LoginViewModel from the Login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { // Gets appComponent from MyApplication available in the base Gradle module val appComponent = (applicationContext as MyApplication).appComponent // Creates a new instance of LoginComponent // Injects the component to populate the @Inject fields DaggerLoginComponent.factory().create(appComponent).inject(this) super.onCreate(savedInstanceState) // Now you can access loginViewModel } }
Java
public class LoginActivity extends Activity { // You want Dagger to provide an instance of LoginViewModel from the Login graph @Inject LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Gets appComponent from MyApplication available in the base Gradle module AppComponent appComponent = ((MyApplication) getApplicationContext()).appComponent; // Creates a new instance of LoginComponent // Injects the component to populate the @Inject fields DaggerLoginComponent.factory().create(appComponent).inject(this); // Now you can access loginViewModel } }
LoginViewModel
dipende da UserRepository
e affinché LoginComponent
sia in grado di accedervi da AppComponent
, AppComponent
deve esporlo nella sua interfaccia:
Kotlin
@Singleton @Component interface AppComponent { fun userRepository(): UserRepository }
Java
@Singleton @Component public interface AppComponent { UserRepository userRepository(); }
Le regole di definizione dell'ambito con componenti dipendenti funzionano come con i componenti secondari. Poiché LoginComponent
utilizza un'istanza di AppComponent
,
non può utilizzare la stessa annotazione dell'ambito.
Se volessi l'ambito LoginViewModel
in LoginComponent
, dovresti farlo come
in precedenza utilizzando l'annotazione @ActivityScope
personalizzata.
Kotlin
@ActivityScope @Component(dependencies = [AppComponent::class]) interface LoginComponent { ... } @ActivityScope class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
Java
@ActivityScope @Component(dependencies = AppComponent.class) public interface LoginComponent { ... } @ActivityScope public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } }
Best practice
ApplicationComponent
deve sempre essere nel moduloapp
.Crea componenti Dagger in moduli se devi eseguire l'inserimento dei campi in quel modulo o se devi definire l'ambito degli oggetti per un flusso specifico dell'applicazione.
Per i moduli Gradle che sono pensati per essere utilità o supporto e non hanno bisogno di creare un grafico (ecco perché è necessario un componente Dagger), crea ed esponi moduli Dagger pubblici con i metodi @Provides e @Binds di quelle classi che non supportano l'inserimento del costruttore.
Per utilizzare Dagger in un'app per Android con moduli di funzionalità, usa le dipendenze dei componenti per accedere alle dipendenze fornite dal criterio
ApplicationComponent
definito nel moduloapp
.