Рекомендуемая архитектура приложений Android поощряет разделение кода на классы, чтобы воспользоваться принципом разделения ответственности, при котором каждый класс в иерархии имеет единственную определенную обязанность. Это приводит к появлению большего количества меньших классов, которые необходимо связать для выполнения зависимостей друг друга.

Зависимости между классами можно представить в виде графа, в котором каждый класс связан с классами, от которых он зависит. Представление всех ваших классов и их зависимостей образует граф приложения . На рисунке 1 вы можете увидеть абстракцию графа приложения. Когда класс A ( ViewModel ) зависит от класса B ( Repository ), существует линия, указывающая от A к B, представляющая эту зависимость.
Внедрение зависимостей помогает устанавливать эти связи и позволяет заменять реализации для тестирования. Например, при тестировании ViewModel , зависящей от репозитория, вы можете передавать различные реализации Repository с поддельными объектами или моками для проверки различных случаев.
Основы ручного внедрения зависимостей
В этом разделе рассматривается, как применять ручное внедрение зависимостей в реальном сценарии Android-приложения. Пошагово описывается, как начать использовать внедрение зависимостей в вашем приложении. Подход совершенствуется до тех пор, пока не достигнет уровня, очень похожего на тот, который Dagger автоматически генерирует для вас. Для получения дополнительной информации о Dagger, прочитайте раздел «Основы Dagger» .
Представьте себе поток как группу экранов в вашем приложении, соответствующих определенной функции. Вход в систему, регистрация и оформление заказа — все это примеры потоков.
При описании процесса авторизации для типичного Android-приложения, LoginActivity зависит от LoginViewModel , который, в свою очередь, зависит от UserRepository . UserRepository в свою очередь, зависит от UserLocalDataSource и UserRemoteDataSource , которые, в свою очередь, зависят от службы Retrofit .

LoginActivity является точкой входа в процесс авторизации, и пользователь взаимодействует с этой активностью. Таким образом, LoginActivity должна создать LoginViewModel со всеми его зависимостями.
Классы Repository и DataSource потока выглядят следующим образом:
class UserRepository(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) { ... }
class UserLocalDataSource { ... }
class UserRemoteDataSource(
private val loginService: LoginRetrofitService
) { ... }
В Compose ComponentActivity является точкой входа; связывание зависимостей происходит один раз в onCreate , а пользовательский интерфейс описывается компонентами, вызываемыми из setContent :
class ApiService {
/* Your API implementation here */
}
class UserRepository(private val apiService: ApiService) {
/* Your implementation here */
}
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Satisfy the dependencies of LoginViewModel recursively,
// then pass what the UI needs into setContent.
val apiService = ApiService()
val userRepository = UserRepository(apiService)
setContent {
LoginScreen(userRepository)
}
}
}
@Composable
fun LoginScreen(userRepository: UserRepository) {
val viewModel: LoginViewModel = viewModel(
factory = LoginViewModelFactory(userRepository)
)
// ...
}
У такого подхода есть недостатки:
- Зависимости необходимо объявлять в правильном порядке. Для создания объекта
UserRepositoryнеобходимо создать его доLoginViewModel. - Повторное использование объектов затруднительно. Если вы хотите использовать
UserRepositoryв нескольких функциях, вам придётся следовать шаблону Singleton . Шаблон Singleton усложняет тестирование, поскольку все тесты используют один и тот же экземпляр Singleton.
Управление зависимостями с помощью контейнера
Для решения проблемы повторного использования объектов можно создать собственный класс -контейнер зависимостей , который будет использоваться для получения зависимостей. Все экземпляры, предоставляемые этим контейнером, могут быть публичными. В примере, поскольку вам нужен только экземпляр UserRepository , вы можете сделать его зависимости приватными с возможностью сделать их публичными в будущем, если потребуется их предоставление:
// Container of objects shared across the whole app
class AppContainer {
// apiService and userRepository aren't private and will be exposed
val apiService = ApiService()
val userRepository = UserRepository(apiService)
}
Поскольку эти зависимости используются во всем приложении, их необходимо разместить в общем месте, доступном для всех действий: в классе Application . Создайте пользовательский класс Application , содержащий экземпляр AppContainer .
// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
class MyApplication : Application() {
// Instance of AppContainer that will be used by all the Activities of the app
val appContainer = AppContainer()
}
При использовании Compose тот же AppContainer по-прежнему создается в подклассе Application . Вы получаете к нему доступ либо в активности, до вызова setContent , либо изнутри компонуемого объекта, используя LocalContext :
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appContainer = (application as MyApplication).appContainer
setContent {
LoginScreen(appContainer.userRepository)
}
}
}
// Alternatively, read AppContainer from inside a composable:
@Composable
fun LoginScreen() {
val context = LocalContext.current
val appContainer = (context.applicationContext as MyApplication).appContainer
val viewModel: LoginViewModel = viewModel(
factory = LoginViewModelFactory(appContainer.userRepository)
)
// ...
}
Мы рекомендуем передавать зависимости в качестве параметров, которые можно комбинировать, вместо того, чтобы обращаться к LocalContext из глубины дерева. Это позволяет тестировать комбинируемые объекты и явно указывать их входные данные. Разрешите контейнер, находясь в корневом каталоге screen, и передавайте необходимые параметры вниз.
Таким образом, у вас нет единственного экземпляра UserRepository . Вместо этого у вас есть AppContainer общий для всех активностей, который содержит объекты из графа и создает экземпляры этих объектов, которые могут использоваться другими классами.
Если LoginViewModel требуется в нескольких местах приложения, имеет смысл иметь централизованное место для создания экземпляров LoginViewModel . Вы можете перенести создание LoginViewModel в контейнер и предоставлять новые объекты этого типа с помощью фабрики. Код для LoginViewModelFactory выглядит следующим образом:
// Definition of a Factory interface with a function to create objects of a type
interface Factory<T> {
fun create(): T
}
// Factory for LoginViewModel.
// Since LoginViewModel depends on UserRepository, in order to create instances of
// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
class LoginViewModelFactory(private val userRepository: UserRepository) : Factory<LoginViewModel> {
override fun create(): LoginViewModel {
return LoginViewModel(userRepository)
}
}
При использовании Compose обновление AppContainer по-прежнему предоставляет доступ к фабрике. Затем фабрика используется компонуемым объектом viewModel , так что область видимости ViewModel ограничивается ближайшим ViewModelStoreOwner (обычно это хост-активность или, в случае Navigation Compose, элемент навигации):
// AppContainer exposing the factory (unchanged from the snippet above)
class AppContainer {
// ...
val userRepository = UserRepository(localDataSource, remoteDataSource)
val loginViewModelFactory = LoginViewModelFactory(userRepository)
}
// Compose entry point + screen composable
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appContainer = (application as MyApplication).appContainer
setContent {
LoginScreen(appContainer.loginViewModelFactory)
}
}
}
@Composable
fun LoginScreen(factory: LoginViewModelFactory) {
val viewModel: LoginViewModel = viewModel(factory = factory)
// ...
}
Этот подход лучше предыдущего, но всё же есть некоторые проблемы, которые следует учитывать:
Вам придётся самостоятельно управлять
AppContainer, создавая экземпляры для всех зависимостей вручную.В коде по-прежнему много шаблонного кода. Вам приходится создавать фабрики или параметры вручную в зависимости от того, хотите ли вы повторно использовать объект или нет.
Управление зависимостями в потоках приложений
AppContainer усложняется, когда вы хотите добавить в проект больше функциональности. По мере роста приложения и внедрения различных сценариев развития функций возникают ещё большие проблемы:
При наличии нескольких потоков данных может потребоваться, чтобы объекты существовали только в рамках этого потока. Например, при создании
LoginUserData(который может содержать имя пользователя и пароль, используемые только в процессе входа в систему) вам не нужно сохранять данные из старого процесса входа в систему от другого пользователя. Вам нужен новый экземпляр для каждого нового потока. Этого можно добиться, создавая объектыFlowContainerвнутриAppContainer, как показано в следующем примере кода.Оптимизация графа приложения и контейнеров потоков также может быть сложной задачей. Необходимо помнить об удалении ненужных экземпляров в зависимости от используемого потока.
Давайте добавим LoginContainer в пример кода. Вам нужно иметь возможность создавать несколько экземпляров LoginContainer в приложении, поэтому вместо того, чтобы делать его синглтоном, сделайте его классом с зависимостями, необходимыми для процесса авторизации от AppContainer .
class LoginContainer(val userRepository: UserRepository) {
val loginData = LoginUserData()
val loginViewModelFactory = LoginViewModelFactory(userRepository)
}
// AppContainer contains LoginContainer now
class AppContainer {
...
val userRepository = UserRepository(localDataSource, remoteDataSource)
// LoginContainer will be null when the user is NOT in the login flow
var loginContainer: LoginContainer? = null
}
В Compose время жизни контейнера потока привязано к композиции, а не к хост- Activity . Вам не нужно изменять общий AppContainer.loginContainer , поскольку компонуемые объекты получают свои зависимости в качестве параметров или считывают их из поднятой ViewModel . У вас есть два варианта:
- Навигация. Создайте вложенный граф (предпочтительно для многоэкранных сценариев). Разместите все экраны в сценарии входа в систему под вложенным графом навигации и ограничьте область действия контейнера элементом
NavBackStackEntryэтого графа. Контейнер создается, когда пользователь входит в сценарий, и очищается, когда удаляется элемент из стека возврата, без необходимости ручных вызовов жизненного цикла. Для получения дополнительной информации см. раздел «Разработка графа навигации» . -
rememberнаходится в корневом каталоге экрана (для одноэкранного потока или когда вы не используете Navigation Compose). Создайте контейнер внутриrememberтаким образом, чтобы он создавался один раз для каждой записи в композиции и удалялся сборщиком мусора, когда компонуемый объект покидает ее:
@Composable
fun LoginFlow(appContainer: AppContainer) {
val loginContainer = remember(appContainer) {
LoginContainer(appContainer.userRepository)
}
val viewModel: LoginViewModel = viewModel(
factory = loginContainer.loginViewModelFactory
)
// Render the login flow using loginContainer.loginData and viewModel.
}
Заключение
Внедрение зависимостей — это хороший метод для создания масштабируемых и тестируемых Android-приложений. Используйте контейнеры для совместного использования экземпляров классов в разных частях приложения, а также в качестве централизованного места для создания экземпляров классов с помощью фабрик.
По мере роста вашего приложения вы начнете замечать, что пишете много шаблонного кода (например, фабрики), который может быть подвержен ошибкам. Вам также придется самостоятельно управлять областью действия и жизненным циклом контейнеров, оптимизируя и удаляя ненужные контейнеры, чтобы освободить память. Неправильный подход к этому может привести к скрытым ошибкам и утечкам памяти в вашем приложении.
В разделе, посвященном Dagger , вы узнаете, как с помощью Dagger автоматизировать этот процесс и сгенерировать тот же код, который в противном случае пришлось бы писать вручную.