Определение области видимости объекта A в объекте B означает, что на протяжении всего жизненного цикла объект B всегда будет иметь один и тот же экземпляр A. Что касается внедрения зависимостей, то ограничение объекта A контейнером означает, что контейнер всегда будет предоставлять один и тот же экземпляр A до тех пор, пока не будет уничтожен.

В Hilt вы можете ограничить типы контейнерами или компонентами с помощью аннотаций. Предположим, что в приложении содержится тип UserManager, который обрабатывает входы и выходы из системы. Вы можете ограничить его компонентом ApplicationComponent (который является контейнером под управлением жизненного цикла приложения), используя аннотацию @Singleton. Типы областей видимости в компоненте приложения следуют иерархии сверху вниз: в этом примере тот же экземпляр UserManager будет предоставляться остальным компонентам Hilt по иерархии. Каждый тип в приложении, зависящий от UserManager, получит один и тот же экземпляр.

Примечание: по умолчанию привязки в Hilt не ограничены. Они не являются частью какого-либо компонента и доступны на протяжении всего проекта. При каждом запросе будет предоставляться другой экземпляр этого типа. Определение привязки в компоненте ограничивает ее область видимости и зависимости, которые может иметь этот тип.

В Android область видимости можно определить вручную с помощью фреймворка Android, не используя библиотеку внедрения зависимостей. Давайте посмотрим, как это можно сделать, и как такая область видимости соотносится с той, что создана с помощью Hilt.

Определение области видимости в Android

С описанным выше определением можно утверждать, что определить область видимости типа можно с помощью переменной экземпляра этого типа в определенном классе. Без внедрения зависимостей можно сделать нечто вроде этого:

class ExampleActivity : AppCompatActivity() {
  private val analyticsAdapter = AnalyticsAdapter()
  ...
}

Область видимости переменной AnalyticsAdapter ограничена жизненным циклом ExampleActivity, соответственно она будет одним и тем же экземпляром до тех пор, пока ExampleActivity не будет уничтожена. Если другой компонент или контейнер по какой-то причине должен получить доступ к этой переменной, он также будет получать тот же самый экземпляр каждый раз. При создании нового экземпляра ExampleActivity (например, активность происходит через изменение конфигурации) будет создан новый экземпляр AnalyticsAdapter.

С Hilt аналогичный код выглядит так:

@ActivityScoped
class AnalyticsAdapter @Inject constructor() { ... 
}

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

@Inject lateinit var analyticsAdapter: 
AnalyticsAdapter

}

Каждый раз, когда создается ExampleActivity, она будет содержать новый экземпляр контейнера ActivityComponent, который будет предоставлять тот же экземпляр AnalyticsAdapter компонентам ниже него по иерархии до тех пор, пока активность не будет уничтожена.

Вы получаете новый экземпляр AnalyticsAdapter и MainActivity после изменения конфигурации

Определение области видимости с помощью ViewModel

Однако может в возникнуть необходимость в том, чтобы AnalyticsAdapter сохранился после изменений конфигурации. То есть необходимо ограничить этот экземпляр активностью до тех пор, пока пользователь сам не уничтожит его.

Для этого можно использовать Architecture Components ViewModel(модель представления компонентов архитектуры), поскольку она сохраняется после изменений конфигурации.

Без использования внедрения зависимостей код выглядит так:

class AnalyticsAdapter() { ... }

class ExampleViewModel() : ViewModel() {
  val analyticsAdapter = AnalyticsAdapter()
}

class ExampleActivity : AppCompatActivity() {

private val viewModel: ExampleViewModel by 
viewModels()
  private val analyticsAdapter = 
viewModel.analyticsAdapter

}

Этот способ позволяет расширить область видимости AnalyticsAdapter до ViewModel. Как только активность получит доступ к ViewModel, она всегда сможет получать один и тот же экземпляр AnalyticsAdapter.

В случае с Hilt можно добиться такого же поведения, ограничив AnalyticsAdapter компонентом ActivityRetainedComponent, который также сохраняется после изменений конфигурации:

@ActivityRetainedScoped
class AnalyticsAdapter @Inject constructor() { ... 
}

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

@Inject lateinit var analyticsAdapter: 
AnalyticsAdapter

}
Вы получаете тот же экземпляр AnalyticsAdapter после изменений конфигурации, что и с помощью аннотации ViewModel или Hilt ActivityRetainedScope

Если вы все же хотите сохранить ViewModel, потому что он должен выполнять некоторую логику представления, но при этом следовать хорошим практикам внедрения зависимостей, вы можете использовать Hilt для предоставления зависимостей ViewModel с помощью @ViewModelInject, как указано в документации. На этот раз AnalyticsAdapterне нужно ограничивать компонентом ActivityRetainedComponent, поскольку теперь он вручную ограничен областью видимости ViewModel:

class AnalyticsAdapter @Inject constructor() { ... 
}

class ExampleViewModel @ViewModelInject 
constructor(
  val analyticsAdapter: AnalyticsAdapter
) : ViewModel() { ... }

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

private val viewModel: ExampleViewModel by 
viewModels()
  private val analyticsAdapter = 
viewModel.analyticsAdapter

}

Все описанное выше может быть применено ко всем компонентам Hilt, управляемыми классами жизненного цикла фреймворка Android; полный список доступных областей видимости смотрите здесь. Возвращаясь к первоначальному примеру, определять область видимости в ApplicationComponent — это то же самое, что иметь экземпляр этого типа в классе приложения без использования принципов внедрения зависимости.

Определение области видимости в Hilt и с ViewModel

Преимущество Hilt заключается в том, что ограниченные типы доступны в иерархии компонентов Hilt, в то время как в ViewModel необходимо вручную обращаться к этим типам.

Преимущество определения области видимости с помощью ViewModel в том, что модели представления можно использовать для любых объектов LifeСycleOwner в приложении. Например, если вы используете библиотеку Jetpack Navigation, ViewModel можно прикрепить к NavGraph.

Hilt предоставляет ограниченное количество областей видимости. Может оказаться так, что для вашего конкретного варианта использования областей видимости не будет, например для вложенных фрагментов. В этом случае вы можете вернуться к использованию ViewModel.

Использование ViewModels с Hilt

Как было показано выше, вы можете использовать @ViewModelInject для внедрения зависимостей в ViewModels. Они хранятся в ActivityRetainedComponent, поэтому вы можете устанавливать только те типы, которые либо не ограничены, либо ограничены областью видимости ActivityRetainedComponentили ApplicationComponent.

ViewModelfactory, сгенерированный Hilt, доступен в методе getDefaultViewModelProviderFactory() конечной точки @AndroidEntryPoint. Это дает большую гибкость, поскольку вы можете использовать его в ViewModelProviderдля получения других моделей представления, например тех, что ограничены BackStackEntry.

Использование области видимости может быть дорогостоящим, поскольку предоставленный объект остается в памяти до тех пор, пока не будет уничтожен. Нужно быть внимательным при применении таких объектов в приложении. Целесообразно ограничивать объекты с внутренним состоянием, требующим использования одного и того же экземпляра, объекты, требующие синхронизации, или объекты, создание которых оценивается как дорогостоящее.

Однако, если действительно необходимо расширить область видимости, можно использовать аннотации Hilt или фреймворк Android.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Maniel Vivo: Scoping in Android and Hilt

Предыдущая статьяКак собрать кубик Рубика с помощью генетических алгоритмов
Следующая статьяNull - это зло!