На странице Основы Dagger объясняется, как Dagger помогает автоматизировать внедрение зависимостей в приложение. С Dagger вам не нужно писать утомительный и подверженный ошибкам шаблонный код.
Общие рекомендации
- По возможности используйте внедрение через конструктор с
@Inject, чтобы добавлять типы в граф Dagger. Если она отключена:- Используйте
@Binds, чтобы указать Dagger, какую реализацию должен иметь интерфейс. - Используйте
@Provides, чтобы указать Dagger, как предоставлять классы, которые не принадлежат вашему проекту.
- Используйте
- Модули в компоненте нужно объявлять только один раз.
- Назовите аннотации области видимости в зависимости от времени жизни, в котором используется аннотация. Примеры:
@ApplicationScope,@LoggedUserScopeи@ActivityScope.
Добавление зависимостей
Чтобы использовать Dagger в проекте, добавьте в файл build.gradle приложения следующие зависимости: Последнюю версию Dagger можно найти в этом проекте GitHub.
Kotlin
plugins { id 'kotlin-kapt' } dependencies { implementation 'com.google.dagger:dagger:2.x' kapt 'com.google.dagger:dagger-compiler:2.x' }
Java
dependencies { implementation 'com.google.dagger:dagger:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x' }
Dagger в Android
Рассмотрим пример приложения для Android с графиком зависимостей, показанным на рисунке 1.
Рисунок 1. График зависимостей для примера кода
В Android вы обычно создаете граф Dagger, который находится в классе вашего приложения, потому что вы хотите, чтобы экземпляр графа находился в памяти до тех пор, пока приложение работает. Таким образом, граф привязан к жизненному циклу приложения. В некоторых случаях вам также может потребоваться, чтобы в графе был доступен контекст приложения. Для этого вам также понадобится, чтобы граф был в классе Application. Преимущество такого подхода заключается в том, что граф доступен другим классам фреймворка Android.
Кроме того, это упрощает тестирование, позволяя использовать в тестах собственный класс Application.
Поскольку интерфейс, генерирующий граф, аннотирован с помощью @Component, вы можете вызвать его ApplicationComponent или ApplicationGraph. Обычно экземпляр этого компонента хранится в пользовательском классе Application и вызывается каждый раз, когда требуется граф приложения, как показано в следующем фрагменте кода:
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(); }
Поскольку некоторые классы фреймворка Android, такие как действия и фрагменты, создаются системой, Dagger не может создать их за вас. В частности, для действий любой код инициализации должен быть помещен в метод onCreate().
Это означает, что вы не можете использовать аннотацию @Inject в конструкторе класса (внедрение через конструктор), как в предыдущих примерах. Вместо этого вам нужно использовать внедрение через поле.
Вместо того чтобы создавать зависимости, необходимые для действия, в методе onCreate(), вы хотите, чтобы Dagger заполнил эти зависимости за вас. Для внедрения через поле вы вместо этого применяете аннотацию @Inject к полям, которые хотите получить из графа Dagger.
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; }
Для простоты LoginViewModel не является компонентом архитектуры Android ViewModel. Это обычный класс, который действует как ViewModel.
Дополнительную информацию о том, как внедрять эти классы, можно найти в коде официальной реализации Dagger в Android Blueprints в ветке dev-dagger.
Одно из соображений, связанных с Dagger, заключается в том, что внедренные поля не могут быть частными. Они должны иметь по крайней мере видимость на уровне пакета, как в предыдущем коде.
Внедрение действий
Dagger должен знать, что LoginActivity должен получить доступ к графу, чтобы предоставить необходимый ViewModel. На странице Основы Dagger вы использовали интерфейс @Component, чтобы получать объекты из графа, предоставляя функции с типом возвращаемого значения, которое вы хотите получить из графа. В этом случае вам нужно сообщить Dagger об объекте (в данном случае LoginActivity), которому требуется внедрение зависимости. Для этого вы предоставляете функцию, которая принимает в качестве параметра объект, запрашивающий внедрение.
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); }
Эта функция сообщает Dagger, что LoginActivity хочет получить доступ к графу и запрашивает внедрение. Dagger должен удовлетворять всем зависимостям, которые требуются для LoginActivity (LoginViewModel с собственными зависимостями).
Если у вас есть несколько классов, запрашивающих внедрение, вы должны явно объявить их все в компоненте с их точным типом. Например, если у вас есть LoginActivity и RegistrationActivity, запрашивающие внедрение, у вас будет два метода inject() вместо одного общего, охватывающего оба случая. Общий метод inject() не сообщает Dagger, что нужно предоставить. Функции в интерфейсе могут иметь любое название, но в Dagger принято называть их inject(), когда они получают объект для внедрения в качестве параметра.
Чтобы внедрить объект в действие, используйте appComponent, определенный в классе Application, и вызовите метод inject(), передав экземпляр действия, запрашивающего внедрение.
При использовании действий внедряйте Dagger в метод onCreate() действия перед вызовом super.onCreate(), чтобы избежать проблем с восстановлением фрагментов. На этапе восстановления в super.onCreate() действие прикрепляет фрагменты, которым может потребоваться доступ к привязкам действия.
При использовании фрагментов внедрите Dagger в метод onAttach() фрагмента. В этом случае его можно вызвать до или после super.onAttach().
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; } }
Давайте расскажем Dagger, как предоставить остальные зависимости для построения графа:
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; } }
Модули Dagger
В этом примере используется сетевая библиотека Retrofit.
UserRemoteDataSource зависит от LoginRetrofitService. Однако способ создания экземпляра LoginRetrofitService отличается от того, что вы делали до сих пор. Это не экземпляр класса, а результат вызова Retrofit.Builder() и передачи различных параметров для настройки сервиса входа.
Помимо аннотации @Inject, есть ещё один способ сообщить Dagger, как предоставить экземпляр класса: информация внутри модулей Dagger. Модуль Dagger – это класс, аннотированный с помощью @Module. В нем можно определить зависимости с помощью аннотации @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); } }
Модули позволяют семантически инкапсулировать информацию о том, как предоставлять объекты. Как видите, вы назвали класс NetworkModule, чтобы сгруппировать логику предоставления объектов, связанных с сетью. Если приложение расширяется, вы также можете добавить сюда информацию о том, как предоставить OkHttpClient или как настроить Gson или Moshi.
Зависимости метода @Provides – это его параметры. Для предыдущего метода LoginRetrofitService можно указать без зависимостей, так как у него нет параметров. Если бы вы объявили OkHttpClient в качестве параметра, Dagger потребовалось бы предоставить экземпляр OkHttpClient из графа, чтобы удовлетворить зависимости LoginRetrofitService. Пример:
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) { ... } }
Чтобы граф Dagger знал об этом модуле, добавьте его в интерфейс @Component следующим образом:
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 { ... }
Рекомендуемый способ добавления типов в граф Dagger – внедрение через конструктор (с помощью аннотации @Inject в конструкторе класса).
Иногда это невозможно, и вам приходится использовать модули Dagger. Например, когда вы хотите, чтобы Dagger использовал результат вычисления для определения того, как создать экземпляр объекта. Всякий раз, когда ему нужно предоставить экземпляр этого типа, Dagger запускает код внутри метода @Provides.
Вот как сейчас выглядит граф Dagger в примере:
Рисунок 2. Представление графа с внедрением LoginActivity с помощью Dagger
Точка входа в граф – LoginActivity. Поскольку LoginActivity внедряет LoginViewModel, Dagger создает граф, который знает, как предоставить экземпляр LoginViewModel и рекурсивно его зависимости. Dagger знает, как это сделать, благодаря аннотации @Inject в конструкторе классов.
Внутри ApplicationComponent, сгенерированного Dagger, есть метод фабричного типа для получения экземпляров всех классов, которые он может предоставить. В этом примере Dagger делегирует NetworkModule, включенному в ApplicationComponent, получение экземпляра LoginRetrofitService.
Области действия Dagger
Области действия упоминались на странице Основы Dagger как способ получить уникальный экземпляр типа в компоненте. Это и называется привязкой типа к жизненному циклу компонента.
Поскольку вы можете использовать UserRepository в других функциях приложения и не хотите создавать новый объект каждый раз, когда он вам нужен, вы можете назначить его уникальным экземпляром для всего приложения. То же самое касается LoginRetrofitService: его создание может быть дорогостоящим, и вы также хотите, чтобы уникальный экземпляр этого объекта использовался повторно. Создание экземпляра UserRemoteDataSource не требует больших ресурсов, поэтому ограничивать его жизненным циклом компонента не нужно.
@Singleton – единственная аннотация области видимости, которая поставляется с пакетом javax.inject. Вы можете использовать ее для аннотирования ApplicationComponent
и объектов, которые хотите повторно использовать во всем приложении.
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() { ... } }
Будьте осторожны, чтобы не допустить утечки памяти при применении областей видимости к объектам. Пока компонент находится в памяти, созданный объект также находится в памяти. Поскольку ApplicationComponent создается при запуске приложения (в классе Application), он удаляется при удалении приложения. Таким образом, уникальный экземпляр UserRepository всегда остается в памяти до тех пор, пока приложение не будет удалено.
Дочерние компоненты Dagger
Если процесс входа (управляемый одним LoginActivity) состоит из нескольких фрагментов, во всех них следует использовать один и тот же экземпляр LoginViewModel. @Singleton не может аннотировать LoginViewModel, чтобы повторно использовать экземпляр, по следующим причинам:
Экземпляр
LoginViewModelостанется в памяти после завершения процесса.Для каждого процесса входа нужен отдельный экземпляр
LoginViewModel. Например, если пользователь выходит из системы, вам нужен другой экземплярLoginViewModel, а не тот же, что и при первом входе пользователя в систему.
Чтобы ограничить область действия LoginViewModel жизненным циклом LoginActivity, вам нужно создать новый компонент (новый подграф) для процесса входа и новую область действия.
Давайте создадим граф, относящийся к процессу входа.
Kotlin
@Component interface LoginComponent {}
Java
@Component public interface LoginComponent { }
Теперь LoginActivity должен получать инъекции от LoginComponent, поскольку у него есть конфигурация, специфичная для входа. Это снимает ответственность за внедрение LoginActivity из класса ApplicationComponent.
Kotlin
@Component interface LoginComponent { fun inject(activity: LoginActivity) }
Java
@Component public interface LoginComponent { void inject(LoginActivity loginActivity); }
LoginComponent должен иметь доступ к объектам из ApplicationComponent, потому что LoginViewModel зависит от UserRepository. Чтобы сообщить Dagger, что вы хотите, чтобы новый компонент использовал часть другого компонента, используйте субкомпоненты Dagger. Новый компонент должен быть дочерним по отношению к компоненту, содержащему общие ресурсы.
Дочерние компоненты наследуют и расширяют граф объектов родительского компонента. Таким образом, все объекты, предоставляемые в родительском компоненте, также предоставляются в подкомпоненте. Таким образом, объект из подкомпонента может зависеть от объекта, предоставляемого родительским компонентом.
Чтобы создать экземпляры дочерних компонентов, вам нужен экземпляр родительского компонента. Поэтому объекты, предоставляемые родительским компонентом дочернему, по-прежнему относятся к родительскому компоненту.
В нашем примере LoginComponent нужно определить как дочерний компонент ApplicationComponent. Для этого добавьте в LoginComponent аннотацию @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); }
Вам также необходимо определить фабрику дочерних компонентов внутри LoginComponent, чтобы ApplicationComponent знал, как создавать экземпляры 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); }
Чтобы сообщить Dagger, что LoginComponent является подкомпонентом ApplicationComponent, необходимо указать это следующим образом:
Создайте новый модуль Dagger (например,
SubcomponentsModule), передав класс дочернего компонента в атрибутsubcomponentsаннотации.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 { }
Добавление нового модуля (например,
SubcomponentsModule) в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 { }
Обратите внимание, что
ApplicationComponentбольше не нужно внедрятьLoginActivity, поскольку теперь за это отвечаетLoginComponent, поэтому вы можете удалить методinject()изApplicationComponent.Пользователям
ApplicationComponentнужно знать, как создавать экземплярыLoginComponent. Родительский компонент должен добавить в свой интерфейс метод, позволяющий пользователям создавать экземпляры дочернего компонента из экземпляра родительского компонента:Предоставьте фабрику, которая создает экземпляры
LoginComponentв интерфейсе: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(); }
Назначение областей действия дочерним компонентам
Если вы создадите проект, то сможете создавать экземпляры как ApplicationComponent, так и LoginComponent. ApplicationComponent привязан к жизненному циклу приложения, поскольку вы хотите использовать один и тот же экземпляр графа, пока приложение находится в памяти.
Каков жизненный цикл LoginComponent? Одна из причин, по которой вам понадобился LoginComponent, заключается в том, что вам нужно было использовать один и тот же экземпляр LoginViewModel между фрагментами, связанными со входом в систему. Но также вам нужны разные экземпляры LoginViewModel при каждом новом процессе входа. LoginActivity – это правильный срок действия для LoginComponent. Для каждого нового действия нужен новый экземпляр LoginComponent и фрагменты, которые могут использовать этот экземпляр LoginComponent.
Поскольку LoginComponent привязан к жизненному циклу LoginActivity, вам нужно сохранить ссылку на компонент в действии так же, как вы сохранили ссылку на applicationComponent в классе Application. Таким образом,
фрагменты могут получить к нему доступ.
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; ... }
Обратите внимание, что переменная loginComponent не аннотирована с помощью @Inject, поскольку вы не ожидаете, что эта переменная будет предоставлена Dagger.
Вы можете использовать ApplicationComponent, чтобы получить ссылку на LoginComponent, а затем внедрить LoginActivity следующим образом:
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); } }
Объект LoginComponent создается в методе onCreate() операции и неявно уничтожается при уничтожении операции.
LoginComponent должен всегда предоставлять один и тот же экземпляр LoginViewModel при каждом запросе. Для этого можно создать специальную область аннотаций и добавить в нее LoginComponent и LoginViewModel. Обратите внимание, что вы не можете использовать аннотацию @Singleton, поскольку она уже используется родительским компонентом, и это сделает объект синглтоном приложения (уникальным экземпляром для всего приложения). Вам нужно создать другую область аннотаций.
В этом случае вы могли бы назвать область действия @LoginScope, но это не рекомендуется. Название аннотации области действия не должно явно указывать на ее назначение. Вместо этого его следует назвать в зависимости от времени его жизни, поскольку аннотации могут повторно использоваться родственными компонентами, такими как RegistrationComponent и SettingsComponent. Поэтому вместо @LoginScope лучше использовать @ActivityScope.
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; } }
Теперь, если у вас есть два фрагмента, которым нужен LoginViewModel, оба они получат один и тот же экземпляр. Например, если у вас есть LoginUsernameFragment и LoginPasswordFragment, их нужно внедрить с помощью 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); }
Компоненты получают доступ к экземпляру компонента, который находится в объекте LoginActivity. Пример фрагмента кода для LoginUserNameFragment приведен ниже:
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) } }
И то же самое для 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) } }
На рисунке 3 показано, как выглядит граф Dagger с новым подкомпонентом. Классы с белой точкой (UserRepository, LoginRetrofitService и LoginViewModel) – это классы, у которых есть уникальный экземпляр, ограниченный соответствующими компонентами.
Рисунок 3. Представление графа, созданного для примера приложения Android
Давайте разберём части графика:
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:
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(...); } }
Сквозные тесты
Для интеграционных тестов рекомендуется создать TestApplicationComponent, предназначенный для тестирования.
В рабочей и тестовой средах используются разные конфигурации компонентов.
Это требует более тщательного проектирования модулей в приложении. Тестовый компонент расширяет рабочий компонент и устанавливает другой набор модулей.
Kotlin
// TestApplicationComponent extends from ApplicationComponent to have them both // with the same interface methods. You need to include the modules of the // component here as well, and you can replace the ones you want to override. // This sample uses FakeNetworkModule instead of NetworkModule @Singleton @Component(modules = [FakeNetworkModule::class, SubcomponentsModule::class]) interface TestApplicationComponent : ApplicationComponent { }
Java
// TestApplicationComponent extends from ApplicationComponent to have them both // with the same interface methods. You need to include the modules of the // Component here as well, and you can replace the ones you want to override. // This sample uses FakeNetworkModule instead of NetworkModule @Singleton @Component(modules = {FakeNetworkModule.class, SubcomponentsModule.class}) public interface TestApplicationComponent extends ApplicationComponent { }
FakeNetworkModule – это поддельная реализация оригинального NetworkModule.
Там вы можете предоставить поддельные экземпляры или макеты того, что хотите заменить.
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(); } }
В интеграционных или сквозных тестах вы будете использовать TestApplication, который создает TestApplicationComponent вместо 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(); }
Затем это тестовое приложение используется в специальном TestRunner, который вы будете использовать для запуска инструментальных тестов. Дополнительную информацию можно найти в практической работе Использование Dagger в приложении для Android.
Работа с модулями Dagger
Модули Dagger позволяют инкапсулировать способ предоставления объектов семантическим способом. Вы можете включать модули в компоненты, а также в другие модули. Это мощный инструмент, но его легко использовать неправильно.
После добавления модуля в компонент или другой модуль он уже находится в графе Dagger. Dagger может предоставлять эти объекты в этом компоненте. Прежде чем добавлять модуль, проверьте, является ли он уже частью графа Dagger. Для этого посмотрите, добавлен ли он в компонент, или скомпилируйте проект и проверьте, может ли Dagger найти необходимые зависимости для этого модуля.
Рекомендуется объявлять модули в компоненте только один раз (за исключением особых случаев использования Dagger).
Предположим, вы настроили график следующим образом: ApplicationComponent включает Module1 и Module2, а Module1 включает ModuleX.
Kotlin
@Component(modules = [Module1::class, Module2::class]) interface ApplicationComponent { ... } @Module(includes = [ModuleX::class]) class Module1 { ... } @Module class Module2 { ... }
Java
@Component(modules = {Module1.class, Module2.class}) public interface ApplicationComponent { ... } @Module(includes = {ModuleX.class}) public class Module1 { ... } @Module public class Module2 { ... }
Если теперь Module2 зависит от классов, предоставляемых ModuleX. Не рекомендуется включать ModuleX в Module2, поскольку в этом случае ModuleX будет дважды включен в граф, как показано в следующем фрагменте кода:
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 { ... }
Вместо этого выполните одно из следующих действий:
- Проведите рефакторинг модулей и извлеките общий модуль в компонент.
- Создайте новый модуль с объектами, которые используются в обоих модулях, и извлеките его в компонент.
Если не провести рефакторинг, то получится много модулей, которые включают друг друга без четкой организации, и будет сложнее понять, откуда берется каждая зависимость.
Рекомендуемый способ (вариант 1). ModuleX объявляется в графе Dagger один раз.
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 { ... }
Рекомендуемый вариант 2. Общие зависимости из Module1 и Module2 в ModuleX извлекаются в новый модуль ModuleXCommon, который включается в компонент. Затем создаются два других модуля с именами ModuleXWithModule1Dependencies и ModuleXWithModule2Dependencies с зависимостями, специфичными для каждого модуля. Все модули объявляются в графе Dagger один раз.
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 { ... }
Внедрение с помощью ИИ
Вспомогательное внедрение – это шаблон внедрения зависимостей, который используется для создания объекта, в котором некоторые параметры могут быть предоставлены фреймворком внедрения зависимостей, а другие должны быть переданы пользователем во время создания.
В Android этот шаблон часто используется на экранах сведений, где идентификатор элемента, который нужно показать, известен только во время выполнения, а не во время компиляции, когда Dagger генерирует граф внедрения зависимостей. Дополнительную информацию о вспомогательном внедрении с помощью Dagger можно найти в документации по Dagger.
Заключение
Если вы ещё этого не сделали, ознакомьтесь с рекомендациями. Чтобы узнать, как использовать Dagger в приложении для Android, ознакомьтесь с практической работой.