Używanie Daggera w aplikacjach na Androida

Na stronie z podstawowymi informacjami o Dagger wyjaśniono, jak Dagger może pomóc Ci zautomatyzować wstrzykiwanie zależności w aplikacji. Dzięki Dagger nie musisz pisać uciążliwego, stałego kodu podatnego na błędy.

Podsumowanie sprawdzonych metod

  • Jeśli to możliwe, używaj wstrzykiwania przez konstruktor z elementami @Inject, aby dodawać typy do wykresu sztyletu. Gdy nie:
    • Użyj elementu @Binds, aby wskazać Daggerowi implementację interfejsu.
    • Użyj @Provides, aby poinformować Dagger, jak udostępnić zajęcia, które nie należą do Twojego projektu.
  • Moduły należy zadeklarować w komponencie tylko raz.
  • Nadaj adnotacjom zakresu nazwy zależnie od okresu, w którym są one używane. Przykłady to @ApplicationScope, @LoggedUserScope i @ActivityScope.

Dodawanie zależności

Aby używać Dagger w projekcie, dodaj te zależności do aplikacji w pliku build.gradle. Najnowszą wersję Daggera znajdziesz w tym projekcie na GitHubie.

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

Sztylet w Androidzie

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

LoginActivity zależy od parametru LoginViewModel, który zależy od klasy UserRepository, która zależy od wartości UserLocalDataSource i UserRemoteDataSource, które z kolei zależą od modelu Retrofit.

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

Na Androidzie zwykle tworzysz wykres Daggera, który znajduje się w klasie aplikacji, ponieważ chcesz, aby wystąpienie wykresu było w pamięci, tak długo, jak aplikacja jest uruchomiona. Dzięki temu wykres jest dołączony do cyklu życia aplikacji. W niektórych przypadkach warto też uwzględnić na wykresie kontekst aplikacji. Aby to zrobić, wykres musi znajdować się w klasie Application. Jedną z zalet tego podejścia jest to, że wykres jest dostępny dla innych klas platformy Androida. Dodatkowo upraszcza to testowanie, umożliwiając użycie w nich niestandardowej klasy Application.

Interfejs, który generuje wykres, jest oznaczony adnotacją @Component, więc możesz go nazywać ApplicationComponent lub ApplicationGraph. Zwykle przechowujesz ten komponent w niestandardowej klasie Application i wywołujesz go za każdym razem, gdy potrzebujesz wykresu aplikacji, jak pokazano w tym fragmencie kodu:

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

Ponieważ niektóre klasy platformy Androida, takie jak działania i fragmenty, są zainicjowane przez system, Dagger nie może ich utworzyć za Ciebie. W przypadku działań każdy kod inicjowania musi zostać podany w metodzie onCreate(). Oznacza to, że nie możesz używać adnotacji @Inject w konstruktorze klasy (wstrzyknięcie konstruktora), jak w poprzednich przykładach. Zamiast tego użyj wstrzykiwania pól.

Zamiast tworzyć zależności, których aktywność wymaga w metodzie onCreate(), chcesz, by Dagger wypełniał te zależności za Ciebie. W przypadku wstrzykiwania pól musisz zamiast tego zastosować adnotację @Inject do pól, które chcesz uzyskać z wykresu 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 to nie jest obiektem ViewModel architektury Androida – to po prostu zwykła klasa, która działa jako obiekt ViewModel. Więcej informacji o wstrzykiwaniu tych klas znajdziesz w kodzie w oficjalnej implementacji Android Blueprints Dagger w gałęzi dev-dagger.

W przypadku Daggera wstrzykiwane pola nie mogą być prywatne. Muszą one mieć co najmniej widoczność w pakiecie, tak jak w poprzednim kodzie.

Wstrzykiwanie

Dagger musi wiedzieć, że LoginActivity ma dostęp do wykresu, aby dostarczyć potrzebne ViewModel. Na stronie Podstawowe informacje o Dagger wykorzystano w interfejsie @Component obiekty z wykresu, prezentując funkcje z typem zwracanym, który chcesz uzyskać z wykresu. W tym przypadku musisz poinformować Daggera o obiekcie (w tym przypadku LoginActivity), który wymaga wstrzykiwania zależności. Służy do tego funkcja, która przyjmuje jako parametr obiekt żądający wstrzykiwania.

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 żąda wstrzykiwania. Dagger musi spełnić wszystkie zależności, których wymaga LoginActivity (z własnymi zależnościami LoginViewModel). Jeśli jest wiele klas, które żądają wstrzykiwania, musisz zadeklarować je wszystkie w komponencie dokładnie, podając ich dokładny typ. Jeśli na przykład usługa LoginActivity i RegistrationActivity prosi o wstrzykiwanie, uzyskasz 2 metody inject(), a nie ogólną, która obejmuje oba przypadki. Ogólna metoda inject() nie informuje Daggera, co należy dostarczyć. Funkcje w interfejsie mogą mieć dowolną nazwę, ale wywoływanie ich inject() po otrzymaniu obiektu do wstrzyknięcia jako parametru jest konwencją w Daggerze.

Aby wstrzyknąć obiekt do działania, użyj metody appComponent zdefiniowanej w klasie Application i wywołaj metodę inject(), przekazując wystąpienie działania, które wymaga wstrzykiwania.

Podczas korzystania z działań wstrzyknij Dagger do metody onCreate() aktywności przed wywołaniem metody super.onCreate(), aby uniknąć problemów z przywracaniem fragmentu. W fazie przywracania w super.onCreate() działanie dołącza fragmenty, które mogą chcieć uzyskać dostęp do powiązań działań.

Jeśli używasz fragmentów, wstrzyknij Dagger w metodzie onAttach() danego fragmentu. W takim przypadku można to zrobić przed wywołaniem funkcji super.onAttach() lub po nim.

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

Pokażmy Daggerowi, jak dostarczyć pozostałe zależności, by zbudować 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 sztyletu

W tym przykładzie używasz biblioteki sieciowej Retrofit. UserRemoteDataSource jest zależność od LoginRetrofitService. Jednak sposób tworzenia instancji LoginRetrofitService różni się od tego, co robiliśmy do tej pory. Nie jest to wystąpienie klasy, a jedynie wynik wywołania Retrofit.Builder() i przekazywania różnych parametrów w celu skonfigurowania usługi logowania.

Oprócz adnotacji @Inject istnieje inny sposób na poinformowanie Daggera, jak udostępnić instancję klasy – informacje zawarte w modułach Daggera. Moduł Dagger to klasa z adnotacją @Module. Możesz tam zdefiniować zależności za pomocą adnotacji @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 umożliwiają semantyczne przedstawianie informacji o udostępnianiu obiektów. Jak widać, wywołaliśmy klasę NetworkModule, aby zgrupować logikę udostępniania obiektów związanych z siecią. Jeśli aplikacja się rozwija, możesz tu też dodać sposób podawania OkHttpClient oraz konfigurację Gson lub Moshi.

Zależności metody @Provides są parametrami tej metody. W przypadku poprzedniej metody obiekt LoginRetrofitService można podać bez zależności, ponieważ nie ma ona parametrów. Jeśli jako parametr zadeklarowano OkHttpClient, Dagger musi dostarczyć z wykresu instancję OkHttpClient, aby spełnić 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 wykres Daggera wiedział o tym module, musisz go dodać do interfejsu @Component w ten 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 wykresu Daggera jest użycie wstrzykiwania przez konstruktor (np. za pomocą adnotacji @Inject w konstruktorze klasy). Czasami jest to niemożliwe i konieczne jest użycie modułów Daggera. Przykładem może być sytuacja, w której Dagger ma wykorzystać wynik obliczeń do określenia, jak utworzyć instancję obiektu. Gdy musi udostępnić instancję tego typu, Dagger uruchamia kod w metodzie @Provides.

Oto jak obecnie wygląda wykres Sztylet w przykładzie:

Diagram wykresu zależności LoginActivity

Rysunek 2. Ilustracja przedstawiająca wykres z elementem LoginActivity wstrzykiwanym przez Daggera

Punkt wejścia na wykres to LoginActivity. Ponieważ LoginActivity wstrzykuje LoginViewModel, Dagger tworzy wykres, który potrafi udostępnić wystąpienie zależności LoginViewModel i rekursywnie. Dagger wie, jak to zrobić dzięki adnotacji @Inject w konstruktorze klas.

W elemencie ApplicationComponent wygenerowanym przez Daggera znajduje się metoda fabryczna, która pozwala pobrać wystąpienia wszystkich klas, które potrafi udostępnić. W tym przykładzie Dagger przekazuje połączenie do obiektu NetworkModule zawartego w ApplicationComponent, aby uzyskać wystąpienie LoginRetrofitService.

Zakresy Dagger

Zakresy zostały wspomniane na stronie Podstawowe informacje o Dagger jako sposób na utworzenie unikalnej instancji danego typu w komponencie. Chodzi o określenie zakresu typu na cykl życia komponentu.

UserRepository może się przydać w innych funkcjach aplikacji i może nie chcieć tworzyć nowego obiektu za każdym razem, gdy go potrzebujesz, dlatego możesz oznaczyć go jako unikalną instancję dla całej aplikacji. Tak samo jest w przypadku LoginRetrofitService: utworzenie może być kosztowne, a zapewnia też ponowne wykorzystanie unikalnej instancji obiektu. Tworzenie instancji UserRemoteDataSource nie jest zbyt kosztowne, dlatego nie trzeba ograniczać jej do cyklu życia komponentu.

@Singleton to jedyna adnotacja dotycząca zakresu dostępna w pakiecie javax.inject. Możesz go używać do dodawania adnotacji do ApplicationComponent i obiektów, których chcesz używać ponownie 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 doszło do wycieku pamięci podczas stosowania zakresów do obiektów. Dopóki komponent z ograniczonym zakresem znajduje się w pamięci, utworzony obiekt znajduje się też w pamięci. Ponieważ aplikacja ApplicationComponent jest tworzona podczas uruchamiania aplikacji (w klasie Application), jest niszczona po zniszczeniu aplikacji. Dlatego unikalna instancja UserRepository zawsze pozostaje w pamięci, dopóki aplikacja nie zostanie zniszczona.

Podkomponenty sztyletu

Jeśli przepływ logowania (zarządzany przez pojedynczy LoginActivity) składa się z wielu fragmentów, użyj tego samego wystąpienia LoginViewModel we wszystkich fragmentach. @Singleton nie może dodać adnotacji do LoginViewModel, aby ponownie użyć instancji z tych powodów:

  1. Instancja LoginViewModel zostanie zachowana w pamięci po zakończeniu przepływu.

  2. Chcesz mieć inną instancję LoginViewModel dla każdego procesu logowania. Jeśli na przykład użytkownik się wyloguje, chcesz użyć innej instancji LoginViewModel, a nie tej samej, do której użytkownik zalogował się po raz pierwszy.

Aby zakres LoginViewModel obejmował cykl życia jednostki LoginActivity, musisz utworzyć nowy komponent (nowy podgraf) dla procesu logowania oraz nowy zakres.

Utwórzmy wykres przedstawiający proces logowania.

Kotlin

@Component
interface LoginComponent {}

Java

@Component
public interface LoginComponent {
}

Teraz LoginActivity powinien otrzymywać wstrzykiwanie z LoginComponent, ponieważ ma konfigurację odpowiednią do logowania. Dzięki temu nie będzie już można wstrzykiwać LoginActivity z klasy 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 ApplicationComponent, ponieważ LoginViewModel zależy od UserRepository. Jeśli chcesz, by nowy komponent wykorzystywał część innego, możesz użyć podkomponentów Dagger. Nowy komponent musi być podkomponentem zawierającym udostępnione zasoby.

Komponenty podrzędne to komponenty, które dziedziczą i rozszerzają graf obiektów obiektu nadrzędnego. W ten sposób wszystkie obiekty zawarte w komponencie nadrzędnym są dostarczane również w komponencie podrzędnym. W ten sposób obiekt z podkomponentu może zależeć od obiektu dostarczonego przez komponent nadrzędny.

Aby utworzyć wystąpienia komponentu podrzędnego, potrzebujesz wystąpienia komponentu nadrzędnego. Dlatego obiekty udostępniane przez komponent nadrzędny w komponencie podrzędnym są nadal ograniczone do komponentu nadrzędnego.

W tym przykładzie musisz zdefiniować LoginComponent jako składnik podrzędny ApplicationComponent. Aby to zrobić, dodaj adnotację LoginComponent za pomocą atrybutu @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 też zdefiniować fabrykę komponentów podrzędnych w elemencie LoginComponent, aby usługa ApplicationComponent wiedziała, jak tworzyć instancje komponentów 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 elementu ApplicationComponent, musisz to wskazać:

  1. Utworzenie nowego modułu Dagger (np. SubcomponentsModule) przekazującego klasę 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 (np. SubcomponentsModule) do projektu 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 ApplicationComponent nie musi już wstrzykiwać metody LoginActivity, ponieważ odpowiada teraz za to LoginComponent, możesz więc usunąć metodę inject() z metody ApplicationComponent.

    Konsumenci środowiska ApplicationComponent muszą wiedzieć, jak utworzyć wystąpienia LoginComponent. Komponent nadrzędny musi dodać w swoim interfejsie metodę, która pozwala klientom tworzyć wystąpienia komponentu podrzędnego z instancji komponentu nadrzędnego:

  3. Udostępnij fabrykę, która tworzy instancje LoginComponent w interfejsie:

    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 kompilujesz projekt, możesz tworzyć instancje zarówno ApplicationComponent, jak i LoginComponent. Zasób ApplicationComponent jest dołączony do cyklu życia aplikacji, ponieważ chcesz używać tego samego wystąpienia wykresu, dopóki aplikacja znajduje się w pamięci.

Jaki jest cykl życia usługi LoginComponent? Jedną z powodów, dla których potrzebujesz LoginComponent, jest to, że musisz udostępniać to samo wystąpienie LoginViewModel między fragmentami związanymi z logowaniem. Trzeba też jednak pamiętać, że za każdym razem, gdy pojawia się nowy proces logowania, LoginViewModel ma inne instancje. LoginActivity to właściwy czas życia obiektu LoginComponent: dla każdej nowej aktywności potrzebujesz nowego wystąpienia LoginComponent i fragmentów, które mogą korzystać z tego wystąpienia LoginComponent.

Ponieważ element LoginComponent jest powiązany z cyklem życia LoginActivity, musisz się odnosić do komponentu w aktywności w taki sam sposób, w jaki zachowano odwołanie do elementu applicationComponent w klasie Application. Dzięki temu fragmenty 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 do zmiennej loginComponent nie ma adnotacji @Inject, ponieważ Dagger nie powinien podawać tej zmiennej.

Możesz użyć ApplicationComponent, aby uzyskać odwołanie do LoginComponent, a następnie wstrzyknąć 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 jest tworzony w metodzie onCreate() aktywności i zostanie domyślnie zniszczony po zniszczeniu aktywności.

LoginComponent musi zawsze dostarczać tę samą instancję LoginViewModel za każdym razem, gdy jest potrzebne. Aby to zrobić, utwórz niestandardowy zakres adnotacji i dodaj do niego adnotacje LoginComponent i LoginViewModel. Pamiętaj, że nie możesz użyć adnotacji @Singleton, ponieważ jest ona już używana przez komponent nadrzędny, co sprawi, że obiekt stanie się singlem (unikalnym wystąpieniem dla całej aplikacji). Musisz utworzyć inny zakres adnotacji.

W tym przypadku można nazwać ten zakres @LoginScope, ale nie jest to dobra praktyka. Nazwa adnotacji zakresu nie powinna być jednoznacznie określona dla jej celu. Należy mu nadać nazwę w zależności od jego czasu trwania, ponieważ adnotacje mogą być wielokrotnie używane przez komponenty równorzędne, takie jak RegistrationComponent i SettingsComponent. Dlatego lepiej ją nazwać @ActivityScope, a nie @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ą parametru LoginViewModel, oba są udostępniane w tej samej instancji. Jeśli na przykład masz LoginUsernameFragment i LoginPasswordFragment, muszą one być wstrzykiwane przez 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 działa w obiekcie LoginActivity. Przykładowy kod dla pola LoginUserNameFragment znajduje się w tym fragmencie 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)
    }
}

Ta sama zmiana w przypadku 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)
    }
}

Rysunek 3 przedstawia wygląd wykresu Daggera z nowym podkomponentem. Klasy z białą kropką (UserRepository, LoginRetrofitService i LoginViewModel) to te, które mają unikalną instancję ograniczoną do odpowiednich komponentów.

Wykres aplikacji po dodaniu ostatniego podkomponentu

Rysunek 3. Ilustracja przedstawiająca wykres utworzony na potrzeby przykładowej aplikacji na Androida

Przeanalizujmy następujące elementy wykresu:

  1. Element NetworkModule (i tym samym LoginRetrofitService) jest uwzględniony w elemencie ApplicationComponent, ponieważ został on przez Ciebie podany w komponencie.

  2. UserRepository pozostaje w tabeli ApplicationComponent, ponieważ jest ograniczony do ApplicationComponent. Jeśli projekt się rozwija, chcesz udostępniać tę samą instancję różnym funkcjom (np. rejestracji).

    UserRepository jest częścią komponentu ApplicationComponent, więc jego zależności (tj. UserLocalDataSource i UserRemoteDataSource) muszą też znajdować się w tym komponencie, aby mogły występować wystąpienia funkcji UserRepository.

  3. LoginViewModel jest zawarte w LoginComponent, ponieważ jest wymagane tylko przez klasy wstrzykiwane przez LoginComponent. LoginViewModel nie został uwzględniony w ApplicationComponent, ponieważ brak zależności w ApplicationComponent wymaga LoginViewModel.

    Podobnie gdyby nie zakres UserRepository na ApplicationComponent, Dagger automatycznie dodałby do elementu LoginComponent element UserRepository i jego zależności, ponieważ obecnie jest to jedyne miejsce używane w polu UserRepository.

Oprócz ograniczania zakresu obiektów do innego cyklu życia tworzenie podkomponentów jest dobrą praktyką, aby oddzielić poszczególne części aplikacji.

Utworzenie aplikacji w taki sposób, aby tworzyła różne podgrafy Daggera w zależności od przepływu aplikacji, pomaga stworzyć bardziej wydajną i skalowalną aplikację pod względem pamięci i czasu uruchamiania.

Sprawdzone metody tworzenia wykresu sztyletowego

Tworząc wykres Dagger dla swojej aplikacji:

  • Podczas tworzenia komponentu warto się zastanowić, który z nich odpowiada za jego okres eksploatacji. W tym przypadku klasa Application zajmuje się zajęciami ApplicationComponent, a LoginActivityLoginComponent.

  • Zakresu używaj tylko wtedy, gdy ma to sens. Nadużywanie zakresu może mieć negatywny wpływ na wydajność aplikacji w czasie działania: obiekt znajduje się w pamięci, dopóki komponent znajduje się w pamięci, a uzyskanie obiektu o zakresie jest droższe. Gdy Dagger udostępnia obiekt, używa blokady DoubleCheck zamiast dostawcy typu fabrycznego.

Testowanie projektu korzystającego z Daggera

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

Testy jednostkowe

Nie musisz używać Daggera do testów jednostkowych. Gdy testujesz klasę, która używa wstrzykiwania konstruktora, nie musisz używać Daggera do utworzenia instancji tej klasy. Możesz bezpośrednio wywoływać jego konstruktor, przekazując fałszywe lub pozorowane zależności bezpośrednio, tak jak gdyby nie były opatrzone adnotacjami.

Na przykład podczas testowania 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ć TestApplicationComponent do testowania. Wersja produkcyjna i testowa korzystają z innej konfiguracji komponentów.

Wymaga to bardziej wcześniejszego zaprojektowania modułów w aplikacji. Komponent testowy rozszerza komponent produkcyjny i 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 {
}

Element FakeNetworkModule ma fałszywą implementację tagu NetworkModule. Możesz tam podać fałszywe wystąpienia lub symulacje tego, co 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 ramach integracji lub testów kompleksowych należy użyć obiektu TestApplication, który tworzy TestApplicationComponent, a nie 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 jest używana w niestandardowym obiekcie TestRunner, którego używasz do uruchamiania testów z instrumentacją. Więcej informacji na ten temat znajdziesz w ćwiczeniach z programowania za pomocą aplikacji Dagger w aplikacjach na Androida.

Praca z modułami Dagger

Moduły sztyletu to sposób na semantyczną prezentację sposobu dostarczania obiektów. Moduły możesz dodawać do komponentów, ale także w innych modułach. Ta funkcja jest bardzo potężna, ale łatwo może być niewłaściwie wykorzystywana.

Po dodaniu modułu do komponentu lub innego modułu jest on już widoczny na wykresie Daggera. Dagger może dostarczać te obiekty do komponentu. Zanim dodasz moduł, sprawdź, czy nie jest on już częścią wykresu Dagger – sprawdź, czy jest już dodany do komponentu, lub skompiluj projekt i sprawdź, czy Dagger może znaleźć wymagane zależności.

Zgodnie z dobrą praktyką moduły należy zadeklarować w komponencie tylko raz (poza konkretnymi zaawansowanymi przypadkami użycia Daggera).

Załóżmy, że masz swój wykres skonfigurowany w ten sposób. ApplicationComponent obejmuje Module1 i Module2 oraz Module1ModuleX.

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 zależy od zajęć prowadzonych przez ModuleX. Niewłaściwą praktyką jest uwzględnianie ModuleX w elemencie Module2, ponieważ ModuleX występuje na wykresie dwukrotnie, tak jak 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. refaktoryzuj moduły i wyodrębnij moduł wspólny do komponentu.
  2. Utwórz nowy moduł z obiektami, które są wspólne dla obu modułów, i wyodrębnij go do komponentu.

Brak refaktoryzacji w ten sposób powoduje, że wiele modułów (w tym się nawzajem) nie ma jednoznacznych pomyłek w organizacji, co utrudnia określenie, skąd się bierze każda zależność.

Dobra metoda (opcja 1): moduł X jest deklarowany na wykresie sztywnym tylko raz.

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 od Module1 i Module2 w językach ModuleX są wyodrębniane do nowego modułu o nazwie ModuleXCommon, który jest zawarty w komponencie. Następnie zostaną utworzone 2 inne moduły o nazwach ModuleXWithModule1Dependencies i ModuleXWithModule2Dependencies z zależnościami, które występują w poszczególnych modułach. Wszystkie moduły są deklarowane 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 wykorzystywany do konstruowania obiektu, w którym niektóre parametry mogą być dostarczane przez platformę DI, a inne muszą być przekazywane przez użytkownika w momencie tworzenia.

Na Androidzie ten wzorzec jest powszechny na ekranach szczegółów, gdzie identyfikator wyświetlanego elementu jest znany tylko w czasie działania, a nie w czasie kompilacji, gdy Dagger generuje wykres DI. Więcej informacji o wstrzykiwaniu wspomaganym metodą Dagger znajdziesz w dokumentacji Daggera.

Podsumowanie

Zapoznaj się z sekcją ze sprawdzonymi metodami. Instrukcje, jak używać Daggera w aplikacji na Androida, znajdziesz w ćwiczeniach z programowania za pomocą Daggera w aplikacji na Androida.