Ручное внедрение зависимостей

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

Приложения Android обычно состоят из множества классов, и некоторые из них     зависят друг от друга.
Рисунок 1. Модель графа приложения 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);     } }

Есть проблемы с этим подходом:

  1. Там много шаблонного кода. Если вы захотите создать еще один экземпляр LoginViewModel в другой части кода, у вас возникнет дублирование кода.

  2. Зависимости должны быть объявлены по порядку. Чтобы создать его, вам необходимо создать экземпляр UserRepository перед LoginViewModel .

  3. Повторно использовать объекты сложно. Если вы хотите повторно использовать 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();     } }

Этот подход лучше предыдущего, но есть еще некоторые проблемы, которые следует учитывать:

  1. Вам придется управлять AppContainer самостоятельно, вручную создавая экземпляры для всех зависимостей.

  2. Существует еще много шаблонного кода. Вам необходимо создавать фабрики или параметры вручную в зависимости от того, хотите ли вы повторно использовать объект или нет.

Управление зависимостями в потоках приложений

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

  1. Если у вас разные потоки, вы можете захотеть, чтобы объекты просто находились в пределах этого потока. Например, при создании LoginUserData (который может состоять из имени пользователя и пароля, используемых только в процессе входа в систему) вы не хотите сохранять данные из старого потока входа в систему от другого пользователя. Вам нужен новый экземпляр для каждого нового потока. Этого можно добиться, создав объекты FlowContainer внутри AppContainer , как показано в следующем примере кода.

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

Представьте, что у вас есть поток входа в систему, состоящий из одного действия ( LoginActivity ) и нескольких фрагментов ( LoginUsernameFragment и LoginPasswordFragment ). Эти взгляды хотят:

  1. Получите доступ к тому же экземпляру LoginUserData , который необходимо использовать совместно до завершения процесса входа в систему.

  2. Создайте новый экземпляр 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 для автоматизации этого процесса и создания того же кода, который в противном случае вы бы написали вручную.