- Способы инжектить ViewModel с помощью Dagger: что может пойти не так
- 1. Map , Provider > в ViewModelProvider.Factory (с мультибиндингом или без)
- 2. Используем Hilt
- 3. Получаем ViewModel из DI и передаем ссылку в фабрику во viewModels-делегате
- 4. Передаем лямбду для создания ViewModel в фабрику
- 5. Используем @AssistedInject
- 6. Бонус
- Заключение
- Dagger2 и архитектурный компонент «ViewModel»
- Вариант с фабрикой по умолчанию
- Вариант с собственной фабрикой
- Dagger-Dot-Android Part 2: ViewModels and ViewModel Factories
- Setup
- MainActivity
- Explicit bindings
- Implicit bindings
- MainActivityViewModelFactory
- ViewModelProvider.Factory
- ViewModelProvider.Factory, defined
- Why do we care about ViewModel s at all?
- Heavy objects and MutableObjectFactory
Способы инжектить ViewModel с помощью Dagger: что может пойти не так
Инъекция зависимостей во ViewModel — очень популярная тема для статей по всему интернету. Давайте посмотрим, какие проблемы могут скрывать популярные подходы, и разберемся, есть ли способ инжектить ViewModel с помощью Dagger без огромного количества кода или потерь валидации графа зависимостей во время компиляции.
Disclaimer: чтобы разобраться в содержании этой статьи, вам потребуется знание Dagger.
Основная сложность использования DI с ViewModel заключается в том, что при создании ViewModel должна так или иначе проходить через ‘ViewModelProvider(this, factory).get(YourViewModel::class.java)’. Этот метод может быть скрыт внутри делегата ‘by viewModels < factory >’ или вызван напрямую. Без этого ViewModel не будет сохраняться при повороте экрана, а метод onCleared() не будет вызываться, когда ViewModel больше не нужна.
Чтобы сделать примеры как можно проще, я предположу, что у нас есть один компонент AppComponent. Но почти все примеры можно адаптировать к архитектуре с Subcomponent для каждой ViewModel или одним Subcomponent на все вьюмодели.
В большинстве примеров мы будем использовать такую ViewModel:
Repository предоставляется одним из модулей в AppComponent или просто имеет конструктор с аннотацией @Inject.
Также я предполагаю, что мы можем легко получить AppComponent внутри фрагмента, используя метод:
Теперь давайте посмотрим на существующие подходы и разберемся, какие проблемы они скрывают.
1. Map , Provider > в ViewModelProvider.Factory (с мультибиндингом или без)
Есть несколько вариантов реализации такого подхода. Самый простой — инжектить провайдеры в фабрику вьюмоделей и там собирать их в Map вручную:
Добавляем фабрику в компонент:
Теперь мы можем создать вьюмодель внутри фрагмента или активити:
Этот же подход можно реализовать, используя аннотации @IntoMap и @ClassKey(VM::class) для мультибайндинга, но суть будет та же.
Такой подход работает и позволяет заинжектить вьюмодель в пару строк, но у него есть и определенные ограничения:
Фабрика становится сервис-локатором. Это значит, что если мы забываем добавить несколько строк в фабрику (или модуль, если используем мультибайндинг) для новой вьюмодели, то получаем исключение во время выполнения приложения без какой-либо индикации во время компиляции. Обнаружение проблем во время компиляции — это одно из главных преимуществ Dagger, и не хотелось бы его терять.
Мы не можем передавать параметры во вьюмодель из фрагмента или активити. Этот подход не позволяет использовать @AssistedInject, хотя во всех вьюмоделях для однообразных параметров вроде SavedStateHandle можно использовать Subcomponent.
2. Используем Hilt
Hilt — это отличный инструмент от Google. С ним можно обойтись меньшим количеством кода. Пока мы не передаем никаких дополнительных параметров из фрагмента во вьюмодель, этот инструмент работает как часы:
Во фрагменте нам нужна будет только одна строчка (кроме необходимой аннотации):
Само собой это будет работать только в том случае, если корректно настроить Hilt, но для этого есть множество статей и официальная инструкция. Обратите внимание: если забыть @HiltViewModel, то приложение «упадет» во время выполнения, а не во время компиляции.
Но если мы хотим передать что-то из фрагмента во вьюмодель, то придется инжектить AssistedFactory во фрагмент и создавать фабрику вьюмоделей. ViewModel в этом случае может выглядеть примерно так:
Нам также понадобится универсальная фабрика вьюмоделей, просто чтобы избежать повторяющегося кода:
Теперь мы можем инжектить фабрику во фрагмент и использовать ее для создания вьюмодели:
В этом случае мы теряем некоторые преимущества Hilt и получаем что-то больше похожее на старый добрый Dagger с дополнительными шагами. Если это вас устраивает или вам не нужно ничего передавать во вьюмодель из фрагмента, то Hilt будет для вас отличным решением.
3. Получаем ViewModel из DI и передаем ссылку в фабрику во viewModels-делегате
Такой подход не будет работать, потому что лямбда, переданная во viewModels, будет вызываться при каждом повороте экрана и создавать новые экземпляры вьюмодели. Как ни странно, я видел такой подход в статье где-то на просторах интернета.
Метод viewModelComponent().myViewModel(), который вызывается при каждом повороте экрана, приведет к тому, что вьюмодели будут множиться. Если мы используем какие-то ресурсы внутри вьюмодели или запускаем корутины в конструкторе, то эти ресурсы и контекст для корутин не будут чиститься для всех вьюмоделей, кроме первой.
Даже если бы этот подход работал, есть шанс, что кто-нибудь вызовет ViewModelProvider().get() напрямую и получит тот же самый результат.
4. Передаем лямбду для создания ViewModel в фабрику
В принципе, этот подход работает, но в нем есть скрытая опасность. Допустим, мы используем ViewModel для сохранения какого-то утилитарного класса при повороте экрана (например, Router), а этот класс имеет конструктор, помеченный аннотацией @Inject, и наследует от ViewModel. Тогда мы можем, не глядя в код Router, добавить его как параметр в конструктор вьюмодели:
Что произойдет в таком случае? Router будет создан вместе с вьюмоделью и заинжекчен в ее конструктор, ничего необычного. Но когда будет вызван метод onCleared() вьюмодели, этот же метод не будет вызван для Router. Это может потенциально привести к утечке памяти или еще более неприятным и сложным к поимке багам.
Обратите внимание, что то же самое может произойти, если заинжектить и более очевидную вьюмодель внутрь вьюмодели. Не очень корректно использовать их таким образом, но лучший подход не оставляет места для ошибки, иначе мы бы использовали Koin или другой сервис-локатор вместо Dagger и не беспокоились бы о таких вещах.
Как же избежать этой проблемы? Например, использовать @AssistedInject.
5. Используем @AssistedInject
Если мы договоримся всегда использовать @AssistedInject для классов, наследующих от ViewModel, то указанная выше проблема не возникнет, а также у нас будет возможность передавать во вьюмодель дополнительные параметры.
Давайте немного подкорректируем нашу вьюмодель, чтобы поддержать assisted injection:
Подготовим фабрику, аналогичную предыдущему примеру:
И один метод, чтобы создавать «ленивый» делегат с фабрикой:
И, наконец, мы можем получить вьюмодель во фрагменте:
Этот подход немного сложнее предыдущих, но позволяет избежать редких проблем и передавать во вьюмодель дополнительные параметры, влючая SavedStateHandle, параметры страницы и прочее.
6. Бонус
В моей предыдущей статье я предложил подход, который освобождает от необходимости наследовать классы от ViewModel, а также альтернативный способ очистки ресурсов вьюмоделей. Похожий подход используется в этой библиотеке, так что я не один до этого додумался.
Если вы читали статью, то могли заметить, что lazyViewModel чем-то похож на getOrCreatePersisted из той статьи, хотя последний и не возвращает делегат.
Мы могли бы упаковать все зависимости из статьи в один Subcomponent примерно так:
Добавим функцию для создания сабкомпонента во фрагменте:
Добавим зависимостей и уберем наследование от ViewModel из нашей вьюмодели:
Упакуем lazy и getOrCreatePersisted в один метод:
И теперь можем легко создать нашу вьюмодель во фрагменте:
Таким образом, у нас не будет необходимости использовать @AssistedInject в том случае, когда он не нужен. Все зависимости вьюмодели, которые требуют очищения ресурсов, могут сами разобраться с ними, приняв в конструктор PersistentLifecycle в качестве параметра. Также наша вьюмодель больше не зависит напрямую от фреймворка, хотя уйти в Kotlin Multiplatform нам пока не позволит Dagger.
Заключение
Хотя это и не всегда очевидно, есть способы инжектить вьюмодель с помощью Dagger, не терять при этом валидацию графа зависимостей при компиляции и не использовать огромное количества кода. Особенно если избавиться от наследования ViewModel, всегда создавать вьюмодель через @AssistedInject или внимательно следить за тем, чтобы во вьюмодели не инджектились другие вьюмодели.
Источник
Dagger2 и архитектурный компонент «ViewModel»
ViewModel — это компонент из набора библиотек под названием Android Architecture Components, которые были представлены на Google I/O 2017. ViewModel — предназначена для хранения и управления данными связанных с представлением, а также с возможностью “пережить” пересоздание активити (например переворот экрана).
На Хабре уже была хорошая статья посвящена ViewModel, где можно ознакомится с данной темой более детально.
В данной статье будет рассмотрены варианты инжекта(предоставление) зависимостей в компонент ViewModel с использованием Dagger 2. Проблема заключается в том, что получение ViewModel должно осуществляться специальным образом, что в свою очередь накладывает некоторые ограничения, которые связанные с предоставлением зависимостей в сам класс ViewModel , а также предоставление ViewModel в качестве зависимости. Данная статья также возможно будет интересна тем, кто интересуется практическим применением такой функциональности Dagger, как multibinding.
Специальный способ получение ViewModel заключается в следующем:
В начале мы должны получить ViewModelProvider , который будет связан с активити или фрагментом, так же это определяет время жизни ViewModel .
Второй параметр служит для указания фабрики, которая будет использоваться для создания инстанса ViewModel, не является обязательным, если мы его не указываем, будет использоваться фабрика по умолчанию. Фабрика по умолчанию поддерживает создание инстанса классов, которые являются наследниками ViewModel (с конструктором без аргументов) и классов, которые являются наследниками AndroidViewModel (c конструктором с одним аргументом — тип Application ).
Если мы хотим создать инстанс ViewModel с собственными аргументами в конструкторе (которые не поддерживаются фабрикой по умолчанию), то нам необходимо реализовать собственную фабрику.
После того, как получили ViewModelProvider , мы уже можем получить ViewModel :
Из вышеперечисленного описания следует:
- Чтобы иметь возможность предоставлять зависимости в ViewModel в качестве аргументов конструктора, нам необходимо реализовать собственную фабрику.
- Мы можем воспользоваться фабрикой по умолчанию, но при этом также можем предоставить зависимости в ViewModel , используя компонент, который мы можем получить из Application.
- Для правильного получения ViewModel нам необходимо иметь доступ к активити или фрагменту и получение должно осуществляться через класс ViewModelProviders .
Предоставить зависимости в ViewModel можно разными способами и каждого из способов есть свои плюсы и минусы, поэтому будет рассмотрены несколько вариантов.
Вариант с фабрикой по умолчанию
Начнем с определения модуля и сабкомпонента, которые будут использоваться для инжекта в активити:
Наличие такого модуля и сабкомпонента дает нам возможность запросить вью модель через @Inject вместо ViewModelProviders.of(activity).get(ProductViewModel.class) внутри нашей активити.
При использовании фабрики по умолчанию, созданием инстансов наших ViewModel будет заниматься эта фабрика и мы не можем запрашивать зависимости в ViewModel через конструктор, поэтому будем инжектить зависимости через компонент. Для того чтобы не засорять root компонент создадим сабкомпонент специально для вью моделей.
Определим наш root компонент:
AppModule — будет содержать зависимости, которые нужны будут нашим вью моделям(например ProductDetailsFacade ).
Создадим Application, который будет содержать в себе root компонент и сабкомпонент для вью моделей:
Теперь мы можем заинжектить зависимости в ViewModel :
Вместо инжекта можно использовать provide методы у компонента.
Инжект ViewModel в активити:
Преимущества данного способа:
- Минимальные знания по Dagger 2.
- Используются только базовые “фичи” Dagger 2 (можно использовать на ранних версиях библиотеки).
- Использование стандартной фабрики по предоставлению ViewModel .
- Inject метод для каждой ViewModel (или наличие провайд методов для каждой зависимости у компонента).
- Inject внутри ViewModel .
Вариант с собственной фабрикой
Создадим пару вью моделей, где будем предоставлять зависимости через конструктор:
Создадим собственную аннотацию ключа, которые мы будем использовать для байндинга наших вью моделей в коллекцию для использования мультибайндинга. Про мультибайндиг можно почитать здесь.
Определим модуль, где мы будем байндить в коллекцию наши вью модели:
Перейдем к написанию нашей фабрики по предоставлению ViewModel с использованием мультибайндинга:
Provider дает нам возможность использовать отложенную инициализацию вью модели, а также получение каждый раз нового инстанса вью модели.
Без мультибайндинга у нас мог бы быть огромный блок из if/else.
Предоставление ViewModel в наше активити:
Преимущества данного способа:
- Можем использовать @Inject над конструктором в ViewModel и тем самым получать зависимости, а также добавить ViewModel в граф зависимостей. Не нужно писать под каждую вью модель inject метод.
- Одна простая фабрика для всех моделей.
- Простота добавления новой вью модели в фабрику.
- По ошибке можно запросить модель не через фабрику(не через ViewModelProviders.of() , а с помощью @Inject MyViewModel ). Не совсем удобное получение вью модели.
- Версия Dagger с поддержкой multibinding и Provider<> .
Если бы мы запрашивали модель через @Inject , то мы бы просто получили инстанс вью модели (т.к. она уже находится в графе зависимостей) и она бы никак не была бы связана с жизненным циклом активити или фрагментом и не смогла бы “пережить” например переворот экрана.Чтобы это работало нам необходимо, чтобы создание происходило через фабрику.
Мы не можем дважды добавить в граф вью модели, т.е. мы не можем сделать следующее:
Для обхода данного ограничения можно ввести интерфейс для модели и запрашивать вью модель по интерфейсу:
На момент написания статьи использовался dagger 2.11 и архитектурные компоненты версии 1.0.0-alpha9. Как вы могли заметить архитектурные компоненты на момент написания статьи имеют альфа версию. Возможно в будущем появятся и другие методы получения вью модели.
Источник
Dagger-Dot-Android Part 2: ViewModels and ViewModel Factories
(no, not those kind of factories)
If you read the first part of this tutorial, then you already know the basics of how to set up a project that uses the dagger.android package, Google’s (relatively) new take on Dagger and Android. You know the basics of injecting an Activity , and you know how to replace your app’s production objects with test doubles to make instrumentation testing a breeze (relatively speaking. ).
Now in Part 2, we’re going to learn how to use ViewModels and LiveData, from the Android Architecture Components, to manage the lifecycle of our Dagger-provided objects.
Setup
Add the following to your app/build.gradle file:
Exit fullscreen mode
As you can see, we’re just adding a single dependency. This one gives us access to both ViewModel and LiveData (and friends). Now, let’s use these classes for managing the state of our «View», represented by our activity.
MainActivity
This is very similar to what we saw in Part 1. New section are called out with comments.
Exit fullscreen mode
Here’s what we’ve changed from last time:
- We’re injecting a «view model factory», which is literally a ViewModelProvider.Factory. This is only necessary if your ViewModel has a non-default constructor. In other words, it’s absolutely necessary as soon as you want to do anything interesting.
- We use our view model factory to provide our actual ViewModel, which is the thing we really care about.
- We’re using by lazy because we really like immutability, and it’s the only way to have a val view model. (See the note at the bottom of this article for why I’m setting the mode.)
- We then use our ViewModel in concert with a couple of widgets.
The eagle-eyed among you will have noticed that we’re injecting a new object ( MainActivityViewModelFactory ), but I haven’t shown any code for how to inject this new object. Doesn’t it need a @Provides or @Binds inside a @Module ? Not so much. Dagger supports two kinds of bindings, which I will refer to as «explicit» and «implicit.»
Explicit bindings
Exit fullscreen mode
In this case, we are explicitly telling Dagger how it can create instances of IThing s and Thingama s. In the first case, we bind the IThing interface to its concrete implementation ThingImpl , and therefore we use @Bind . In the second case, the Thingama class (tragically) doesn’t implement an interface, so we have to provide instances of it directly, and therefore we use @Provides .
But even here we see that I’ve cheated. Where does ThingImpl come from? What about Bob ?
Implicit bindings
Exit fullscreen mode
These are implicit bindings, and they simply require we annotate one of our constructors (even a default one!) with @Inject .
Of course, we can specify non-default constructors with an arbitrary number of parameters, and still rely upon Dagger to instantiate these objects — just so long as our Directed Acyclic Graph of dependencies contains the information DAGger needs to provide each of the required objects.
MainActivityViewModelFactory
For trivial ViewModel s, we could follow the basic sample code in the docs and just do this:
Exit fullscreen mode
and, as you can see, nary a factory to be seen.
But if our view model is at all interesting, it will certainly require collaborators; that is, a non-default constructor. For example:
Exit fullscreen mode
but if we tried to get a reference to such a view model with the above example, it would fail! There is a default ViewModelProvider.Factory that knows how to create ViewModel s with default (no-arg) constructors, but it can’t possibly know how to create our custom view model.
ViewModelProvider.Factory
You will certainly have noticed the following:
Exit fullscreen mode
This is an overload on the ViewModelProviders.of() method that takes a custom factory. What does one of those look like? Here’s one way to do it:
Exit fullscreen mode
We have defined two new classes here. Let’s talk about the view model factory, first.
ViewModelProvider.Factory, defined
This is an interface that declares a single method, create() . It takes a a reference to the class it must create, and returns an instance of that class. If we strip it down, this is what we have:
Exit fullscreen mode
and this is essentially what the default factory does.
The other class, MutableObjectFactory , is trivial. I’ve declared it to showcase an important point.
Why do we care about ViewModel s at all?
We care about ViewModel s because they
- Live outside the Activity/Fragment lifecycle
- Know about the Activity/Fragment lifecycle.
This is amazingly useful. Because view models live outside of the lifecycle, they don’t get destroyed every time your activity gets destroyed (haha, rotation, go away). But because they also know about the lifecycle, you can start observing a LiveData object in Activity.onCreate() , and you will only receive updates on that object while the the activity is in the «started» state (between onStart and onStop ).
In the context of Dagger, this is a bit like having a custom @Scope , without having to declare or manage one. We create our objects in Activity.onCreate() , mutate those objects, rotate the screen, get references to those same objects again, and keep on going, no problem at all.
There is a small gotcha, though. Here’s that activity code, one more time:
Exit fullscreen mode
Every time we rotate our device, the activity goes through onDestroy / onCreate . Therefore, every time we rotate our device, we inject a new view model factory! However, even though the factory is new, the call to ViewModelProviders.of(this, factory).get(MyViewModel::class.java) will return the same view model! The Android Architecture Component framework maintains a separate map of class-to-view model that is independent from Dagger. So, each time you try to get a reference to a view model that already exists, it returns the extant instance.
Heavy objects and MutableObjectFactory
This brings us back to MutableObjectFactory . If, instead of providing that factory to our view model factory, we provided an instance of MutableObject itself, we’d be instantiating a new MutableObject each time we instantiated a view model factory which, incidentally, is every time we inject a view model factory in our activity — and that’s every time we rotate. If MutableObject is very heavy, then we’d like to avoid creating new instances that will never get used. So, we provide a factory instead. Illustrating in code:
Exit fullscreen mode
You may be thinking (like me) «ew, gross. Creating a factory is better than creating the heavy object itself, but can I avoid even creating that factory?» Well. yes, but. You can declare and manage a custom Dagger @Scope to handle that. You can use static instances, somehow. You can get really clever with programming techniques. Or you can profile this code and decide for yourself if it’s worth the effort. (A future post will discuss custom scopes.)
Is that it for view models? Not quite. We still have yet to discuss them in the context of fragments, and believe me, there are some interesting gotchas there. But. until next time.
Источник