Utilizzare pugnale nelle app con più moduli

Un progetto con più moduli Gradle è noto come progetto multimodulo. In un progetto multimodulo che viene fornito come singolo APK senza funzionalità di moduli, è comune avere un modulo app che può dipendere dalla maggior parte moduli del tuo progetto e un modulo base o core che il resto del da cui dipendono generalmente i moduli. Il modulo app in genere contiene i tuoi Application, mentre base contiene tutti i corsi comuni condivisi tra tutti i moduli del tuo progetto.

Il modulo app è ideale per dichiarare il componente dell'applicazione (ad (ad esempio ApplicationComponent nell'immagine sotto) che possono fornire oggetti di cui potrebbero avere bisogno gli altri componenti, così come i singleton della tua app. Come ad esempio classi come OkHttpClient, parser JSON, funzioni di accesso per il database, o SharedPreferences oggetti che possono essere definiti nel modulo core, verrà fornito 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 per l'utente (come UserSession) dopo un accesso.

Nei diversi moduli del progetto, puoi definire almeno uno con una logica specifica per il modulo, come si vede nella Figura 1.

Figura 1. Esempio di grafo Dagger in una progetto multimodulo

Ad esempio, in un modulo login, potresti avere un LoginComponent con ambito con un'annotazione @ModuleScope personalizzata che può fornire oggetti comuni a quella funzionalità, ad esempio LoginRepository. All'interno di questo modulo, avere altri componenti che dipendono da un LoginComponent con un'impostazione ambito, ad esempio @FeatureScope per LoginActivityComponent o TermsAndConditionsComponent in cui puoi definire l'ambito di una logica più specifica per le funzionalità ad esempio ViewModel oggetti.

Per gli altri moduli, ad esempio Registration, viene utilizzata una configurazione simile.

Una regola generale per un progetto multimodulo è che moduli dello stesso livello non devono dipendere l'uno dall'altro. In tal caso, valuta se la logica condivisa (le dipendenze tra loro) devono fare parte del modulo padre. Se sì, eseguire il refactoring per spostare le classi nel modulo padre; In caso contrario, crea un nuovo modulo che estende il modulo principale e fa sì che entrambi i moduli originali estendono nuovo modulo.

Come best practice, in genere si crea un componente in una nei seguenti casi:

  • Devi eseguire l'inserimento del campo, come per LoginActivityComponent.

  • Devi definire l'ambito degli oggetti, come per LoginComponent.

Se nessuna di queste condizioni è applicabile e devi dire a Dagger come fornire oggetti da quel modulo, crea ed esponi un modulo Dagger con @Provides o @Binds metodi se l'inserimento dei lavori non è possibile per queste classi.

Implementazione con i sottocomponenti di Dagger

La pagina del documento Utilizzare Dagger nelle app Android spiega come creare e utilizzare i componenti secondari. Non puoi, però, utilizzare lo stesso codice i moduli delle funzionalità non conoscono il modulo app. Ad esempio, se ritieni su un tipico flusso di accesso e sul codice che abbiamo nella pagina precedente, compila ancora:

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 sa di MyApplicationappComponent. Affinché funzioni, devi definire un'interfaccia nella caratteristica che fornisce un FeatureComponent di cui MyApplication ha bisogno da implementare.

Nell'esempio seguente, puoi definire un'interfaccia LoginComponentProvider che fornisce un LoginComponent nel modulo login per il flusso di accesso:

Kotlin

interface LoginComponentProvider {
    fun provideLoginComponent(): LoginComponent
}

Java

public interface LoginComponentProvider {
   public LoginComponent provideLoginComponent();
}

Ora l'LoginActivity utilizzerà questa interfaccia al posto dello snippet di codice definita sopra:

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);

        ...
    }
}

A questo punto, MyApplication deve implementare l'interfaccia e implementare 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 di Dagger in un progetto multimodulo. Con i moduli delle funzionalità, la soluzione è diversa per via del fatto che i moduli dipendono l'uno dall'altro.

Dipendenze dei componenti con moduli delle funzionalità

Con i moduli delle caratteristiche, il modo in cui i moduli dipendono solitamente invertiti l'uno con l'altro. Al posto del modulo app include la funzionalità , i moduli delle funzionalità dipendono dal modulo app. Vedi figura 2 per una rappresentazione di come sono strutturati i moduli.

Figura 2. Esempio di grafo Dagger in una progetto con moduli delle funzionalità

In Dagger, i componenti devono conoscere i relativi sottocomponenti. Queste informazioni è incluso in un modulo Dagger aggiunto al componente padre (come modulo SubcomponentsModule in Utilizzo di Dagger nelle app Android).

Purtroppo, con la dipendenza invertita tra l'app e modulo delle funzionalità, il sottocomponente non è visibile nel modulo app perché ma non nel percorso di build. Ad esempio, un valore LoginComponent definito in Il modulo delle funzionalità login non può essere un sottocomponente di ApplicationComponent definita nel modulo app.

Dagger ha un meccanismo chiamato dipendenze dei componenti che puoi utilizzare per per risolvere il problema. Anziché essere un componente secondario del il componente principale, il componente figlio dipende da quest'ultimo. Con che non esista una relazione genitore-figlio; Ora i componenti dipendono da altri per ottenere determinate dipendenze. I componenti devono esporre i tipi del grafico per consentire ai componenti dipendenti di utilizzarli.

Ad esempio: un modulo di funzionalità chiamato login vuole creare una LoginComponent che dipende dai AppComponent disponibili in app modulo Gradle.

Di seguito sono riportate le definizioni delle classi e dei AppComponent che fanno parte di il 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 modulo di laurea login che include il modulo di livello app, hai un 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 da UserRepository disponibile e con 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 delle dipendenze dell'annotazione del componente. Poiché LoginActivity inserito da Dagger, aggiungi il metodo inject() all'interfaccia.

Quando crei un LoginComponent, un'istanza di AppComponent deve essere passato. Per farlo, utilizza il data di fabbrica del componente:

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 per LoginComponent possa accedervi da AppComponent, AppComponent deve esporlo in nell'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 quelle i componenti secondari. Poiché LoginComponent utilizza un'istanza di AppComponent, non possono utilizzare la stessa annotazione di ambito.

Se volessi applicare l'ambito LoginViewModel a LoginComponent, devi utilizzare che hai fatto 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 essere sempre presente nel modulo app.

  • Crea componenti Dagger nei moduli se devi eseguire l'inserimento dei campi in quel modulo o devi definire l'ambito degli oggetti per un flusso specifico la tua applicazione.

  • Per i moduli Gradle che sono pensati come utilità o helper e che non richiedono per creare un grafico (per questo ti serve un componente Dagger), creare ed esporre di moduli Dagger pubblici con metodi @Provides e @Binds di quelle classi che non supportano l'inserimento del costruttore.

  • Per utilizzare Dagger in un'app Android con moduli di funzionalità, usa il componente alle dipendenze per poter accedere alle dipendenze fornite ApplicationComponent definita nel modulo app.