Предлагаю вашему вниманию перевод оригинальной статьи от Jamie Sanson
Создание Activity до Android 9 Pie
Внедрение зависимостей (DI) — это общая модель, по ряду причин используемая во всех формах разработки. Благодаря проекту Dagger, он взят в качестве шаблона, используемого в разработке для Android. Недавние изменения в Android 9 Pie привели к тому, что теперь у нас есть больше возможностей, когда речь идет о DI, особенно с новым классом AppComponentFactory
.
DI очень важно, когда речь заходит о современной разработке Android. Это позволяет сократить общее количество кода при получении ссылок на сервисы, используемые между классами, и в целом хорошо разделяет приложение на компоненты. В этой статье мы сосредоточимся на Dagger 2, самой распространенной библиотеке DI, используемой в разработке Android. Предполагается, что вы уже обладаете базовыми знаниями о том, как это работает, но не обязательно понимать все тонкости. Стоит отметить, что эта статья — нечто вроде авантюры. Это интересно и всё, но на момент её написания Android 9 Pie даже не появлялся на панели версий платформы, поэтому, вероятно, данная тема не будет иметь отношения к повседневной разработке в течение как минимум нескольких лет.
Внедрение зависимостей в Android сегодня
Проще говоря, мы используем DI для предоставления экземпляров классов «зависимости» нашим зависимым классам, то есть тем, которые выполняют работу. Давайте предположим, что мы используем паттерн Репозиторий для обработки нашей логики, связанной с данными, и хотим использовать наш репозиторий в Activity для показа пользователю некоторых данных. Возможно, мы захотим использовать один и тот же репозиторий в нескольких местах, поэтому мы используем внедрение зависимостей, чтобы упростить совместное использование одного и того же экземпляра между кучей разных классов.
Для начала предоставим репозиторий. Мы определим функцию Provides
в модуле, позволяющую Dagger знать, что это именно тот экземпляр, который мы хотим внедрить. Обратите внимание, что нашему репозиторию нужен экземпляр контекста для работы с файлами и сетью. Мы предоставим ему контекст приложения.
@Module
class AppModule(val appContext: Context) {
@Provides
@ApplicationScope
fun provideApplicationContext(): Context = appContext
@Provides
@ApplicationScope
fun provideRepository(context: Context): Repository = Repository(context)
}
Теперь нам нужно определить Component
для обработки внедрения классов, в которых мы хотим использовать наш Repository
.
@ApplicationScope
@Component(modules = [AppModule::class])
interface ApplicationComponent {
fun inject(activity: MainActivity)
}
Наконец, мы можем настроить нашу Activity
чтобы использовать наш репозиторий. Предположим, что мы создали экземпляр нашего ApplicationComponent
где-то еще.
class MainActivity: AppCompatActivity() {
@Inject
lateinit var repository: Repository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Инжектим зависимости здесь
application.applicationComponent.inject(this)
// Теперь мы можем использовать наш репозиторий
}
}
Вот и всё! Мы только что настроили внедрение зависимостей в рамках приложения с помощью Dagger. Есть несколько способов сделать это, но это кажется самым простым подходом.
Что не так с текущим подходом?
В приведенных выше примерах мы увидели два разных типа инъекций, один очевиднее другого.
Первый, который вы, возможно, пропустили, известен как внедрение в конструктор. Это метод предоставления зависимостей через конструктор класса, означающий, что класс, использующий зависимости, не имеет представления о происхождении экземпляров. Это считается самой чистой формой внедрения зависимостей, так как она прекрасно инкапсулирует нашу логику внедрения в наши классы Module
. В нашем примере мы использовали этот подход для предоставления репозитория:
fun provideRepository(context: Context): Repository = Repository(context)
Для этого нужен был Context
, который мы предоставили в функции provideApplicationContext()
.
Второе, более очевидное, что мы увидели, это внедрение в поле класса. Этот метод использовался в нашей MainActivity
для предоставления нашего хранилища. Здесь мы определяем поля как получателей инъекций, используя аннотацию Inject
. Затем в нашей функции onCreate
мы сообщаем ApplicationComponent
что в наши поля надо инжектить зависимости. Выглядит не так чисто, как внедрение в конструктор, поскольку у нас есть явная ссылка на наш компонент, что означает, что концепция внедрения просачивается внутрь наших зависимых классов. Другой недостаток в классах Android Framework, так как мы должны быть уверены, что первое, что мы делаем — это предоставляем зависимости. Если это произойдёт не в той точке жизненного цикла, мы можем случайно попытаться использовать объект, который ещё не был инициализирован.
В идеале, следовало бы полностью избавиться от внедрений в поля класса. Такой подход пропускает информацию о внедрении в классы, которые не должны знать об этом, и потенциально может вызвать проблемы с жизненными циклами. Мы видели попытки сделать это лучше, и Dagger в Android — довольно надежный способ, но в конечном итоге было бы лучше, если бы мы могли просто использовать внедрение в конструктор. В настоящее время мы не можем использовать такой подход для ряда классов фреймворка, таких как «Activity», «Service», «Application» и т. д., поскольку они создаются для нас системой. Кажется, на данный момент мы застряли на внедрении в поля классов. Тем не менее, в Android 9 Pie готовится нечто интересное, что, возможно, в корне всё изменит.
Внедрение зависимостей в Android 9 Pie
Как уже упоминалось в начале статьи, в Android 9 Pie есть класс AppComponentFactory. Документация для него довольно скудная, и размещена просто на сайте разработчика как таковая:
Интерфейс, используемый для управления созданием элементов манифеста.
Это интригует. «Элементы манифеста» здесь относятся к классам, которые мы перечисляем в нашем файле AndroidManifest
— таким как Activity, Service и наш Application класс. Это позволяет нам «контролировать создание» этих элементов… так, погодите, мы теперь можем установить правила для создания наших Activity? Прелесть какая!
Давайте копнём глубже. Мы начнём с расширения AppComponentFactory
и переопределения метода instantiateActivity
.
class InjectionComponentFactory: AppComponentFactory() {
private val repository = NonContextRepository()
override fun instantiateActivity(cl: ClassLoader, className: String, intent: Intent?): Activity {
return when {
className == MainActivity::class.java.name -> MainActivity(repository)
else -> super.instantiateActivity(cl, className, intent)
}
}
}
Теперь нам нужно объявить нашу фабрику компонентов в манифесте внутри тега application.
<application android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:name=".InjectionApp"
android:appComponentFactory="com.mypackage.injectiontest.component.InjectionComponentFactory"
android:theme="@style/AppTheme"
tools:replace="android:appComponentFactory">
Наконец мы можем запустить наше приложение… и это работает! Наш NonContextRepository
предоставляется через конструктор MainActivity. Изящно!
Обратите внимание, что здесь есть некоторые оговорки. Мы не можем использовать Context
здесь, так как ещё до его существования происходит вызов нашей функции — это сбивает с толку! Мы можем пойти дальше, чтобы конструктор внедрил наш класс Application, но давайте посмотрим, как Dagger может сделать это еще проще.
Встречайте — Dagger Multi-Binds
Я не буду вдаваться в подробности работы множественного связывания Dagger под капотом, так как это выходит за пределы данной статьи. Всё, что вам нужно знать, — это то, что он обеспечивает хороший способ внедрения в конструктор класса без необходимости вызова конструктора вручную. Мы можем использовать это, чтобы легко внедрить классы фреймворка масштабируемым способом. Посмотрим, как всё это складывается.
Давайте сначала настроим нашу Activity, чтобы понять, что куда двигаться дальше.
class MainActivity @Inject constructor(
private val repository: NonContextRepository
): Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Отсюда мы можем использовать наш репозиторий
}
}
Из этого сразу видно, что почти нет упоминаний о внедрении зависимостей. Единственное, что мы видим, — это аннотацию Inject
перед конструктором.
Теперь необходимо изменить компонент и модуль Dagger:
@Component(modules = [ApplicationModule::class])
interface ApplicationComponent {
fun inject(factory: InjectionComponentFactory)
}
@Module(includes = [ComponentModule::class])
class ApplicationModule {
@Provides
fun provideRepository(): NonContextRepository = NonContextRepository()
}
Ничего особенного не изменилось. Теперь нам нужно только внедрить нашу фабрику компонентов, но как нам создать наши элементы манифеста? Здесь нам понадобится ComponentModule
. Давайте посмотрим:
@Module
abstract class ComponentModule {
@Binds
@IntoMap
@ComponentKey(MainActivity::class)
abstract fun bindMainActivity(activity: MainActivity): Any
@Binds
abstract fun bindComponentHelper(componentHelper: ComponentHelper): ComponentInstanceHelper
}
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ComponentKey(val clazz: KClass<out Any>)
Ага, хорошо, всего лишь несколько аннотаций. Здесь мы связываем нашу Activity
с мапой, внедряем эту мапу в наш класс ComponentHelper
и предоставляем этот ComponentHelper
— все в двух инструкциях Binds
. Dagger знает, как создать экземпляр нашей MainActivity
благодаря аннотации Inject
поэтому может «привязать» провайдера к этому классу, автоматически предоставляя необходимые нам зависимости для конструктора. Наш ComponentHelper
выглядит следующим образом.
class ComponentHelper @Inject constructor(
private val creators: Map<Class<out Any>, @JvmSuppressWildcards Provider<Any>>
): ComponentInstanceHelper {
@Suppress("UNCHECKED_CAST")
override fun <T> resolve(className: String): T? =
creators
.filter { it.key.name == className }
.values
.firstOrNull()
?.get() as? T
}
interface InstanceComponentHelper {
fun <T> resolve(className: String): T?
}
Проще говоря, теперь у нас есть карта классов для поставщиков для этих классов. Когда мы пытаемся разрешить класс по имени, мы просто находим провайдера для этого класса (если он у нас есть), вызываем его, чтобы получить новый экземпляр этого класса, и возвращаем его.
Наконец, нам нужно внести изменения в наш AppComponentFactory
чтобы использовать наш новый вспомогательный класс.
class InjectionComponentFactory: AppComponentFactory() {
@Inject
lateinit var componentHelper: ComponentInstanceHelper
init {
DaggerApplicationComponent.create().inject(this)
}
override fun instantiateActivity(cl: ClassLoader, className: String, intent: Intent?): Activity {
return componentHelper
.resolve<Activity>(className)
?.apply { setIntent(intent) } ?: super.instantiateActivity(cl, className, intent)
}
}
Запустите код еще раз. Это всё работает! Прелесть какая.
Проблемы внедрения в конструктор
Такой заголовок может выглядеть не очень впечатляющим. Хотя мы можем внедрить большинство экземпляров в обычном режиме с помощью внедрения в конструктор, у нас нет очевидного способа предоставления контекста для наших зависимостей стандартными способами. А ведь Context
в Android — это всё. Он нужен для доступа к настройкам, сети, конфигурации приложения и многому другому. Нашими зависимостями часто являются вещи, которые используют связанные с данными службы, такие как сеть и настройки. Мы можем обойти это, переписав наши зависимости, чтобы они состояли из чистых функций, или инициализируя все с помощью экземпляров контекста в нашем классе Application
, но для определения наилучшего способа сделать это требуется гораздо больше усилий.
Другим недостатком этого подхода является определение области видимости. В Dagger одной из ключевых концепций реализации высокопроизводительного внедрения зависимостей с хорошим разделением отношений между классами является модульность графа объекта и использование области видимости. Хотя этот подход не запрещает использование модулей, он ограничивает использование области видимости. AppComponentFactory
существует на совершенно ином уровне абстракции относительно наших стандартных классов фреймворка — мы не можем получить ссылку на него программно, поэтому у нас нет способа дать ему указание предоставить зависимости для Activity
в другой области видимости.
Существует множество способов решить наши проблемы с областями видимости на практике, одним из которых является использование FragmentFactory
для внедрения наших фрагментов в конструктор с областями видимости. Я не буду вдаваться в подробности, но оказывается, что сейчас у нас есть метод управления созданием фрагментов, который не только дает нам гораздо большую свободу с точки зрения области видимости, но и обладает обратной совместимостью.
Заключение
В Android 9 Pie появился способ использования внедрения в конструктор для предоставления зависимостей в наших классах фреймворка, таких как «Activity» и «Application». Мы увидели, что с помощью Dagger Multi-binding мы можем с легкостью предоставлять зависимости на уровне приложения.
Конструктор, внедряющий все наши компоненты, чрезвычайно привлекателен, и мы можем даже сделать что-то, чтобы заставить его работать должным образом с экземплярами контекста. Это многообещающее будущее, но оно доступно только начиная с API 28. Если вы хотите охватить менее 0,5% пользователей — можете попробовать. В противном случае стоит подождать и посмотреть, останется ли такой способ актуальным через несколько лет.