Концепции и реализация Jetpack Compose
Рекомендуемая архитектура приложений 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
) { ... }
Java
class UserLocalDataSource {
public UserLocalDataSource() { }
...
}
class UserRemoteDataSource {
private final Retrofit retrofit;
public UserRemoteDataSource(Retrofit retrofit) {
this.retrofit = retrofit;
}
...
}
class UserRepository {
private final UserLocalDataSource userLocalDataSource;
private final UserRemoteDataSource userRemoteDataSource;
public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
this.userLocalDataSource = userLocalDataSource;
this.userRemoteDataSource = userRemoteDataSource;
}
...
}
Вот как выглядит LoginActivity :
Котлин
class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// In order to satisfy the dependencies of LoginViewModel, you have to also
// satisfy the dependencies of all of its dependencies recursively.
// First, create retrofit which is the dependency of UserRemoteDataSource
val retrofit = Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LoginService::class.java)
// Then, satisfy the dependencies of UserRepository
val remoteDataSource = UserRemoteDataSource(retrofit)
val localDataSource = UserLocalDataSource()
// Now you can create an instance of UserRepository that LoginViewModel needs
val userRepository = UserRepository(localDataSource, remoteDataSource)
// Lastly, create an instance of LoginViewModel with userRepository
loginViewModel = LoginViewModel(userRepository)
}
}
Java
public class MainActivity extends Activity {
private LoginViewModel loginViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// In order to satisfy the dependencies of LoginViewModel, you have to also
// satisfy the dependencies of all of its dependencies recursively.
// First, create retrofit which is the dependency of UserRemoteDataSource
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LoginService.class);
// Then, satisfy the dependencies of UserRepository
UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit);
UserLocalDataSource localDataSource = new UserLocalDataSource();
// Now you can create an instance of UserRepository that LoginViewModel needs
UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);
// Lastly, create an instance of LoginViewModel with userRepository
loginViewModel = new LoginViewModel(userRepository);
}
}
У такого подхода есть недостатки:
Здесь много шаблонного кода. Если бы вы захотели создать ещё один экземпляр
LoginViewModelв другом месте кода, это привело бы к дублированию кода.Зависимости необходимо объявлять в правильном порядке. Для создания объекта
UserRepositoryнеобходимо создать его доLoginViewModel.Повторное использование объектов затруднительно. Если вы хотите использовать
UserRepositoryв нескольких функциях, вам придётся следовать шаблону Singleton . Шаблон Singleton усложняет тестирование, поскольку все тесты используют один и тот же экземпляр Singleton.
Управление зависимостями с помощью контейнера
Для решения проблемы повторного использования объектов можно создать собственный класс -контейнер зависимостей , который будет использоваться для получения зависимостей. Все экземпляры, предоставляемые этим контейнером, могут быть публичными. В примере, поскольку вам нужен только экземпляр UserRepository , вы можете сделать его зависимости приватными с возможностью сделать их публичными в будущем, если потребуется их предоставление:
Котлин
// Container of objects shared across the whole app
class AppContainer {
// Since you want to expose userRepository out of the container, you need to satisfy
// its dependencies as you did before
private val retrofit = Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LoginService::class.java)
private val remoteDataSource = UserRemoteDataSource(retrofit)
private val localDataSource = UserLocalDataSource()
// userRepository is not private; it'll be exposed
val userRepository = UserRepository(localDataSource, remoteDataSource)
}
Java
// Container of objects shared across the whole app
public class AppContainer {
// Since you want to expose userRepository out of the container, you need to satisfy
// its dependencies as you did before
private Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LoginService.class);
private UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit);
private UserLocalDataSource localDataSource = new UserLocalDataSource();
// userRepository is not private; it'll be exposed
public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);
}
Поскольку эти зависимости используются во всем приложении, их необходимо разместить в общем месте, доступном для всех действий: в классе 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()
}
Java
// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
public class MyApplication extends Application {
// Instance of AppContainer that will be used by all the Activities of the app
public AppContainer appContainer = new AppContainer();
}
Теперь вы можете получить экземпляр AppContainer из приложения и получить общий экземпляр UserRepository:
Котлин
class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Gets userRepository from the instance of AppContainer in Application
val appContainer = (application as MyApplication).appContainer
loginViewModel = LoginViewModel(appContainer.userRepository)
}
}
Java
public class MainActivity extends Activity {
private LoginViewModel loginViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Gets userRepository from the instance of AppContainer in Application
AppContainer appContainer = ((MyApplication) getApplication()).appContainer;
loginViewModel = new LoginViewModel(appContainer.userRepository);
}
}
Таким образом, у вас нет единственного экземпляра 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)
}
}
Java
// Definition of a Factory interface with a function to create objects of a type
public interface Factory<T> {
T create();
}
// 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 implements Factory<LoginViewModel> {
private final UserRepository userRepository;
public LoginViewModelFactory(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public LoginViewModel create() {
return new LoginViewModel(userRepository);
}
}
Вы можете включить LoginViewModelFactory в AppContainer и настроить LoginActivity на его использование:
Котлин
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory
class AppContainer {
...
val userRepository = UserRepository(localDataSource, remoteDataSource)
val loginViewModelFactory = LoginViewModelFactory(userRepository)
}
class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Gets LoginViewModelFactory from the application instance of AppContainer
// to create a new LoginViewModel instance
val appContainer = (application as MyApplication).appContainer
loginViewModel = appContainer.loginViewModelFactory.create()
}
}
Java
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory
public class AppContainer {
...
public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);
public LoginViewModelFactory loginViewModelFactory = new LoginViewModelFactory(userRepository);
}
public class MainActivity extends Activity {
private LoginViewModel loginViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Gets LoginViewModelFactory from the application instance of AppContainer
// to create a new LoginViewModel instance
AppContainer appContainer = ((MyApplication) getApplication()).appContainer;
loginViewModel = appContainer.loginViewModelFactory.create();
}
}
Этот подход лучше предыдущего, но всё же есть некоторые проблемы, которые следует учитывать:
- Вам придётся самостоятельно управлять
AppContainer, создавая экземпляры для всех зависимостей вручную. - В коде по-прежнему много шаблонного кода. Вам приходится создавать фабрики или параметры вручную в зависимости от того, хотите ли вы повторно использовать объект или нет.
Управление зависимостями в потоках приложений
AppContainer усложняется, когда вы хотите добавить в проект больше функциональности. По мере роста приложения и внедрения различных сценариев развития функций возникают ещё большие проблемы:
- При наличии нескольких потоков данных может потребоваться, чтобы объекты существовали только в рамках этого потока. Например, при создании
LoginUserData(который может содержать имя пользователя и пароль, используемые только в процессе входа в систему) вам не нужно сохранять данные из старого процесса входа в систему от другого пользователя. Вам нужен новый экземпляр для каждого нового потока. Этого можно добиться, создавая объектыFlowContainerвнутриAppContainer, как показано в следующем примере кода. - Оптимизация графа приложения и контейнеров потоков также может быть сложной задачей. Необходимо помнить об удалении ненужных экземпляров в зависимости от используемого потока.
Представьте, что у вас есть процесс авторизации, состоящий из одной активности ( LoginActivity ) и нескольких фрагментов ( LoginUsernameFragment и LoginPasswordFragment ). Эти представления хотят:
- До завершения процесса авторизации используйте тот же экземпляр
LoginUserData, который необходимо совместно использовать. - Создайте новый экземпляр объекта
LoginUserDataпри повторном запуске процесса.
Этого можно добиться с помощью контейнера для процесса авторизации. Этот контейнер необходимо создать в начале процесса авторизации и удалить из памяти по его завершении.
Давайте добавим 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
}
Java
// Container with Login-specific dependencies
class LoginContainer {
private final UserRepository userRepository;
public LoginContainer(UserRepository userRepository) {
this.userRepository = userRepository;
loginViewModelFactory = new LoginViewModelFactory(userRepository);
}
public LoginUserData loginData = new LoginUserData();
public LoginViewModelFactory loginViewModelFactory;
}
// AppContainer contains LoginContainer now
public class AppContainer {
...
public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);
// LoginContainer will be null when the user is NOT in the login flow
public LoginContainer loginContainer;
}
После того, как у вас есть контейнер, специфичный для потока, вам нужно решить, когда создавать и удалять экземпляр контейнера. Поскольку ваш поток авторизации является самодостаточным в активности ( LoginActivity ), именно активность управляет жизненным циклом этого контейнера. LoginActivity может создать экземпляр в onCreate и удалить его в onDestroy .
Котлин
class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel
private lateinit var loginData: LoginUserData
private lateinit var appContainer: AppContainer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appContainer = (application as MyApplication).appContainer
// Login flow has started. Populate loginContainer in AppContainer
appContainer.loginContainer = LoginContainer(appContainer.userRepository)
loginViewModel = appContainer.loginContainer.loginViewModelFactory.create()
loginData = appContainer.loginContainer.loginData
}
override fun onDestroy() {
// Login flow is finishing
// Removing the instance of loginContainer in the AppContainer
appContainer.loginContainer = null
super.onDestroy()
}
}
Java
public class LoginActivity extends Activity {
private LoginViewModel loginViewModel;
private LoginData loginData;
private AppContainer appContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
appContainer = ((MyApplication) getApplication()).appContainer;
// Login flow has started. Populate loginContainer in AppContainer
appContainer.loginContainer = new LoginContainer(appContainer.userRepository);
loginViewModel = appContainer.loginContainer.loginViewModelFactory.create();
loginData = appContainer.loginContainer.loginData;
}
@Override
protected void onDestroy() {
// Login flow is finishing
// Removing the instance of loginContainer in the AppContainer
appContainer.loginContainer = null;
super.onDestroy();
}
}
Подобно LoginActivity , фрагменты авторизации могут получать доступ к LoginContainer из AppContainer и использовать общий экземпляр LoginUserData .
Поскольку в данном случае вы имеете дело с логикой жизненного цикла представления, использование наблюдения за жизненным циклом имеет смысл.