Рекомендуемая архитектура приложений 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 ) { ... }
Ява
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) } }
Ява
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) }
Ява
// 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() }
Ява
// 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) } }
Ява
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{ override fun create(): LoginViewModel { return LoginViewModel(userRepository) } }
Ява
// 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{ 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() } }
Ява
// 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 }
Ява
// 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() } }
Ява
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
.
Поскольку в этом случае вы имеете дело с логикой жизненного цикла представления, использование наблюдения за жизненным циклом имеет смысл.
Заключение
Внедрение зависимостей — хороший метод создания масштабируемых и тестируемых приложений для Android. Используйте контейнеры как способ совместного использования экземпляров классов в разных частях вашего приложения и как централизованное место для создания экземпляров классов с помощью фабрик.
Когда ваше приложение станет больше, вы начнете видеть, что пишете много шаблонного кода (например, фабрик), который может быть подвержен ошибкам. Вам также придется самостоятельно управлять объемом и жизненным циклом контейнеров, оптимизируя и удаляя контейнеры, которые больше не нужны, чтобы освободить память. Неправильное выполнение этого действия может привести к тонким ошибкам и утечкам памяти в вашем приложении.
В разделе Dagger вы узнаете, как можно использовать Dagger для автоматизации этого процесса и создания того же кода, который в противном случае вы бы написали вручную.