Używanie Daggera w aplikacjach na Androida

Na stronie Podstawowe informacje o Dagger wyjaśniono, jak Dagger może pomóc w automatyzacji zależności wstrzyknięcie kodu źródłowego w aplikacji. Dzięki Sztyletowi nie musisz pisać żmudnych, podatnego na błędy powtarzalnego kodu.

Podsumowanie sprawdzonych metod

  • Użyj wstrzykiwania do konstruktora za pomocą funkcji @Inject, aby dodać typy do Sztyletu gdy tylko jest to możliwe. Jeśli:
    • Użyj polecenia @Binds, aby poinformować Daggera, którą implementację powinien mieć interfejs.
    • Użyj pola @Provides, by poinformować Daggera, jak udostępnić zajęcia, że Twój projekt nie należy do Google.
  • Moduły należy zadeklarować w komponencie tylko raz.
  • Nazwij adnotacje zakresu w zależności od okresu ważności, w którym adnotacji. Przykłady: @ApplicationScope, @LoggedUserScope, i @ActivityScope.

Dodawanie zależności

Aby używać Daggera w projekcie, dodaj te zależności do aplikacji w build.gradle. Znajdziesz na niej najnowszą wersję Daggera w tym projekcie 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'
}

Krzyżyk w Androidzie

Przyjrzyjmy się przykładowej aplikacji na Androida zawierającej wykres zależności z ilustracji 1.

LoginActivity zależy od LoginViewModel, który zależy od UserRepository,
  które są zależne od UserLocalDataSource i UserRemoteDataSource, które z kolei
  zależy od przekształcenia.

Rysunek 1. Wykres zależności przykładowego kod

Na Androidzie zwykle tworzysz wykres Daggera, który znajduje się w Twojej aplikacji. , ponieważ chcesz, aby instancja grafu była w pamięci, dopóki jest uruchomiona aplikacja. W ten sposób wykres zostanie powiązany z cyklem życia aplikacji. W niektórych warto również mieć kontekst aplikacji w wykres. Aby to zrobić, wykres musi znajdować się w Application. Jedną z zalet jest dostępny dla innych klas platform Androida. Dodatkowo upraszcza on testowanie, ponieważ umożliwia stosowanie niestandardowych wymiarów Application klasa w testach.

Interfejs generujący wykres jest opatrzony adnotacją @Component, Możesz go nazwać ApplicationComponent lub ApplicationGraph. Zwykle przechowujesz wystąpienie tego komponentu w niestandardowej klasie Application i wywołuj je za każdym razem, gdy potrzebny jest wykres aplikacji, jak pokazano w poniższym kodzie. 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();
}

Niektóre klasy platformy Androida, na przykład działania i fragmenty, są utworzonych przez system, Dagger nie może ich utworzyć. Do aktywności a w szczególności każdy kod inicjowania musi należeć do metody onCreate(). Oznacza to, że nie można używać adnotacji @Inject w konstruktorze (wstrzykiwanie tekstu z konstruktora), tak jak w poprzednich przykładach. Zamiast tego: musisz stosować wstrzykiwanie przez pola.

Zamiast tworzyć zależności wymagane przez aktywność w onCreate() , Dagger ma wypełnić te zależności za Ciebie. Pole możesz zastosować adnotację @Inject do pól, które z grafu Daggera.

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

Dla uproszczenia LoginViewModel nie jest komponentami architektury Androida ViewModel; to po prostu standardowa klasa, która działa jako model widoku. Więcej informacji o wstrzykiwaniu tych klas znajdziesz w kodzie w oficjalnej implementacji Daggera Android Blueprints: gałęzi dev-dagger.

Jedną z kwestii, które warto wziąć pod uwagę w przypadku Daggera, jest to, że wstrzyknięte pola nie mogą być prywatne. Muszą mieć co najmniej prywatną widoczność pakietu, tak jak w poprzednim kodzie.

Wstrzykiwanie aktywności

Krzyżyk musi wiedzieć, że LoginActivity ma dostęp do wykresu, aby podaj wymagane ViewModel. Na stronie Podstawowe informacje o Dagger użyto interfejsu @Component do pobierania obiektów z grafu przez udostępnienie funkcji z typem zwracanym tego, co chcesz uzyskać z funkcji wykres. W tym przypadku powiedz Daggerowi o obiekcie (LoginActivity w tym przypadku) wymaga wstrzyknięcia zależności. W tym celu narażasz funkcję, która przyjmuje jako parametr obiekt żądający wstrzyknięcia.

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

Ta funkcja informuje Daggera, że LoginActivity chce uzyskać dostęp do wykresu i lub wstrzykiwania żądań. Sztylet musi spełnić wszystkie zależności, które Wymagania LoginActivity (LoginViewModel z własnymi zależnościami). Jeśli masz wiele klas, które żądają wstrzykiwania, musisz specjalnie i zadeklarować je wszystkie w komponencie, podając ich dokładny typ. Jeśli na przykład masz LoginActivity i RegistrationActivity proszą o wstrzykiwanie. Masz dwa. inject() zamiast ogólnej metody obejmującej oba przypadki. Ogólne Metoda inject() nie informuje Daggera, co należy podać. Funkcje w interfejsie może mieć dowolną nazwę, ale nazywa się inject(), gdy który do wstrzykiwania jest zgodny z konwencją w Daggerze.

Aby wstrzykiwać obiekt w aktywności, musisz użyć parametru appComponent zdefiniowanego w klasę Application i wywołanie metody inject(), przekazując instancję aktywności, która wymaga wstrzykiwania.

Podczas aktywności wstrzyknij Daggera metody onCreate() aktywności przed wywołaniem metody super.onCreate(), aby uniknąć problemów z przywróceniem fragmentów. Podczas fazy przywracania w super.onCreate(): działanie dołącza fragmenty, które mogą chcieć uzyskać dostęp do powiązań aktywności.

Jeśli używasz fragmentów, wstrzyknij Dagger w elemencie onAttach() fragmentu . W tym przypadku możesz to zrobić przed wywołaniem usługi super.onAttach() lub później.

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

Powiedzmy Daggerowi, jak podać pozostałe zależności potrzebne do utworzenia wykres:

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

Moduły daggera

W tym przykładzie korzystasz z biblioteki sieci Retrofit. Funkcja UserRemoteDataSource jest zależna od elementu LoginRetrofitService. Pamiętaj jednak: sposób utworzenia instancji LoginRetrofitService jest inny niż co robiliście do tej pory. Nie jest to wystąpienie klasowe; to efekt wywołuje funkcję Retrofit.Builder() i przekazuje różne parametry w celu skonfigurowania z usługi logowania.

Poza adnotacją @Inject jest jeszcze inny sposób na poinformowanie Daggera, jak podać instancję klasy: informacje w modułach Daggera. Krzyżyk Moduł to klasa z adnotacją @Module. Możesz tam określić zależności z adnotacją @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);
    }
}

Moduły to sposób na semantyczne ujęcie informacji o tym, jak przekazać obiektów. Jak widać, nazwa została przez Ciebie wywołana klasą NetworkModule, aby zgrupować logikę dostarczania obiektów związanych z siecią. Po rozwinięciu aplikacji możesz: Tutaj też dowiesz się, jak podać OkHttpClient lub jak skonfiguruj Gson lub Moshi.

Zależnościami metody @Provides są jej parametry. Dla: poprzedniej metody, można przekazać funkcję LoginRetrofitService bez zależności ponieważ metoda nie ma parametrów. Jeśli zadeklarowano OkHttpClient jako Dagger musiałby wyświetlić instancję OkHttpClient z metody aby wypełnić zależności zależności LoginRetrofitService. Na przykład:

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) {
        ...
    }
}

Aby graf Daggera wiedział o tym module, musisz dodać go do interfejsu @Component w następujący sposób:

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 {
    ...
}

Zalecanym sposobem dodawania typów do grafu Daggera jest użycie konstruktora wstrzyknięcie (tzn. z adnotacją @Inject w konstruktorze klasy). Czasami nie jest to możliwe i konieczne jest użycie modułów Daggera. Przykład gdy Dagger ma użyć wyniku obliczeń, by określić, przez utworzenie instancji obiektu. zawsze, gdy musi ustanowić taki fragment Dagger uruchamia kod wewnątrz metody @Provides.

Tak teraz wygląda wykres Daggera z przykładu:

Diagram wykresu zależności LoginActivity

Rysunek 2. Prezentacja grafu: Pole LoginActivity wstrzykiwane przez Daggera

Punkt wejścia do wykresu to LoginActivity. Ponieważ LoginActivity wstrzykuje LoginViewModel, Dagger tworzy wykres, który wie, jak udostępnić instancję LoginViewModel i rekurencyjnie z jego zależności. Dagger wie, jak możesz to zrobić dzięki adnotacji @Inject w klasach za pomocą konstruktora.

Wewnątrz urządzenia ApplicationComponent wygenerowanego przez Daggera znajdziesz typ fabryczny , aby pobierać instancje wszystkich klas, które potrafi dostarczyć. W tym na przykład Dagger deleguje do NetworkModule zawartego w ApplicationComponent, aby pobrać instancję LoginRetrofitService.

Zakresy daggera

Zakresy wspomniano na stronie Podstawowe informacje o sztabkach, aby umożliwić unikalnej instancji typu w komponencie. To właśnie jest ograniczyć typ do cyklu życia komponentu.

Aplikacji UserRepository warto używać w innych funkcjach aplikacji, Jeśli nie chcesz tworzyć nowego obiektu za każdym razem, gdy jest Ci potrzebna, możesz wyznaczyć jako unikalną instancję dla całej aplikacji. Tak samo w przypadku LoginRetrofitService: jego utworzenie może być drogie, a do tego warto mieć unikalną instancję tego obiektu do ponownego wykorzystania. Tworzę instancję Usługa UserRemoteDataSource nie jest tak droga, więc ograniczam ją do cykl życia komponentu nie jest wymagany.

@Singleton to jedyna adnotacja zakresu, która jest dostarczana z pakiet javax.inject. Możesz go używać, aby dodawać adnotacje do ApplicationComponent a także obiekty, których chcesz używać w całej aplikacji.

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() { ... }
}

Uważaj, aby nie wprowadzać wycieków pamięci podczas stosowania zakresów do obiektów. Jako dopóki komponent o zakresie ograniczonym jest w pamięci, utworzony obiekt pozostaje w pamięci . Ponieważ obiekt ApplicationComponent jest tworzony podczas uruchamiania aplikacji (w sekcji Application), jest ona zniszczona w momencie zniszczenia aplikacji. Dlatego unikalna instancja UserRepository zawsze pozostaje w pamięci do momentu Aplikacja została zniszczona.

Podkomponent krzyżyka

Jeśli proces logowania (zarządzany przez pojedynczy LoginActivity) składa się z kilku fragmentów, używaj ponownie tego samego wystąpienia ciągu LoginViewModel we wszystkich fragmenty. @Singleton nie może dodawać adnotacji LoginViewModel, aby ponownie użyć instancji z następujących powodów:

  1. Instancja LoginViewModel zostanie zachowana w pamięci, gdy przepływ będzie .

  2. Chcesz używać innej instancji LoginViewModel dla każdego procesu logowania. Jeśli na przykład użytkownik się wyloguje, należy utworzyć inne wystąpienie LoginViewModel, a nie w tej samej instancji, co w przypadku logowania się użytkownika za pierwszym razem.

Aby ograniczyć zakres LoginViewModel do cyklu życia zasobu LoginActivity, musisz utworzyć nowy komponent (nowy podgraf) procesu logowania i nowy zakres.

Utwórzmy wykres dla przepływu logowania.

Kotlin

@Component
interface LoginComponent {}

Java

@Component
public interface LoginComponent {
}

Teraz LoginActivity powinien otrzymywać zastrzyki od LoginComponent, ponieważ ma konfigurację specyficzną dla logowania. Eliminuje to odpowiedzialność za wstrzykiwanie LoginActivity z zajęć ApplicationComponent.

Kotlin

@Component
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

@Component
public interface LoginComponent {
    void inject(LoginActivity loginActivity);
}

LoginComponent musi mieć dostęp do obiektów z poziomu ApplicationComponent bo LoginViewModel zależy od UserRepository. Jak powiedzieć Daggerowi, Jeśli chcesz, aby nowy komponent używał części innego komponentu, Podskładniki sztyletu. Nowy komponent musi być podkomponentem elementu Komponent zawierający udostępnione zasoby.

Komponenty podrzędne to komponenty, które dziedziczą i rozszerzają graf obiektowy obiektu komponent nadrzędny. Dzięki temu wszystkie obiekty udostępniane w komponencie nadrzędnym są dostępnych w podkomponencie. W ten sposób obiekt z podkomponentu może zależeć od obiektu dostarczonego przez komponent nadrzędny.

Aby utworzyć instancje podkomponentów, musisz mieć wystąpienie komponentu nadrzędnego . Dlatego obiekty udostępniane przez komponent nadrzędny podkomponent jest nadal ograniczony do komponentu nadrzędnego.

W przykładzie musisz zdefiniować LoginComponent jako podkomponent elementu ApplicationComponent Aby to zrobić, dodaj do elementu LoginComponent adnotację @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);
}

Musisz również zdefiniować fabrykę podkomponentów wewnątrz komponentu LoginComponent, tak aby ApplicationComponent wie, jak tworzyć instancje instancji 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);
}

Aby poinformować Daggera, że LoginComponent jest podkomponentem ApplicationComponent, musisz wskazać to w następujący sposób:

  1. Utworzenie nowego modułu Daggera (np. SubcomponentsModule) przekazującego podkomponentu do atrybutu subcomponents adnotacji.

    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 {
    }
    
  2. Dodawanie nowego modułu (tj. SubcomponentsModule) do aplikacji ApplicationComponent:

    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 {
    }
    

    Pamiętaj, że usługa ApplicationComponent nie musi już wstrzykiwać wartości LoginActivity ponieważ ta odpowiedzialność należy teraz do użytkownika LoginComponent, więc możesz usunąć metodę inject() z ApplicationComponent.

    Konsumenci domeny ApplicationComponent muszą wiedzieć, jak tworzyć instancje LoginComponent Komponent nadrzędny musi dodać w swoim interfejsie metodę, która pozwoli klienci tworzą instancje podkomponentu z instancji komponent nadrzędny:

  3. Pokaż fabrykę, która tworzy instancje LoginComponent w interfejs:

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

Przypisywanie zakresów do podkomponentów

Jeśli skompilujesz projekt, będziesz mieć możliwość tworzenia instancji zarówno ApplicationComponent, i LoginComponent. ApplicationComponent jest połączony z cyklem życia aplikacji, ponieważ chcesz korzystać z tego samego wystąpienia grafu, o ile jeśli aplikacja jest w pamięci.

Jaki jest cykl życia LoginComponent? Jednym z powodów, dla których potrzeba LoginComponent jest spowodowane koniecznością współdzielenia tego samego wystąpienia LoginViewModel między fragmentami dotyczącymi logowania. Warto też jednak wystąpienia LoginViewModel w przypadku nowego procesu logowania. LoginActivity jest odpowiedni dla użytkownika LoginComponent: w przypadku każdej nowej aktywności musisz określić, nowe wystąpienie elementu LoginComponent i fragmenty, które mogą używać tego wystąpienia LoginComponent

Ścieżka LoginComponent jest powiązana z cyklem życia LoginActivity, więc musisz zachowaj odniesienie do komponentu w aktywności tak samo jak odwołanie do applicationComponent w klasie Application. Dzięki temu fragmenty tekstu mogą uzyskać do niego dostęp.

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;

    ...
}

Zwróć uwagę, że zmienna loginComponent nie jest oznaczona adnotacją @Inject bo nie spodziewasz się, że ta zmienna zostanie udostępniona przez Daggera.

Aby uzyskać odniesienie do: LoginComponent, możesz użyć: ApplicationComponent a następnie wstrzyknij LoginActivity w ten sposób:

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

Element LoginComponent został utworzony w metodzie onCreate() aktywności i zostanie niszczony w sposób niejawny po zniszczeniu działania.

LoginComponent musi zawsze wskazywać to samo wystąpienie LoginViewModel przy każdym żądaniu. Aby to osiągnąć, utwórz adnotację niestandardową zakresu i dodanie do niego adnotacji LoginComponent oraz LoginViewModel. Notatka nie można użyć adnotacji @Singleton, ponieważ została ona już użyta przez komponent nadrzędny, co sprawi, że obiekt stanie się pojedynczym elementem aplikacji. (unikalna instancja dla całej aplikacji). Musisz utworzyć inną adnotację zakresu.

W tym przypadku ten zakres można było nazwać @LoginScope, ale jest on nieodpowiedni praktyk. Nazwa adnotacji zakresu nie powinna być jawna w określonym przeznaczeniu co je wypełni. Zamiast tego powinna uzależnić ją od okresu ważności, ponieważ adnotacje mogą być używane ponownie przez komponenty równorzędne, takie jak RegistrationComponent i SettingsComponent. Dlatego właśnie nadaj mu nazwę @ActivityScope. z @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;
    }
}

Jeśli masz 2 fragmenty, które wymagają atrybutu LoginViewModel, oba są podany z tą samą instancją. Na przykład, jeśli masz LoginUsernameFragment i LoginPasswordFragment wymagają wstrzykiwania autor: 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);
}

Komponenty uzyskują dostęp do instancji komponentu, który znajduje się w LoginActivity obiekt. Przykładowy kod dla elementu LoginUserNameFragment pojawia się w ten fragment kodu:

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

To samo w przypadku usługi 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)
    }
}

Rys. 3 pokazuje graf Daggera z nowym podkomponentem. Zajęcia z białą kropką (UserRepository, LoginRetrofitService i LoginViewModel) to te, które mają unikalną instancję w zakresie odpowiednich komponentów.

Wykres aplikacji po dodaniu ostatniego podkomponentu

Rysunek 3. Prezentacja utworzonego wykresu dla aplikacji na Androida

Przeanalizujmy poszczególne części wykresu:

  1. Element NetworkModule (a tym samym LoginRetrofitService) jest uwzględniony w ApplicationComponent, bo została określona w komponencie.

  2. UserRepository pozostaje w ApplicationComponent, ponieważ jest ograniczony do ApplicationComponent Jeśli projekt będzie się rozwijał, możesz udostępniać instancji w różnych funkcjach (np. rejestracji).

    UserRepository jest częścią elementu ApplicationComponent, dlatego jego zależności (tj. UserLocalDataSource i UserRemoteDataSource) muszą znajdować się w tym komponent, aby móc udostępniać wystąpienia UserRepository.

  3. Pole LoginViewModel znajduje się w: LoginComponent, bo jest wymagane przez klasy wstrzyknięte przez funkcję LoginComponent. LoginViewModel nie należy do ApplicationComponent, ponieważ żadna zależność w ApplicationComponent nie wymaga LoginViewModel

    Podobnie, jeśli zakres UserRepository do ApplicationComponent nie był ograniczony, Dagger automatycznie dodałby element UserRepository i jego zależności w ramach LoginComponent, ponieważ obecnie jest to jedyne miejsce Zajęte miejsce: UserRepository.

Oprócz określenia zakresu obiektów zgodnie z innym cyklem życia tworzenie podkomponentów jest warto uwzględnić różne części aplikacji

Tworzenie struktury aplikacji tak, aby utworzyć różne podgrafy Daggera w zależności od przepływu pomaga stworzyć bardziej wydajną i skalowalną aplikację pod względem pamięci i czasu uruchamiania.

Sprawdzone metody tworzenia grafu Daggera

Podczas tworzenia grafu Daggera dla aplikacji:

  • Tworząc komponent, zastanów się, za który element odpowiada. przez cały okres istnienia tego komponentu. W tym przypadku klasa Application znajduje się w opłata za ApplicationComponent, a LoginActivity odpowiada za LoginComponent

  • Określanie zakresu należy używać tylko wtedy, gdy ma to sens. Nadmierne ograniczanie zakresu może mieć negatywny wpływ na wpływ na wydajność działania aplikacji: obiekt jest w pamięci tak długo, bo komponent jest w pamięci, a uzyskanie obiektu o zakresie jest droższe. Gdy Dagger udostępnia obiekt, używa blokady DoubleCheck zamiast dostawca fabryczny.

Testowanie projektu korzystającego z Daggera

Jedną z zalet korzystania z platform wstrzykiwania zależności, takich jak Dagger, ułatwia testowanie kodu.

Testy jednostkowe

Nie musisz używać daggera do testów jednostkowych. Podczas testowania zajęć, w których konstruktora, nie musisz używać Daggera do utworzenia instancji klasy. Możesz bezpośrednio wywołać jego konstruktor przekazujący fałszywe lub pozorowane zależności bezpośrednio, tak jak gdyby nie były dodawane adnotacje.

Na przykład podczas testowania usługi 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(...);
    }
}

Kompleksowe testy

W przypadku testów integracji dobrze jest utworzyć plik cookie TestApplicationComponent służy do testowania. Wersja produkcyjna i testowanie korzystają z innej konfiguracji komponentu.

Wymaga to bardziej przejrzystego projektu modułów w Twojej aplikacji. Komponent testowania rozszerza komponent produkcyjny. instaluje inny zestaw modułów.

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 zawiera fałszywą implementację oryginału: NetworkModule. Możesz w nim podać fałszywe wystąpienia lub symulacje różnych elementów, które chcesz zastąpić.

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

W przypadku integracji lub testów kompleksowych należy użyć parametru TestApplication, który tworzy TestApplicationComponent zamiast 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();
}

Następnie ta aplikacja testowa zostanie użyta w niestandardowym TestRunner, którego użyjesz do przeprowadzać testy narzędzi. Więcej informacji na ten temat znajdziesz w artykule Korzystanie z usługi Dagger w ćwiczeniu z programowania w aplikacji na Androida.

Praca z modułami Daggera

Moduły dagger pozwalają określić sposób przekazywania obiektów w sposób semantyczny sposób. Możesz umieszczać moduły w komponentach, ale możesz też dołączać do nich w innych modułach. Potężne narzędzie ma duże możliwości, ale można je łatwo wykorzystać w niewłaściwy sposób.

Po dodaniu modułu do komponentu lub innego modułu już na wykresie Daggera, Sztylet może przekazać te obiekty w ramach tego komponentu. Przed dodaniem modułu sprawdź, czy jest on już częścią grafu Daggera. sprawdzając, czy został już dodany do komponentu, lub poprzez kompilację projektu i sprawdzić, czy Dagger zdoła znaleźć wymagane zależności tego modułu.

Zgodnie z dobrą praktyką moduły powinny być zadeklarowane w komponencie tylko raz. (poza konkretnymi zaawansowanymi zastosowaniami Daggera).

Załóżmy, że Twój wykres jest skonfigurowany właśnie w ten sposób. ApplicationComponent obejmuje Module1 i Module2, a Module1 obejmuje 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 { ... }

Jeśli teraz Module2 korzysta z zajęć udostępnionych przez: ModuleX. Zła praktyka uwzględnia ModuleX w Module2, ponieważ ModuleX występuje dwukrotnie w Jak widać w tym fragmencie kodu:

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 { ... }

Zamiast tego wykonaj jedną z tych czynności:

  1. Refaktoryzacja modułów i wyodrębnienie wspólnego modułu .
  2. Utwórz nowy moduł z obiektami współużytkowanymi i wyodrębnionymi z obu modułów i przekazywać je do komponentu.

Brak refaktoryzacji w ten sposób powoduje, że wiele modułów obejmuje siebie nawzajem. bez wyraźnego poczucia organizacji i przez co trudniej jest rozejrzeć się skąd pochodzi każda zależność.

Sprawdzona metoda (opcja 1): moduł X jest zadeklarowany raz na wykresie Daggera.

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 { ... }

Sprawdzona metoda (opcja 2): typowe zależności z elementów Module1 i Module2 w ModuleX są wyodrębniane do nowego modułu o nazwie ModuleXCommon, który jest w komponencie. Następnie dwa inne moduły o nazwie ModuleXWithModule1Dependencies i ModuleXWithModule2Dependencies są z zależnościami specyficznymi dla każdego modułu. Wszystkie moduły są deklarowane raz na wykresie Daggera.

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 { ... }

Wstrzykiwanie wspomagane

Wstrzykiwanie wspomagane to wzorzec DI używany do tworzenia obiektu, w którym niektóre parametry mogą być dostarczane przez platformę DI, a inne muszą być przekazywane w momencie utworzenia przez użytkownika.

Na Androidzie ten wzorzec pojawia się często na ekranach szczegółów, na których identyfikator klucza Pokazany element jest znany tylko w czasie działania, a nie w czasie kompilacji, gdy Dagger generuje wykres DI. Aby dowiedzieć się więcej na temat wspomagania wstrzykiwania za pomocą sztyletu, zobacz dokumentację narzędzia Dagger.

Podsumowanie

W razie potrzeby przeczytaj sekcję ze sprawdzonymi metodami. Do jak używać Daggera w aplikacji na Androida, patrz Używanie Daggera w aplikacji na Androida