Использование Dagger в приложениях для Android,Использование Dagger в приложениях для Android

На странице основ Dagger объясняется, как Dagger может помочь вам автоматизировать внедрение зависимостей в ваше приложение. С Dagger вам не придется писать утомительный и подверженный ошибкам шаблонный код.

Обзор лучших практик

  • Используйте внедрение конструктора с помощью @Inject , чтобы добавлять типы в граф Dagger, когда это возможно. Когда это не так:
    • Используйте @Binds чтобы сообщить Dagger, какую реализацию должен иметь интерфейс.
    • Используйте @Provides чтобы сообщить Dagger, как предоставлять классы, которыми не владеет ваш проект.
  • Вы должны объявлять модули в компоненте только один раз.
  • Назовите аннотации области в зависимости от времени существования, в котором используется аннотация. Примеры: @ApplicationScope , @LoggedUserScope и @ActivityScope .

Добавление зависимостей

Чтобы использовать Dagger в своем проекте, добавьте эти зависимости в свое приложение в файле build.gradle . Последнюю версию Dagger вы можете найти в этом проекте на GitHub .

Котлин

plugins {
  id 'kotlin-kapt'
}

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    kapt 'com.google.dagger:dagger-compiler:2.x'
}

Ява

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}

Кинжал в Android

Рассмотрим пример Android-приложения с графом зависимостей, показанным на рисунке 1.

LoginActivity зависит от LoginViewModel, который зависит от UserRepository, который зависит от UserLocalDataSource и UserRemoteDataSource, который, в свою очередь, зависит от Retrofit.

Рисунок 1. Граф зависимостей примера кода

В Android вы обычно создаете граф Dagger, который находится в классе вашего приложения, потому что вы хотите, чтобы экземпляр графика находился в памяти, пока приложение работает. Таким образом, график привязывается к жизненному циклу приложения. В некоторых случаях вам также может потребоваться, чтобы контекст приложения был доступен на графике. Для этого вам также понадобится, чтобы график находился в классе Application . Одним из преимуществ этого подхода является то, что граф доступен другим классам платформы Android. Кроме того, он упрощает тестирование, позволяя использовать в тестах собственный класс Application .

Поскольку интерфейс, генерирующий график, помечен @Component , вы можете назвать его ApplicationComponent или ApplicationGraph . Обычно вы сохраняете экземпляр этого компонента в своем пользовательском классе Application и вызываете его каждый раз, когда вам нужен граф приложения, как показано в следующем фрагменте кода:

Котлин

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

Ява

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

Поскольку некоторые классы платформы Android, такие как действия и фрагменты, создаются системой, Dagger не может создать их за вас. В частности, для действий любой код инициализации должен быть передан в метод onCreate() . Это означает, что вы не можете использовать аннотацию @Inject в конструкторе класса (внедрение конструктора), как вы это делали в предыдущих примерах. Вместо этого вам придется использовать внедрение поля.

Вместо того, чтобы создавать зависимости, необходимые для действия, в методе onCreate() , вы хотите, чтобы Dagger заполнил эти зависимости за вас. Для внедрения полей вместо этого вы применяете аннотацию @Inject к полям, которые хотите получить из графа Dagger.

Котлин

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel
}

Ява

public class LoginActivity extends Activity {

    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject
    LoginViewModel loginViewModel;
}

Для простоты LoginViewModel не является ViewModel компонентов архитектуры Android ; это просто обычный класс, который действует как ViewModel. Для получения дополнительной информации о том, как внедрить эти классы, ознакомьтесь с кодом официальной реализации Android Blueprints Dagger , в ветке dev-dagger .

Одним из соображений, связанных с Dagger, является то, что внедренные поля не могут быть частными. Они должны иметь по крайней мере частную видимость пакета, как в предыдущем коде.

Инъекционная деятельность

Dagger должен знать, что LoginActivity должен получить доступ к графу, чтобы предоставить требуемую ViewModel . На странице основ Dagger вы использовали интерфейс @Component для получения объектов из графа, предоставляя функции с типом возврата того, что вы хотите получить из графа. В этом случае вам нужно сообщить Dagger об объекте (в данном случае LoginActivity ), который требует внедрения зависимости. Для этого вы предоставляете функцию, которая принимает в качестве параметра объект, запрашивающий внедрение.

Котлин

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

Ява

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

Эта функция сообщает Dagger, что LoginActivity хочет получить доступ к графу и запрашивает внедрение. Dagger должен удовлетворять всем зависимостям, которые требует LoginActivity ( LoginViewModel со своими собственными зависимостями). Если у вас есть несколько классов, требующих внедрения, вам необходимо специально объявить их все в компоненте с указанием их точного типа. Например, если у вас есть LoginActivity и RegistrationActivity , запрашивающие внедрение, у вас будет два метода inject() вместо общего, охватывающего оба случая. Общий метод inject() не сообщает Dagger, что необходимо предоставить. Функции в интерфейсе могут иметь любое имя, но вызов их inject() , когда они получают объект для внедрения в качестве параметра, является соглашением в Dagger.

Чтобы внедрить объект в действие, вы должны использовать appComponent , определенный в классе вашего Application , и вызвать метод inject() , передав экземпляр действия, которое запрашивает внедрение.

При использовании действий внедрите Dagger в метод onCreate() действия перед вызовом super.onCreate() чтобы избежать проблем с восстановлением фрагмента. На этапе восстановления в super.onCreate() действие присоединяет фрагменты, которым может потребоваться доступ к привязкам действий.

При использовании фрагментов добавьте Dagger в метод onAttach() фрагмента. В данном случае это можно сделать до или после вызова super.onAttach() .

Котлин

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

Ява

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

Давайте сообщим Dagger, как предоставить остальные зависимости для построения графа:

Котлин

class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor(
    private val loginService: LoginRetrofitService
) { ... }

Ява

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

Модули кинжала

В этом примере вы используете сетевую библиотеку Retrofit . UserRemoteDataSource зависит от LoginRetrofitService . Однако способ создания экземпляра LoginRetrofitService отличается от того, что вы делали до сих пор. Это не создание экземпляра класса; это результат вызова Retrofit.Builder() и передачи различных параметров для настройки службы входа в систему.

Помимо аннотации @Inject , есть еще один способ сообщить Dagger, как предоставить экземпляр класса: информация внутри модулей Dagger. Модуль Dagger — это класс, помеченный @Module . Там вы можете определить зависимости с помощью аннотации @Provides .

Котлин

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

Ява

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

Модули — это способ семантической инкапсуляции информации о том, как предоставлять объекты. Как видите, вы вызвали класс NetworkModule для группировки логики предоставления объектов, связанных с сетью. Если приложение расширяется, вы также можете добавить сюда информацию о том, как предоставить OkHttpClient или как настроить Gson или Moshi .

Зависимости метода @Provides — это параметры этого метода. Для предыдущего метода LoginRetrofitService можно предоставить без зависимостей, поскольку метод не имеет параметров. Если бы вы объявили OkHttpClient в качестве параметра, Dagger должен был бы предоставить экземпляр OkHttpClient из графа, чтобы удовлетворить зависимости LoginRetrofitService . Например:

Котлин

@Module
class NetworkModule {
    // Hypothetical dependency on LoginRetrofitService
    @Provides
    fun provideLoginRetrofitService(
        okHttpClient: OkHttpClient
    ): LoginRetrofitService { ... }
}

Ява

@Module
public class NetworkModule {

    @Provides
    public LoginRetrofitService provideLoginRetrofitService(OkHttpClient okHttpClient) {
        ...
    }
}

Чтобы граф Dagger знал об этом модуле, вам необходимо добавить его в интерфейс @Component следующим образом:

Котлин

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    ...
}

Ява

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
    ...
}

Рекомендуемый способ добавления типов в граф Dagger — использование внедрения конструктора (т. е. с помощью аннотации @Inject в конструкторе класса). Иногда это невозможно и приходится использовать модули Dagger. Одним из примеров является ситуация, когда вы хотите, чтобы Dagger использовал результат вычислений, чтобы определить, как создать экземпляр объекта. Всякий раз, когда ему необходимо предоставить экземпляр этого типа, Dagger запускает код внутри метода @Provides .

Вот как сейчас выглядит график Dagger в примере:

Схема графика зависимостей LoginActivity

Рисунок 2. Представление графа с LoginActivity , внедряемым Dagger.

Точкой входа в график является LoginActivity . Поскольку LoginActivity внедряет LoginViewModel , Dagger строит граф, который знает, как предоставить экземпляр LoginViewModel и рекурсивно его зависимости. Dagger знает, как это сделать, благодаря аннотации @Inject в конструкторе классов.

Внутри ApplicationComponent , сгенерированного Dagger, есть метод фабричного типа для получения экземпляров всех классов, которые он знает, как предоставить. В этом примере Dagger делегирует NetworkModule включенный в ApplicationComponent , чтобы получить экземпляр LoginRetrofitService .

Прицелы для кинжалов

Области были упомянуты на странице основ Dagger как способ иметь уникальный экземпляр типа в компоненте. Именно это подразумевается под ограничением типа жизненного цикла компонента .

Поскольку вы, возможно, захотите использовать UserRepository в других функциях приложения и не захотите создавать новый объект каждый раз, когда он вам понадобится, вы можете назначить его уникальным экземпляром для всего приложения. То же самое и с LoginRetrofitService : его создание может оказаться дорогостоящим, и вы также хотите, чтобы уникальный экземпляр этого объекта использовался повторно. Создание экземпляра UserRemoteDataSource не так уж и затратно, поэтому ограничивать его жизненным циклом компонента нет необходимости.

@Singleton — единственная аннотация области действия, которая поставляется с пакетом javax.inject . Вы можете использовать его для аннотирования ApplicationComponent и объектов, которые вы хотите повторно использовать во всем приложении.

Котлин

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

Ява

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

Будьте осторожны, чтобы не допускать утечек памяти при применении областей действия к объектам. Пока компонент с областью действия находится в памяти, созданный объект также находится в памяти. Поскольку ApplicationComponent создается при запуске приложения (в классе Application ), он уничтожается при уничтожении приложения. Таким образом, уникальный экземпляр UserRepository всегда остается в памяти до тех пор, пока приложение не будет уничтожено.

Подкомпоненты кинжала

Если ваш поток входа (управляемый одним LoginActivity ) состоит из нескольких фрагментов, вам следует повторно использовать один и тот же экземпляр LoginViewModel во всех фрагментах. @Singleton не может аннотировать LoginViewModel для повторного использования экземпляра по следующим причинам:

  1. Экземпляр LoginViewModel сохранится в памяти после завершения потока.

  2. Вам нужен другой экземпляр LoginViewModel для каждого потока входа в систему. Например, если пользователь выходит из системы, вам нужен другой экземпляр LoginViewModel , а не тот же экземпляр, который был при первом входе пользователя в систему.

Чтобы ограничить LoginViewModel жизненным циклом LoginActivity вам необходимо создать новый компонент (новый подграф) для потока входа в систему и новую область.

Давайте создадим график, специфичный для процесса входа в систему.

Котлин

@Component
interface LoginComponent {}

Ява

@Component
public interface LoginComponent {
}

Теперь LoginActivity должен получать инъекции от LoginComponent поскольку он имеет конфигурацию, специфичную для входа в систему. Это снимает необходимость внедрения LoginActivity из класса ApplicationComponent .

Котлин

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

Ява

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

LoginComponent должен иметь возможность доступа к объектам из ApplicationComponent , поскольку LoginViewModel зависит от UserRepository . Способ сообщить Dagger, что вы хотите, чтобы новый компонент использовал часть другого компонента, — это использовать подкомпоненты Dagger . Новый компонент должен быть подкомпонентом компонента, содержащего общие ресурсы.

Подкомпоненты — это компоненты, которые наследуют и расширяют граф объектов родительского компонента. Таким образом, все объекты, представленные в родительском компоненте, также предоставляются и в подкомпоненте. Таким образом, объект подкомпонента может зависеть от объекта, предоставленного родительским компонентом.

Для создания экземпляров подкомпонентов вам понадобится экземпляр родительского компонента. Таким образом, объекты, предоставляемые родительским компонентом подкомпоненту, по-прежнему относятся к родительскому компоненту.

В этом примере вы должны определить LoginComponent как подкомпонент ApplicationComponent . Для этого добавьте к LoginComponent аннотацию @Subcomponent :

Котлин

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

Ява

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

Вы также должны определить фабрику подкомпонентов внутри LoginComponent , чтобы ApplicationComponent знал, как создавать экземпляры LoginComponent .

Котлин

@Subcomponent
interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    fun inject(loginActivity: LoginActivity)
}

Ява

@Subcomponent
public interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    void inject(LoginActivity loginActivity);
}

Чтобы сообщить Dagger, что LoginComponent является подкомпонентом ApplicationComponent , вы должны указать это:

  1. Создание нового модуля Dagger (например, SubcomponentsModule ) с передачей класса субкомпонента в атрибут subcomponents аннотации.

    Котлин

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

    Ява

    // 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. Добавление нового модуля (т. е. SubcomponentsModule ) в ApplicationComponent :

    Котлин

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = [NetworkModule::class, SubcomponentsModule::class])
    interface ApplicationComponent {
    }
    

    Ява

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = {NetworkModule.class, SubcomponentsModule.class})
    public interface ApplicationComponent {
    }
    

    Обратите внимание, что ApplicationComponent больше не нужно внедрять LoginActivity поскольку теперь эта ответственность принадлежит LoginComponent , поэтому вы можете удалить метод inject() из ApplicationComponent .

    Потребители ApplicationComponent должны знать, как создавать экземпляры LoginComponent . Родительский компонент должен добавить в свой интерфейс метод, позволяющий потребителям создавать экземпляры подкомпонента из экземпляра родительского компонента:

  3. Откройте фабрику, которая создает экземпляры LoginComponent в интерфейсе:

    Котлин

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

    Ява

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

Назначение областей действия подкомпонентам

Если вы создаете проект, вы можете создавать экземпляры как ApplicationComponent , так и LoginComponent . ApplicationComponent привязан к жизненному циклу приложения, поскольку вы хотите использовать один и тот же экземпляр графа, пока приложение находится в памяти.

Каков жизненный цикл LoginComponent ? Одна из причин, по которой вам нужен LoginComponent заключается в том, что вам нужно было использовать один и тот же экземпляр LoginViewModel между фрагментами, связанными с входом в систему. Но также вам нужны разные экземпляры LoginViewModel при каждом новом процессе входа в систему. LoginActivity — подходящее время жизни для LoginComponent : для каждого нового действия вам нужен новый экземпляр LoginComponent и фрагменты, которые могут использовать этот экземпляр LoginComponent .

Поскольку LoginComponent привязан к жизненному циклу LoginActivity , вам необходимо сохранять ссылку на компонент в действии так же, как вы сохраняли ссылку на applicationComponent в классе Application . Таким образом, фрагменты смогут получить к нему доступ.

Котлин

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent
    ...
}

Ява

public class LoginActivity extends Activity {

    // Reference to the Login graph
    LoginComponent loginComponent;

    ...
}

Обратите внимание, что переменная loginComponent не помечена аннотацией @Inject поскольку вы не ожидаете, что Dagger предоставит эту переменную.

Вы можете использовать ApplicationComponent , чтобы получить ссылку на LoginComponent , а затем внедрить LoginActivity следующим образом:

Котлин

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

Ява

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

LoginComponent создается в методе onCreate() активности и неявно уничтожается при уничтожении активности.

LoginComponent всегда должен предоставлять один и тот же экземпляр LoginViewModel при каждом запросе. Вы можете обеспечить это, создав собственную область аннотаций и аннотировав ею LoginComponent и LoginViewModel . Обратите внимание, что вы не можете использовать аннотацию @Singleton , поскольку она уже использовалась родительским компонентом, и это сделало бы объект синглтоном приложения (уникальным экземпляром для всего приложения). Вам необходимо создать другую область аннотации.

В этом случае вы могли бы назвать эту область @LoginScope , но это не очень хорошая практика. Имя аннотации области не должно явно отражать цель, которую она выполняет. Вместо этого его следует называть в зависимости от его времени существования, поскольку аннотации могут повторно использоваться родственными компонентами, такими как RegistrationComponent и SettingsComponent . Вот почему вам следует называть его @ActivityScope вместо @LoginScope .

Котлин

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

Ява

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

Теперь, если у вас есть два фрагмента, которым требуется LoginViewModel , им обоим предоставляется один и тот же экземпляр. Например, если у вас есть LoginUsernameFragment и LoginPasswordFragment их необходимо внедрить с помощью LoginComponent :

Котлин

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

Ява

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

Компоненты получают доступ к экземпляру компонента, который находится в объекте LoginActivity . Пример кода для LoginUserNameFragment приведен в следующем фрагменте кода:

Котлин

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

Ява

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

То же самое для LoginPasswordFragment :

Котлин

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

Ява

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

На рис. 3 показано, как выглядит график Dagger с новым подкомпонентом. Классы с белой точкой ( UserRepository , LoginRetrofitService и LoginViewModel ) — это те классы, которые имеют уникальный экземпляр, ограниченный соответствующими компонентами.

График приложения после добавления последнего подкомпонента

Рис. 3. Представление построенного вами графика для примера приложения Android.

Разберем части графика:

  1. NetworkModule (и, следовательно, LoginRetrofitService ) включен в ApplicationComponent поскольку вы указали его в компоненте.

  2. UserRepository остается в ApplicationComponent , поскольку его область действия ограничена ApplicationComponent . Если проект растет, вы хотите использовать один и тот же экземпляр для разных функций (например, регистрации).

    Поскольку UserRepository является частью ApplicationComponent , его зависимости (т. е. UserLocalDataSource и UserRemoteDataSource ) также должны находиться в этом компоненте, чтобы иметь возможность предоставлять экземпляры UserRepository .

  3. LoginViewModel включен в LoginComponent поскольку он требуется только для классов, внедренных LoginComponent . LoginViewModel не включен в ApplicationComponent поскольку никакая зависимость в ApplicationComponent не требует LoginViewModel .

    Аналогично, если бы вы не указали область действия UserRepository в ApplicationComponent , Dagger автоматически включил бы UserRepository и его зависимости как часть LoginComponent , поскольку в настоящее время это единственное место, где используется UserRepository .

Помимо привязки объектов к разным жизненным циклам, хорошей практикой является создание подкомпонентов для инкапсуляции различных частей вашего приложения друг от друга .

Структурирование вашего приложения для создания различных подграфов Dagger в зависимости от потока вашего приложения помогает создать более производительное и масштабируемое приложение с точки зрения памяти и времени запуска.

Лучшие практики при построении графика Dagger

При построении графика Dagger для вашего приложения:

  • Когда вы создаете компонент, вам следует учитывать, какой элемент отвечает за время существования этого компонента. В этом случае класс Application отвечает за ApplicationComponent , а LoginActivity — за LoginComponent .

  • Используйте область видимости только тогда, когда это имеет смысл. Чрезмерное использование области действия может отрицательно повлиять на производительность вашего приложения во время выполнения: объект находится в памяти до тех пор, пока в памяти находится компонент, а получение объекта с областью действия обходится дороже. Когда Dagger предоставляет объект, он использует блокировку DoubleCheck вместо поставщика фабричного типа.

Тестирование проекта, использующего Dagger

Одним из преимуществ использования фреймворков внедрения зависимостей, таких как Dagger, является то, что они упрощают тестирование вашего кода.

Модульные тесты

Вам не обязательно использовать Dagger для модульных тестов . При тестировании класса, использующего внедрение конструктора, вам не нужно использовать Dagger для создания экземпляра этого класса. Вы можете напрямую вызвать его конструктор, передав поддельные или макетные зависимости, как если бы они не были аннотированы.

Например, при тестировании LoginViewModel :

Котлин

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

Ява

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

Сквозные тесты

Для интеграционных тестов хорошей практикой является создание TestApplicationComponent , предназначенного для тестирования. В производстве и тестировании используются разные конфигурации компонентов .

Это требует более тщательного проектирования модулей вашего приложения. Компонент тестирования расширяет производственный компонент и устанавливает другой набор модулей.

Котлин

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

Ява

// 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 имеет поддельную реализацию оригинального NetworkModule . Там вы можете предоставить поддельные экземпляры или макеты того, что вы хотите заменить.

Котлин

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

Ява

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

В ваших интеграционных или сквозных тестах вы должны использовать TestApplication , который создает TestApplicationComponent вместо ApplicationComponent .

Котлин

// Your test application needs an instance of the test graph
class MyTestApplication: MyApplication() {
    override val appComponent = DaggerTestApplicationComponent.create()
}

Ява

// Your test application needs an instance of the test graph
public class MyTestApplication extends MyApplication {
    ApplicationComponent appComponent = DaggerTestApplicationComponent.create();
}

Затем это тестовое приложение используется в пользовательском TestRunner , который вы будете использовать для запуска инструментальных тестов. Для получения дополнительной информации об этом ознакомьтесь с кодовой лабораторией «Использование Dagger в вашем приложении Android» .

Работа с модулями Dagger

Модули Dagger — это способ инкапсулировать семантический способ предоставления объектов. Вы можете включать модули в компоненты, но вы также можете включать модули внутри других модулей. Это мощный инструмент, но его легко использовать неправильно.

Как только модуль был добавлен к компоненту или другому модулю, он уже находится в графе Dagger; Dagger может предоставить эти объекты в этом компоненте. Прежде чем добавлять модуль, проверьте, является ли этот модуль уже частью графа Dagger, проверив, добавлен ли он уже в компонент, или скомпилировав проект и проверив, сможет ли Dagger найти необходимые зависимости для этого модуля.

Хорошая практика требует, чтобы модули объявлялись в компоненте только один раз (за исключением конкретных расширенных случаев использования Dagger).

Допустим, ваш график настроен таким образом. ApplicationComponent включает Module1 и Module2 , а Module1 включает ModuleX .

Котлин

@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module
class Module2 { ... }

Ява

@Component(modules = {Module1.class, Module2.class})
public interface ApplicationComponent { ... }

@Module(includes = {ModuleX.class})
public class Module1 { ... }

@Module
public class Module2 { ... }

Если теперь Module2 зависит от классов, предоставляемых ModuleX . Плохая практика — включать ModuleX в Module2 , поскольку ModuleX включается в граф дважды, как показано в следующем фрагменте кода:

Котлин

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

Ява

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

Вместо этого вам следует выполнить одно из следующих действий:

  1. Выполните рефакторинг модулей и извлеките общий модуль из компонента.
  2. Создайте новый модуль с объектами, которые являются общими для обоих модулей, и извлеките его в компонент.

Отсутствие рефакторинга таким образом приводит к тому, что множество модулей включают друг друга без четкого понимания организации и затрудняют понимание того, откуда берется каждая зависимость.

Хорошая практика (вариант 1) : ModuleX объявляется один раз в графе Dagger.

Котлин

@Component(modules = [Module1::class, Module2::class, ModuleX::class])
interface ApplicationComponent { ... }

@Module
class Module1 { ... }

@Module
class Module2 { ... }

Ява

@Component(modules = {Module1.class, Module2.class, ModuleX.class})
public interface ApplicationComponent { ... }

@Module
public class Module1 { ... }

@Module
public class Module2 { ... }

Передовая практика (вариант 2) : общие зависимости из Module1 и Module2 в ModuleX извлекаются в новый модуль с именем ModuleXCommon , который включается в компонент. Затем создаются два других модуля с именами ModuleXWithModule1Dependencies и ModuleXWithModule2Dependencies с зависимостями, специфичными для каждого модуля. Все модули объявляются один раз в графе Dagger.

Котлин

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

Ява

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

Вспомогательная инъекция

Вспомогательное внедрение — это шаблон внедрения внедрения, который используется для создания объекта, где некоторые параметры могут быть предоставлены платформой внедрения внедрения, а другие должны быть переданы пользователем во время создания.

В Android этот шаблон часто встречается на экранах подробных сведений , где идентификатор отображаемого элемента известен только во время выполнения, а не во время компиляции, когда Dagger генерирует график DI. Чтобы узнать больше о вспомогательной инъекции с помощью Dagger, см. документацию Dagger .

Заключение

Если вы еще этого не сделали, ознакомьтесь с разделом рекомендаций . Чтобы узнать, как использовать Dagger в приложении Android, см. кодовую лабораторию «Использование Dagger в приложении Android» .