На странице основ 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.
В 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
. Поскольку 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
для повторного использования экземпляра по следующим причинам:
Экземпляр
LoginViewModel
сохранится в памяти после завершения потока.Вам нужен другой экземпляр
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
, вы должны указать это:
Создание нового модуля 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 { }
Добавление нового модуля (т. е.
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
. Родительский компонент должен добавить в свой интерфейс метод, позволяющий потребителям создавать экземпляры подкомпонента из экземпляра родительского компонента:Откройте фабрику, которая создает экземпляры
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
) — это те классы, которые имеют уникальный экземпляр, ограниченный соответствующими компонентами.
Разберем части графика:
NetworkModule
(и, следовательно,LoginRetrofitService
) включен вApplicationComponent
поскольку вы указали его в компоненте.UserRepository
остается вApplicationComponent
, поскольку его область действия ограниченаApplicationComponent
. Если проект растет, вы хотите использовать один и тот же экземпляр для разных функций (например, регистрации).Поскольку
UserRepository
является частьюApplicationComponent
, его зависимости (т. е.UserLocalDataSource
иUserRemoteDataSource
) также должны находиться в этом компоненте, чтобы иметь возможность предоставлять экземплярыUserRepository
.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) : 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» .