ViewModel — это компонент из набора библиотек под названием Android Architecture Components, которые были представлены на Google I/O 2017. ViewModel — предназначена для хранения и управления данными связанных с представлением, а также с возможностью “пережить” пересоздание активити (например переворот экрана).
На Хабре уже была хорошая статья посвящена ViewModel, где можно ознакомится с данной темой более детально.
В данной статье будет рассмотрены варианты инжекта(предоставление) зависимостей в компонент ViewModel
с использованием Dagger 2. Проблема заключается в том, что получение ViewModel должно осуществляться специальным образом, что в свою очередь накладывает некоторые ограничения, которые связанные с предоставлением зависимостей в сам класс ViewModel
, а также предоставление ViewModel
в качестве зависимости. Данная статья также возможно будет интересна тем, кто интересуется практическим применением такой функциональности Dagger, как multibinding.
Специальный способ получение ViewModel
заключается в следующем:
В начале мы должны получить ViewModelProvider
, который будет связан с активити или фрагментом, так же это определяет время жизни ViewModel
.
ViewModelProvider provider =
ViewModelProviders.of(<Activity|Fragment>[, ViewModelProvider.Factory]);
Второй параметр служит для указания фабрики, которая будет использоваться для создания инстанса ViewModel, не является обязательным, если мы его не указываем, будет использоваться фабрика по умолчанию. Фабрика по умолчанию поддерживает создание инстанса классов, которые являются наследниками ViewModel
(с конструктором без аргументов) и классов, которые являются наследниками AndroidViewModel
(c конструктором с одним аргументом — тип Application
).
Если мы хотим создать инстанс ViewModel
с собственными аргументами в конструкторе (которые не поддерживаются фабрикой по умолчанию), то нам необходимо реализовать собственную фабрику.
После того, как получили ViewModelProvider
, мы уже можем получить ViewModel
:
ProductViewModel productVM = provider.get(ProductViewModel.class);
Из вышеперечисленного описания следует:
- Чтобы иметь возможность предоставлять зависимости в
ViewModel
в качестве аргументов конструктора, нам необходимо реализовать собственную фабрику. - Мы можем воспользоваться фабрикой по умолчанию, но при этом также можем предоставить зависимости в
ViewModel
, используя компонент, который мы можем получить из Application. - Для правильного получения
ViewModel
нам необходимо иметь доступ к активити или фрагменту и получение должно осуществляться через классViewModelProviders
.
Предоставить зависимости в ViewModel
можно разными способами и каждого из способов есть свои плюсы и минусы, поэтому будет рассмотрены несколько вариантов.
Вариант с фабрикой по умолчанию
Начнем с определения модуля и сабкомпонента, которые будут использоваться для инжекта в активити:
@Module
public class ActivityModule {
@Provides
public ProductViewModel productViewModel(AppCompatActivity activity) {
return ViewModelProviders.of(activity).get(ProductViewModel.class);
}
}
@Subcomponent(modules = {ActivityModule.class})
public interface ActivitySubComponent {
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder with(AppCompatActivity activity);
ActivitySubComponent build();
}
void inject(MainActivity mainActivity);
}
Наличие такого модуля и сабкомпонента дает нам возможность запросить вью модель через @Inject
вместо ViewModelProviders.of(activity).get(ProductViewModel.class)
внутри нашей активити.
При использовании фабрики по умолчанию, созданием инстансов наших ViewModel
будет заниматься эта фабрика и мы не можем запрашивать зависимости в ViewModel
через конструктор, поэтому будем инжектить зависимости через компонент. Для того чтобы не засорять root компонент создадим сабкомпонент специально для вью моделей.
@Subcomponent
public interface ViewModelSubComponent {
@Subcomponent.Builder
interface Builder {
ViewModelSubComponent build();
}
void inject(ProductViewModel productViewModel);
}
Определим наш root компонент:
@Component(modules = {AppModule.class})
@Singleton
public interface AppComponent {
@Component.Builder
interface Builder {
@BindsInstance
Builder withApplication(Application application);
AppComponent build();
}
ViewModelSubComponent.Builder viewModelSubComponentBuilder();
ActivitySubComponent.Builder activitySubComponentBuilder();
}
AppModule
— будет содержать зависимости, которые нужны будут нашим вью моделям(например ProductDetailsFacade
).
Создадим Application, который будет содержать в себе root компонент и сабкомпонент для вью моделей:
public class App extends Application {
private AppComponent appComponent;
private ViewModelSubComponent viewModelSubComponent;
@Override
public void onCreate() {
super.onCreate();
appComponent = DaggerAppComponent
.builder()
.withApplication(this)
.build();
viewModelSubComponent = appComponent
.viewModelSubComponentBuilder()
.build();
}
public AppComponent getAppComponent() {
return appComponent;
}
public ViewModelSubComponent getViewModelSubComponent() {
return viewModelSubComponent;
}
}
Теперь мы можем заинжектить зависимости в ViewModel
:
public class ProductViewModel extends AndroidViewModel {
@Inject
ProductFacade productFacade;
public ProductViewModel(Application application) {
super(application);
//Получение компонента, может быть оптимизировано.
//Данный вариант для демонстрации
((App)application)
.getViewModelSubComponent()
.inject(this);
}
//methods
}
Вместо инжекта можно использовать provide методы у компонента.
Инжект ViewModel
в активити:
public class MainActivity extends AppCompatActivity {
@Inject
ProductViewModel productViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Получение компонента, может быть оптимизировано.
//Данный вариант для демонстрации
((App) getApplication())
.getAppComponent()
.activitySubComponentBuilder()
.with(this)
.build()
.inject(this);
}
}
Преимущества данного способа:
- Минимальные знания по Dagger 2.
- Используются только базовые “фичи” Dagger 2 (можно использовать на ранних версиях библиотеки).
- Использование стандартной фабрики по предоставлению
ViewModel
.
Недостатки:
- Inject метод для каждой
ViewModel
(или наличие провайд методов для каждой зависимости у компонента). - Inject внутри
ViewModel
.
Вариант с собственной фабрикой
Создадим пару вью моделей, где будем предоставлять зависимости через конструктор:
public class UserViewModel extends ViewModel {
private UserFacade userFacade;
@Inject
public UserViewModel(UserFacade userFacade) {
this.userFacade = userFacade;
}
//methods
}
public class UserGroupViewModel extends ViewModel {
private UserGroupFacade userGroupFacade;
@Inject
public UserGroupViewModel(UserGroupFacade userGroupFacade) {
this.userGroupFacade = userGroupFacade;
}
//methods
}
Создадим собственную аннотацию ключа, которые мы будем использовать для байндинга наших вью моделей в коллекцию для использования мультибайндинга. Про мультибайндиг можно почитать здесь.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@MapKey
@interface ViewModelKey {
Class<? extends ViewModel> value();
}
Определим модуль, где мы будем байндить в коллекцию наши вью модели:
@Module
public abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(UserViewModel.class)
abstract ViewModel userViewModel(UserViewModel userViewModel);
@Binds
@IntoMap
@ViewModelKey(UserGroupViewModel.class)
abstract ViewModel groupViewModel(UserGroupViewModel groupViewModel);
}
Перейдем к написанию нашей фабрики по предоставлению ViewModel
с использованием мультибайндинга:
public class DemoViewModelFactory implements ViewModelProvider.Factory {
private final Map<Class<? extends ViewModel>,
Provider<ViewModel>> viewModels;
@Inject
public DemoViewModelFactory(Map<Class<? extends ViewModel>,
Provider<ViewModel>> viewModels) {
this.viewModels = viewModels;
}
@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
Provider<ViewModel> viewModelProvider = viewModels.get(modelClass);
if (viewModelProvider == null) {
throw new IllegalArgumentException("model class "
+ modelClass
+ " not found");
}
return (T) viewModelProvider.get();
}
}
Provider<ViewModel>
дает нам возможность использовать отложенную инициализацию вью модели, а также получение каждый раз нового инстанса вью модели.
Без мультибайндинга у нас мог бы быть огромный блок из if/else.
Класс Application
:
@Component(modules = {AppModule.class, ViewModelModule.class})
@Singleton
public interface AppComponent {
@Component.Builder
interface Builder {
@BindsInstance
Builder withApplication(Application application);
AppComponent build();
}
void inject(MainActivity mainActivity);
}
Предоставление ViewModel
в наше активити:
public class MainActivity extends AppCompatActivity {
@Inject
DemoViewModelFactory viewModelFactory;
UserViewModel userViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
((App) getApplication())
.getAppComponent()
.inject(this);
userViewModel = ViewModelProviders.of(this, viewModelFactory)
.get(UserViewModel.class);
}
}
Преимущества данного способа:
- Можем использовать
@Inject
над конструктором вViewModel
и тем самым получать зависимости, а также добавитьViewModel
в граф зависимостей. Не нужно писать под каждую вью модель inject метод. - Одна простая фабрика для всех моделей.
- Простота добавления новой вью модели в фабрику.
Недостатки:
- По ошибке можно запросить модель не через фабрику(не через
ViewModelProviders.of()
, а с помощью@Inject MyViewModel
). Не совсем удобное получение вью модели. - Версия Dagger с поддержкой
multibinding и Provider<>
.
Если бы мы запрашивали модель через @Inject
, то мы бы просто получили инстанс вью модели (т.к. она уже находится в графе зависимостей) и она бы никак не была бы связана с жизненным циклом активити или фрагментом и не смогла бы “пережить” например переворот экрана.Чтобы это работало нам необходимо, чтобы создание происходило через фабрику.
Мы не можем дважды добавить в граф вью модели, т.е. мы не можем сделать следующее:
@Module
public class ActivityModule {
//будет ошибка, т.к. UserViewModel уже в графе зависимостей
//(@Inject над конструктором)
@Provides
public UserViewModel productViewModel(
DemoViewModelFactory viewModelFactory,
AppCompatActivity activity) {
return ViewModelProviders
.of(activity, viewModelFactory)
.get(UserViewModel .class);
}
}
Для обхода данного ограничения можно ввести интерфейс для модели и запрашивать вью модель по интерфейсу:
@Module
public class ActivityModule {
@Provides
public UserViewModelInterface productViewModel(
DemoViewModelFactory viewModelFactory,
AppCompatActivity activity) {
return ViewModelProviders
.of(activity, viewModelFactory)
.get(ProductViewModelImplementation.class);
}
}
public class MainActivity extends AppCompatActivity {
@Inject
UserViewModelInterface userViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
((App) getApplication())
.getAppComponent()
.activitySubComponentBuilder()
.with(this)
.build()
.inject(this);
}
}
На момент написания статьи использовался dagger 2.11 и архитектурные компоненты версии 1.0.0-alpha9. Как вы могли заметить архитектурные компоненты на момент написания статьи имеют альфа версию. Возможно в будущем появятся и другие методы получения вью модели.