Ручное внедрение зависимостей (представления)

Концепции и реализация Jetpack Compose

Рекомендуемая архитектура приложений 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
) { ... }

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);
   }
}

У такого подхода есть недостатки:

  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)
}

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();
   }
}

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

  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
}

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 .

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