Dagger2 и архитектурный компонент «ViewModel»

в 18:32, , рубрики: android, android development, dagger 2, dependency injection, multibindings, Разработка под android

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. Как вы могли заметить архитектурные компоненты на момент написания статьи имеют альфа версию. Возможно в будущем появятся и другие методы получения вью модели.

Автор: txdrive

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js