Внедрение зависимостей (DI) — это шаблон проектирования, используемый для реализации инверсии управления (IoC), где управление созданием и управлением зависимостями передается от приложения внешней сущности. Это помогает создавать более модульный, тестируемый и поддерживаемый код. Это метод, при котором ответственность за создание объектов передается другим частям кода. Это способствует слабой связанности, делая код более модульным и простым в управлении.
Классам часто нужны ссылки на другие классы для правильного функционирования. Например, рассмотрим класс Library
, которому требуется класс Book
. Эти необходимые классы называются зависимостями. Класс Library
зависит от наличия экземпляра класса Book
для работы.
Существует три основных способа, с помощью которых класс может получить необходимые ему объекты:
Library
создаст и инициализирует свой собственный экземпляр класса Book
.Context
getters и getSystemService()
, работают таким образом.Library
получит экземпляр Book
в качестве параметра.Третий вариант — внедрение зависимостей! С помощью DI вы предоставляете зависимости класса, а не заставляете экземпляр класса получать их самостоятельно.
Без DI Library
, создающая собственную зависимость Book
, может выглядеть следующим образом:
class Library { private Book book = new Book(); void open() { book.read(); } } public class Main { public static void main(String[] args) { Library library = new Library(); library.open(); } }
Это не пример DI, поскольку класс Library
создает свой собственный Book
. Это может быть проблематично, потому что:
Library
и Book
тесно связаны. Экземпляр Library
использует один тип Book
, что затрудняет использование подклассов или альтернативных реализаций.Book
усложняет тестирование. Library
использует реальный экземпляр Book
, предотвращая использование тестовых двойников для модификации Book
для различных тестовых случаев. При использовании DI вместо того, чтобы каждый экземпляр Library
создавал свой собственный объект Book
, он получает объект Book
в качестве параметра в своем конструкторе:
class Library { private Book book; Library(Book book) { this.book = book; } void open() { book.read(); } } public class Main { public static void main(String[] args) { Book book = new Book(); Library library = new Library(book); library.open(); }
Основная функция использует Library
. Поскольку Library
зависит от Book
, приложение создает экземпляр Book
и затем использует его для построения экземпляра Library
. Преимущества этого подхода на основе DI следующие:
Library
: Вы можете передавать различные реализации Book
в Library
. Например, вы можете определить новый подкласс Book
с именем EBook
, который вы хотите, чтобы Library
использовала. С DI вы просто передаете экземпляр EBook
в Library
, и он работает без каких-либо дополнительных изменений.Library
: вы можете передавать тестовые дубликаты для проверки различных сценариев. Рассмотрим сценарий, в котором класс NotificationService
опирается на класс Notification
. Без DI NotificationService
напрямую создает экземпляр Notification
, что затрудняет использование различных типов уведомлений или тестирование службы с различными реализациями уведомлений.
Чтобы проиллюстрировать DI, давайте рефакторизируем этот пример:
interface Notification { void send(); } class EmailNotification implements Notification { @Override public void send() { // Send email notification } } class SMSNotification implements Notification { @Override public void send() { // Send SMS notification } } class NotificationService { void sendNotification(Notification notification) { notification.send(); } }
Теперь NotificationService
зависит от интерфейса Notification
, а не от конкретного класса. Это позволяет использовать различные реализации Notification
взаимозаменяемо. Вы можете задать реализацию, которую хотите использовать, с помощью метода sendNotification
:
NotificationService service = new NotificationService(); service.sendNotification(new EmailNotification()); service.sendNotification(new SMSNotification());
Существует три основных типа ДИ:
class NotificationService { private final Notification notification; public NotificationService(Notification notification) { this.notification = notification; } public void sendNotification() { notification.send(); } } public class Main { public static void main(String[] args) { NotificationService service = new NotificationService(new EmailNotification()); service.sendNotification(); } }
3. Field Injection (или Setter Injection) : Определенные классы фреймворка Android, такие как действия и фрагменты, создаются системой, поэтому внедрение конструктора невозможно. При внедрении поля зависимости создаются после создания класса.
class NotificationService { private Notification notification; public Notification getNotification() { return notification; } public void setNotification(Notification notification) { this.notification = notification; } public void sendNotification() { notification.send(); } } public class Main { public static void main(String[] args) { NotificationService service = new NotificationService(); service.setNotification(new EmailNotification()); service.sendNotification(); } }
4. Внедрение метода : зависимости предоставляются через методы, часто с использованием аннотации @Inject
.
В предыдущем примере вы вручную создавали, предоставляли и управляли зависимостями различных классов без использования библиотеки. Этот подход известен как ручное внедрение зависимостей. Хотя он работает в простых случаях, он становится громоздким по мере увеличения числа зависимостей и классов. Ручное внедрение зависимостей имеет несколько недостатков:
Библиотеки могут автоматизировать этот процесс, создавая и предоставляя вам зависимости. Эти библиотеки делятся на две категории:
Dagger — популярная библиотека внедрения зависимостей для Java, Kotlin и Android, поддерживаемая Google. Dagger упрощает DI в вашем приложении, создавая и управляя графом зависимостей для вас. Он предоставляет полностью статические зависимости времени компиляции, решая многие проблемы разработки и производительности, связанные с решениями на основе отражения, такими как Guice.
Эти фреймворки подключают зависимости во время выполнения:
Эти фреймворки генерируют код для подключения зависимостей во время компиляции:
Альтернативой внедрению зависимостей является шаблон локатора служб. Этот шаблон проектирования также помогает отделить классы от их конкретных зависимостей. Вы создаете класс, известный как локатор служб, который создает и хранит зависимости, предоставляя их по требованию.
object ServiceLocator { fun getProcessor(): Processor = Processor() } class Computer { private val processor = ServiceLocator.getProcessor() fun start() { processor.run() } } fun main(args: Array<String>) { val computer = Computer() computer.start() }
Шаблон локатора служб отличается от внедрения зависимостей тем, как потребляются зависимости. С шаблоном локатора служб классы запрашивают необходимые им зависимости; с внедрением зависимостей приложение заранее предоставляет требуемые объекты.
Dagger 2 — популярный фреймворк DI для Android. Он использует генерацию кода во время компиляции и известен своей высокой производительностью. Dagger 2 упрощает процесс внедрения зависимостей, генерируя необходимый код для обработки зависимостей, сокращая шаблонный код и повышая эффективность.
Dagger 2 — это библиотека на основе аннотаций для внедрения зависимостей в Android. Вот основные аннотации и их цели:
ApiClient
для Retrofit.@Module
и @Inject
. Он содержит все модули и предоставляет конструктор для приложения.@Provides
но более лаконично. Dagger может генерировать график зависимостей для вашего проекта, позволяя ему определять, где получать зависимости, когда это необходимо. Чтобы включить это, вам нужно создать интерфейс и аннотировать его с помощью @Component
.
В интерфейсе @Component
вы определяете методы, которые возвращают экземпляры нужных вам классов (например, BookRepository
). Аннотация @Component
предписывает Dagger сгенерировать контейнер со всеми зависимостями, необходимыми для удовлетворения типов, которые он выставляет. Этот контейнер называется компонентом Dagger и содержит граф объектов, которые Dagger знает, как предоставить вместе с их зависимостями.
Давайте рассмотрим пример с использованием LibraryRepository
:
@Inject
к конструктору LibraryRepository
, чтобы Dagger знал, как создать экземпляр LibraryRepository
. public class LibraryRepository { private final LocalLibraryDataSource localDataSource; private final RemoteLibraryDataSource remoteDataSource; @Inject public LibraryRepository(LocalLibraryDataSource localDataSource, RemoteLibraryDataSource remoteDataSource) { this.localDataSource = localDataSource; this.remoteDataSource = remoteDataSource; } }
2. Аннотируйте зависимости : Аналогичным образом аннотируйте конструкторы зависимостей ( LocalLibraryDataSource
и RemoteLibraryDataSource
), чтобы Dagger знал, как их создавать.
public class LocalLibraryDataSource { @Inject public LocalLibraryDataSource() { // Initialization code } } public class RemoteLibraryDataSource { private final LibraryService libraryService; @Inject public RemoteLibraryDataSource(LibraryService libraryService) { this.libraryService = libraryService; } }
3. Определите компонент : создайте интерфейс, аннотированный @Component
, для определения графа зависимостей.
@Component public interface ApplicationComponent { LibraryRepository getLibraryRepository(); }
При сборке проекта Dagger генерирует для вас реализацию интерфейса ApplicationComponent
, обычно называемую DaggerApplicationComponent
.
Теперь вы можете использовать сгенерированный компонент для получения экземпляров ваших классов с автоматически внедренными их зависимостями:
public class MainApplication extends Application { private ApplicationComponent applicationComponent; @Override public void onCreate() { super.onCreate(); applicationComponent = DaggerApplicationComponent.create(); } public ApplicationComponent getApplicationComponent() { return applicationComponent; } }
В вашей активности или фрагменте вы можете получить экземпляр LibraryRepository
:
public class MainActivity extends AppCompatActivity { @Inject LibraryRepository libraryRepository; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((MainApplication) getApplication()).getApplicationComponent().inject(this); // Use the injected libraryRepository } }
1. Модули
∘ Ключевые концепции модулей
∘ Включение модулей в компоненты
2. Области применения
3. Компоненты
4. Зависимости компонентов
5. Привязки во время выполнения
Модули в Dagger 2 — это классы, аннотированные @Module
, которые предоставляют зависимости компонентам. Они содержат методы, аннотированные @Provides
или @Binds
, чтобы указать, как создавать и предоставлять зависимости. Модули необходимы для организации и управления созданием объектов, которые нужны вашему приложению.
@Provides
и используется, когда модуль является абстрактным классом.Пример модуля
@Module public class NetworkModule { @Provides @Singleton Retrofit provideRetrofit() { return new Retrofit.Builder() .baseUrl("https://api.example.com") .addConverterFactory(GsonConverterFactory.create()) .build(); } @Provides @Singleton OkHttpClient provideOkHttpClient() { return new OkHttpClient.Builder() .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .build(); } }
В этом примере NetworkModule
— это класс, аннотированный @Module
. Он содержит два метода, аннотированные @Provides
, которые создают и возвращают экземпляры Retrofit
и OkHttpClient
.
Использование @Binds
Когда у вас есть интерфейс и его реализация, вы можете использовать @Binds
для привязки реализации к интерфейсу. Это более лаконично, чем использование @Provides
.
public interface ApiService { void fetchData(); } public class ApiServiceImpl implements ApiService { @Override public void fetchData() { // Implementation } } @Module public abstract class ApiModule { @Binds abstract ApiService bindApiService(ApiServiceImpl apiServiceImpl); }
В этом примере ApiModule
— абстрактный класс, аннотированный @Module
. Метод bindApiService
аннотирован @Binds
для привязки ApiServiceImpl
к ApiService
.
Модули можно организовать на основе предоставляемой ими функциональности. Например, можно иметь отдельные модули для сетевых операций, операций с базой данных и зависимостей, связанных с пользовательским интерфейсом.
Пример:
Retrofit
и OkHttpClient
.RoomDatabase
.ViewModel
и Presenter
.Модули включены в компоненты для предоставления зависимостей классам, которым они нужны. Вот как это можно настроить:
ApplicationComponent.java:
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
В этом примере ApplicationComponent
включает NetworkModule
и DatabaseModule
для предоставления зависимостей приложению.
Области действия в Dagger 2 — это аннотации, которые определяют жизненный цикл зависимостей. Они гарантируют, что один экземпляр зависимости создается и совместно используется в указанной области действия. Это помогает эффективно управлять памятью и гарантировать повторное использование зависимостей там, где это уместно.
1. Область действия синглтона
Определение : Область действия @Singleton
гарантирует, что будет создан и совместно использован на протяжении всего жизненного цикла приложения один экземпляр зависимости.
Эта область действия обычно используется для зависимостей, которые должны быть общими для всего приложения, например, сетевые клиенты, экземпляры баз данных или общие настройки.
Пример:
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
В этом примере аннотация @Singleton
гарантирует, что экземпляры Retrofit
и Database
, предоставляемые NetworkModule
и DatabaseModule
являются синглтонами и используются совместно во всем приложении.
2. Область деятельности
Определение : @ActivityScope
(настраиваемая область действия) гарантирует, что в жизненном цикле действия будет создан и совместно использован один экземпляр зависимости.
Эта область полезна для зависимостей, которые являются специфичными для действия и должны воссоздаваться каждый раз при повторном создании действия, например, для презентаторов или моделей представлений.
Пример :
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScope { } @ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
В этом примере аннотация @ActivityScope
гарантирует, что зависимости, предоставляемые ActivityModule
, ограничены жизненным циклом действия.
3. Область действия фрагмента
Определение : @FragmentScope
(еще одна настраиваемая область действия) гарантирует, что в жизненном цикле фрагмента создается и совместно используется один экземпляр зависимости.
Вариант использования: эта область полезна для зависимостей, которые специфичны для фрагмента и должны воссоздаваться каждый раз при воссоздании фрагмента, например, презентаторы или модели представлений, специфичные для фрагмента.
Пример :
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface FragmentScope { } @FragmentScope @Component(dependencies = ActivityComponent.class, modules = FragmentModule.class) public interface FragmentComponent { void inject(MyFragment myFragment); }
В этом примере аннотация @FragmentScope
гарантирует, что зависимости, предоставляемые FragmentModule
ограничены жизненным циклом фрагмента.
Зависимости компонентов позволяют одному компоненту зависеть от другого, позволяя повторно использовать зависимости. Существует два основных типа зависимостей компонентов:
1. Компонент приложения
Определение : Компонент приложения предоставляет зависимости, которые необходимы во всем приложении. Обычно он ограничен @Singleton
, чтобы гарантировать, что зависимости являются общими для всего приложения.
Этот компонент используется для зависимостей, которые должны быть доступны глобально, например, сетевые клиенты, экземпляры баз данных или общие настройки.
Пример :
@Singleton @Component(modules = {NetworkModule.class, DatabaseModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
В этом примере ApplicationComponent
отвечает за предоставление экземпляров Retrofit
и Database
, которые являются общими для всего приложения.
2. Компонент активности
Определение : Компонент Activity предоставляет зависимости, необходимые в рамках определенной активности. Обычно он ограничен пользовательской областью действия, например @ActivityScope
, чтобы гарантировать, что зависимости будут воссоздаваться каждый раз при воссоздании активности.
Этот компонент используется для зависимостей, специфичных для определенной деятельности, таких как презентаторы или модели представлений.
Пример :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
В этом примере ActivityComponent
зависит от ApplicationComponent
и предоставляет зависимости, специфичные для MainActivity
.
Зависимости компонентов позволяют одному компоненту зависеть от другого, позволяя повторно использовать зависимости. Существует два основных типа зависимостей компонентов:
1. Подкомпоненты:
Подкомпонент является дочерним элементом другого компонента и может иметь доступ к зависимостям своего родителя. Подкомпоненты определяются внутри родительского компонента и могут наследовать его область действия.
Пример :
@ActivityScope @Subcomponent(modules = ActivityModule.class) public interface ActivitySubcomponent { void inject(MainActivity mainActivity); }
В этом примере ActivitySubcomponent
является подкомпонентом родительского компонента и может получать доступ к его зависимостям.
2. Атрибут зависимости
Это позволяет компоненту зависеть от другого компонента, не будучи подкомпонентом. Зависимый компонент может получить доступ к зависимостям, предоставляемым родительским компонентом.
Пример :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); }
В этом примере ActivityComponent
зависит от ApplicationComponent
и может получить доступ к его зависимостям.
Привязки времени выполнения в Dagger 2 относятся к предоставлению зависимостей, которые создаются и управляются во время выполнения на основе контекста, в котором они необходимы.
1. Контекст приложения
Определение : Контекст приложения — это контекст, привязанный к жизненному циклу всего приложения. Он используется для зависимостей, которые должны существовать так же долго, как и само приложение.
Зависимости, которые являются общими для всего приложения и не требуют повторного создания для каждой активности или фрагмента. Примерами являются сетевые клиенты, экземпляры баз данных и общие настройки.
Пример :
@Module public class AppModule { private final Application application; public AppModule(Application application) { this.application = application; } @Provides @Singleton Application provideApplication() { return application; } @Provides @Singleton Context provideApplicationContext() { return application.getApplicationContext(); } }
В этом примере AppModule
предоставляет контекст приложения как одноэлементную зависимость. Метод provideApplicationContext
гарантирует, что предоставленный контекст привязан к жизненному циклу приложения.
2. Контекст деятельности
Определение : Контекст активности — это контекст, привязанный к жизненному циклу определенной активности. Он используется для зависимостей, которые должны существовать так же долго, как и сама активность.
Зависимости, которые являются специфическими для активности и должны быть созданы заново каждый раз при повторном создании активности. Примерами являются модели представлений, презентаторы и зависимости, связанные с пользовательским интерфейсом.
Пример :
@Module public class ActivityModule { private final Activity activity; public ActivityModule(Activity activity) { this.activity = activity; } @Provides @ActivityScope Activity provideActivity() { return activity; } @Provides @ActivityScope Context provideActivityContext() { return activity; } }
В этом примере ActivityModule
предоставляет контекст активности как зависимость с областью действия. Метод provideActivityContext
гарантирует, что предоставленный контекст привязан к жизненному циклу активности.
Чтобы использовать эти привязки времени выполнения, вам необходимо включить соответствующие модули в ваши компоненты:
Компонент приложения :
@Singleton @Component(modules = {AppModule.class, NetworkModule.class}) public interface ApplicationComponent { void inject(MyApplication application); Context getApplicationContext(); }
Компонент деятельности :
@ActivityScope @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity mainActivity); Context getActivityContext(); }
Внедрение контекстов
После настройки компонентов и модулей вы можете внедрять контексты в свои классы по мере необходимости.
Пример в действии :
public class MainActivity extends AppCompatActivity { @Inject Context activityContext; @Inject Context applicationContext; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ApplicationComponent appComponent = ((MyApplication) getApplication()).getApplicationComponent(); ActivityComponent activityComponent = DaggerActivityComponent.builder() .applicationComponent(appComponent) .activityModule(new ActivityModule(this)) .build(); activityComponent.inject(this); // Use the injected contexts Log.d("MainActivity", "Activity Context: " + activityContext); Log.d("MainActivity", "Application Context: " + applicationContext); } }
В этом примере MainActivity
получает как контекст активности, так и контекст приложения через внедрение зависимости. Это позволяет активности использовать соответствующий контекст на основе конкретных потребностей зависимостей.
Чтобы использовать Dagger 2 в вашем проекте, вам необходимо добавить следующие зависимости в ваш файл build.gradle
:
dependencies { implementation 'com.google.dagger:dagger:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x' }
Замените 2.x
на последнюю версию Dagger 2.
Создайте модуль для предоставления зависимостей. Например, NetworkModule
для предоставления экземпляра Retrofit
:
@Module public class NetworkModule { @Provides @Singleton Retrofit provideRetrofit() { return new Retrofit.Builder() .baseUrl("https://api.example.com") .addConverterFactory(GsonConverterFactory.create()) .build(); } }
Создайте компонент для соединения модуля и классов, которым нужны зависимости:
@Singleton @Component(modules = {NetworkModule.class}) public interface ApplicationComponent { void inject(MyApplication application); }
Используйте компонент для внедрения зависимостей в ваши классы. Например, в вашем классе Application
:
public class MyApplication extends Application { private ApplicationComponent applicationComponent; @Override public void onCreate() { super.onCreate(); applicationComponent = DaggerApplicationComponent.builder() .networkModule(new NetworkModule()) .build(); applicationComponent.inject(this); } public ApplicationComponent getApplicationComponent() { return applicationComponent; } }
Теперь вы можете использовать внедренные зависимости в своих классах. Например, в Activity
:
public class MainActivity extends AppCompatActivity { @Inject Retrofit retrofit; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((MyApplication) getApplication()).getApplicationComponent().inject(this); // Use the injected Retrofit instance // ... } }
Давайте подведем итог этой теме: