Всем привет, эта статья о популярной и многим известной библиотеке Dagger, используемой для внедрения зависимостей (DI).
Статья актуальна не только для Android разработчиков, но и для тех кто собирается использовать готовые решения для DI на Java/Kotlin проектах.
В данном репозитории содержится два варианта Android приложения из Google codelab:
- ветка
with_dagger
- неизмененный вариант из оригинального репозитория - ветка
without_dagger
- переделанный в стиле сгенерированного кода Dagger без использования библиотеки
Я решил что написание кода который генерирует Dagger является хорошим примером для понимания как он устроен.
Давайте пройдемся по коду.
Условимся, что термин зависимость эквивалентен термину класс из Java/Kotlin языков.
Начнем с главного Dagger компонента:
@Singleton
// Definition of a Dagger component that adds info from the different modules to the graph
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
// Factory to create instances of the AppComponent
@Component.Factory
interface Factory {
// With @BindsInstance, the Context passed in will be available in the graph
fun create(@BindsInstance context: Context): AppComponent
}
// Types that can be retrieved from the graph
fun registrationComponent(): RegistrationComponent.Factory
fun loginComponent(): LoginComponent.Factory
fun userManager(): UserManager
}
Dagger компонент (аннотация @Component) это не какая то магическая штука которая должна существовать в единственном экземпляре, на самом деле вы можете создать множество Dagger компонентов с разными модулями и благодаря такой возможности Dagger является неплохим решением для многомодульных проектов.
Ключевая особенность Dagger компонента состоит в том что он является центральной концепцией библиотеки и все крутится вокруг него, это буквально контейнер с зависимостями.
Есть еще понятие дочернего компонента (аннотация @Subcomponent). Это расширенная концепция Dagger компонента, благодаря которой можно вкладывать в основной компонент дочерние. На самом деле вам ничего не мешает создать отдельные Dagger компоненты и ограничивать их жизненный цикл.
Посмотрим какой код будет сгенерирован для вышеуказанного Dagger компонента в упрощенном виде:
class AppComponentImpl private constructor(private val context: Context) : AppComponent {
private val sharedStorageProvider = Provider { SharedPreferencesStorage(context) }
val userManagerProvider = DoubleCheckProvider {
val userComponentFactory = object : UserComponent.Factory {
override fun create(): UserComponent = UserComponentImpl(this@AppComponentImpl)
}
UserManager(sharedStorageProvider.get(), userComponentFactory)
}
override fun loginComponent() = object : LoginComponent.Factory {
override fun create() = LoginComponentImpl(this@AppComponentImpl)
}
override fun registrationComponent() = object : RegistrationComponent.Factory {
override fun create() = RegistrationComponentImpl(this@AppComponentImpl)
}
override fun userManager(): UserManager = userManagerProvider.get()
class Factory : AppComponent.Factory {
override fun create(context: Context) = AppComponentImpl(context)
}
}
Разберемся с ключевыми моментами.
Provider это простейший параметризированный интерфейс с отдним методом, возвращающим экземпляр нужного класса (зависимости):
public interface Provider<T> {
T get();
}
Dagger не может знать когда вам нужен тот или иной класс и поэтому оборачивает процесс создания конкретного экземпляра в Provider.
Код модуля из указанного примера следующий:
@Module
abstract class StorageModule {
// Makes Dagger provide SharedPreferencesStorage when a Storage type is requested
@Binds
abstract fun provideStorage(storage: SharedPreferencesStorage): Storage
}
@Binds аннотация используется для того чтобы связать интерфейс Storage с его реализацией, на самом деле это упрощенная конструкция для:
@Module
object StorageModule {
@Provides
fun provideStorage(context: Context): Storage = SharedPreferencesStorage(context)
}
Если зависимости в модуле используются в нескольких классах или зависят от классов из Dagger компонента, то Dagger делает их частью компонента, для которого был прописан модуль. В более простом случае модуль напрямую передается в нужный класс.
3) Для дочерних компонентов (@Subcomponent) и зависимостей отмеченных Scope аннотациями создаются отдельные фабрики, так же как и для основного Dagger компонента
Для дочернего компонента вы сами прописываете интерфейс Factory, который реализует Dagger при кодгенерации:
@ActivityScope
// Definition of a Dagger subcomponent
@Subcomponent
interface LoginComponent {
// Factory to create instances of LoginComponent
@Subcomponent.Factory
interface Factory {
fun create(): LoginComponent
}
// Classes that can be injected by this Component
fun inject(activity: LoginActivity)
}
Обратите внимание что для объявления дочернего компонента LoginComponent используется самописная Scope аннотация ActivityScope.
Также в Dagger существуют собственные Scope аннотации например @Singleton:
@Singleton
class UserManager @Inject constructor(
....
) {
....
}
Для первого и второго случая Dagger генерирует специальные фабрики:
class AppComponentImpl private constructor(private val context: Context) : AppComponent {
...
override fun loginComponent() = object : LoginComponent.Factory {
override fun create() = LoginComponentImpl(this@AppComponentImpl)
}
...
}
Это тоже своего рода Provider обертки, только с одним отличием - фабрики гарантируют создание нового экземпляра зависимости (класса) при каждом вызове create() метода.
DoubleCheckProvider это одна из реализаций Provider интерфейса, которая при повторном вызове метода get() возвращает один и тот же экземпляр класса (зависимости). Вы можете называть это что-то вроде Singleton зависимости.
На самом деле в Dagger как такового Singleton не существует, так как вы можете хранить Dagger компонент не в пределах всего приложения, а локально в одном месте и он каждый раз будет пересоздаваться.
Важно придерживаться главной фишки Dagger основанной на компонентах, иначе говоря все что вы пишите привязано к Dagger компоненту, и от него зависит жизненный цикл всех зависимостей.
Вернемся к одному из дочерних компонентов и узнаем где происходит вызов inject() в Activity или во Fragment:
class RegistrationComponentImpl(private val appComponent: AppComponentImpl) : RegistrationComponent {
private val registrationViewModelProvider = DoubleCheckProvider {
RegistrationViewModel(appComponent.userManagerProvider.get())
}
override fun inject(activity: RegistrationActivity) {
activity.registrationViewModel = registrationViewModelProvider.get()
}
override fun inject(fragment: EnterDetailsFragment) {
fragment.registrationViewModel = registrationViewModelProvider.get()
fragment.enterDetailsViewModel = EnterDetailsViewModel()
}
override fun inject(fragment: TermsAndConditionsFragment) {
fragment.registrationViewModel = registrationViewModelProvider.get()
}
}
AppComponentImpl - реализация Dagger компонента, содержит общие зависимости для дочерных компонентов, поэтому RegistrationComponentImpl принимает его в качестве параметра конструктора.
RegistrationViewModel является общей зависимостью для RegistrationActivity, EnterDetailsFragment и TermsAndConditionsFragment, поэтому оборачивается в DoubleCheckProvider, чтобы все имели один и тот же экземпляр вьюмодели.
В моем примере конструкция inject() упрощена и не вынесена в отдельные обертки, которые генерирует Dagger:
public final class RegistrationActivity_MembersInjector implements MembersInjector<RegistrationActivity> {
private final Provider<RegistrationViewModel> registrationViewModelProvider;
public RegistrationActivity_MembersInjector(Provider<RegistrationViewModel> registrationViewModelProvider) {
this.registrationViewModelProvider = registrationViewModelProvider;
}
public static MembersInjector<RegistrationActivity> create(Provider<RegistrationViewModel> registrationViewModelProvider) {
return new RegistrationActivity_MembersInjector(registrationViewModelProvider);
}
@Override
public void injectMembers(RegistrationActivity instance) {
injectRegistrationViewModel(instance, registrationViewModelProvider.get());
}
@InjectedFieldSignature("com.example.android.dagger.registration.RegistrationActivity.registrationViewModel")
public static void injectRegistrationViewModel(RegistrationActivity instance, RegistrationViewModel registrationViewModel) {
instance.registrationViewModel = registrationViewModel;
}
}
Вам может показаться что этот код избыточен, но Dagger нуждается в таких обертках также как и в Provider и Factory. Это не человек способный понять, где нужно написать inject(), а где нужно создать зависимость.
Важно отметить, что Dagger генерирует больше кода по сравнению с тем что вы могли написать:
- Provider обертки на каждую зависимость или отдельные фабрики для дочерних компонентов и Scope аннотированных классов
- Классы обертки для inject вызовов в случае с Activity или Fragment
Dagger не стоит использовать в небольших проектах, так как это является лишней абстракцией и дополнительным увеличением количества Java/Kotlin классов в проекте. Следует добавить, что Dagger не является простой в понимании библиотекой, это усложняет читаемость кода и делает его неочевидным для тех кто первый раз столкнулся с ним в проекте.
Я считаю, что Dagger это хорошее решение для крупных проектов с многомодульной структурой, где можно его расширить и адаптировать под свои потребности.
Hilt является оберткой над Dagger и по мнению Google хорошим решением для ваших проектов.
Я не мог не обратить внимание на эту библиотеку и решил рассмотреть ее тоже.
Вы можете скачать codelab-android-hilt и глянуть своими глазами что генерирует Hilt, я лишь отмечу ключевые вещи:
- Hilt генерирует в 2+ раза больше кода чем Dagger
- Если Dagger не трогает ваши Activity и фрагменты, то Hilt генерирует для них суперклассы
- Сгенерированный код запутанный, сложно читаемый и неочевидный в отличии от Dagger
- Не решает задачи которые не может решить Dagger
В итоге, большие проекты с многомодульной структурой, как я уже отметил, вполне могут использовать Dagger, у него достаточно понятный сгенерированный код с возможностью адаптации под свои потребности.
Hilt напротив не стоит использовать в больших проектах, так как он добавляет еще один слой абстракции и его кодген сложный и запутанный, что повышает вероятность ошибок.
Что касается небольших проектов, так же как и в случае с Dagger не советую использовать Hilt как DI решение, пишите без лишних и ненужных абстракций, так ваш код не будет привязан к определенной библиотеки и останется понятным другим.
Наслаждайтесь жизнью, пишите хороший и понятный код, и конечно же делитесь знаниями с людьми!
Пожелания и улучшения: