Utilizzare pugnale nelle app con più moduli

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.

Figura 1. Esempio di grafico Dagger in un progetto multimodulo

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.

Figura 2. Esempio di grafico Dagger in un progetto con moduli di funzionalità

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 modulo app.

  • 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 modulo app.