Skip to content

ruslansharipov/MVVMsandbox

Repository files navigation

MVVMsandbox

ПРоект-исследование Jetpack библиотек и возможности перехода от Surf Android Standard на них с минимальными доработками.

Структура

Пока что проект одномодульный, с разделением фич по пакетам в модуле app.

Архитектура

Используется привычный для Surf подход с Clean Architecture.

DI с мультибайндингом

используется Dagger2, а точнее мультибайндинг. В различных источниках привозятся примеры как сделать инжект непосредственно вьюмодели во фрагмент, с использованием Subcomponent, что в нашем случае не подходит, так как сильно ограничивает возможности переиспользования экранов - они могут переиспользоваться не только в рамках одной активити или другого фрагмент-контейнера. В случае использования Subcomponent'ов их родительские компоненты будут знать о дочерних компонентах и предоставлять билдеры для создания дочерних компонентов.

Как это работает в нашем случае?

  1. У класса вьюмодели проставляется @Inject constructor и dagger генерирует для него Provider
  2. Этот провайдер кладется в генерируемый даггером Map<Class, Provider>, который инжектится в фабрику, создающую вьюмодели (DaggerViewModelFactory.kt)

На примере экрана каталога это происходит в конфигураторе CategoriesScreenConfigurator в абстрактном модуле:

    @Module
    internal abstract class CategoriesViewModelModule {

        @Binds
        @IntoMap
        @ViewModelKey(CategoriesViewModel::class)
        abstract fun bindsFavoritesViewModel(viewModel: CategoriesViewModel): ViewModel
    }
  1. Дальше мы можем инжектить эту фабрику во фрагмент или активити и доставать вьюмодель из провайдера уже во вью, а можем запровайдить через даггер ViewModelStore и извлекать вьюмодель из него уже в методах, которые провайдят вьюмодель. Но тут мы сталкиваемся с тем, что даггер нам уже сгенерировал Provider и мы не можем провайдить CategoriesViewModel из модуля экрана, так как получится циклическая зависимость (даггер генерирует провайдер, который кладется в фабрику, а фабрика используется для того, чтобы получить вьюмодель с этом провайдере). Поэтому в нашем случае инжектится не сама вьюмодель, а ее интерфейс.
        @Provides
        internal fun provideViewModel(
            viewModelStore: ViewModelStore,
            factory: DaggerViewModelFactory,
            route: CategoriesRoute
        ): ICategoriesViewModel { // Вот тут нельзя запровайдить CategoriesViewModel из-за возникновения цикла
            return ViewModelProvider(viewModelStore, factory).get(
                route.getId(),
                CategoriesViewModel::class.java
            )
        }
  1. Так как у нас есть возможность переопределять метод Route.getId() то мы можем получать разные вьюмодели для разных экранов

Минусы:

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

Плюсы:

  • у нас всего одна фабрика, которая может быть использована во всем приложении, у нее нет скоупа, для каждого экрана она создается отдельная.
  • вьюмодель не надо доставать из хранилища во вью, она достается в модуле экрана
  • так как мы используем интерфейс - нам не нужно создавать дублирующие проперти для LiveData полей, чтобы подписываться на них из вью (например val users: LiveData<List> и private val _users: MutableLiveData<List>), мы просто в интерфейсе их описываем как LiveData, а в реализации используем MutableLiveData. Так что если сравнивать с гайдами, то получается даже круче

Конфигураторы можно генерировать как обычно мы генерируем сущности для экранов, так что их код писать не придется.

DI без мультибайндинга

Так как мы инжектим интерфейсы - нам не нужно байндить вьюмодели в Map даггеру и инжектить этот Map в супер фабрику, достаточно создать фабрику, которая принимает в себя провайдер вьюмодели, сгенерированный даггером и передать эту фабрику в методе, который провайдит вьюмодель в компоненте экрана.

  1. то есть у нас в проекте появляется универсальная фабрика
class ProviderViewModelFactory<T: ViewModel>(
    private val provider: Provider<T>
): ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return provider.get() as T
    }
}
  1. проставляем у нашей вьюмодели @Inject constructor, чтобы даггер сгенерировал для нее Provider

  2. и мы эту фабрику передаем в провайдер вьюмоделей.

        @Provides
        fun provideViewModel(
            viewModelStore: ViewModelStore,
            provider: Provider<CategoriesViewModel>,
            route: CategoriesRoute
        ): ICategoriesViewModel {
            return ViewModelProvider(viewModelStore, ProviderViewModelFactory(provider)).get(
                route.getId(),
                CategoriesViewModel::class.java
            )
        }

и не нужно мультибайндингов

получаем все те же плюсы, что у DI с мультибайндингами, только без переусложненности (одним очень большим минусом меньше)

Фичи

Экран каталога по аналогии с экраном каталога из РИВ ГОШ.

При открытии экрана если в роут не были переданы подкатегории стартует запрос категорий (для наглядности используется задержка). Вью умеет отрисовывать три состояния этого запроса - загрузка, ошибка и успешное состояние.

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

При повороте экрана вьюмодели не пересоздаются - можно посмотреть логи.

Видео работы экрана

https://drive.google.com/file/d/11WPFWqCXuO3ryLasOA9jcBHKZhJnf-Gq/view?usp=sharing

Экран продуктов с пагинацией и добавлением/удалением из избранного

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

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

Видео работы экрана

  1. пагинация и обработка ошибок https://drive.google.com/file/d/1vhn3qVU53-Jvm26_wkhnRzr656s1EuV2/view?usp=sharing

  2. добавление в избранное и снэки https://drive.google.com/file/d/1uRfpjgwkPOB7hLhrzWeeTj86yjkLvpJG/view?usp=sharing

Запросы

Используется студийный мокер https://r1.mocker.surfstudio.ru/files/mvvm_android_research/

для работы с флоу добавлен экстеншен, который сразу при начале запроса эмитит Request.Loading, а как только запрос завершится - эмитит успех или ошибку в зависимости от того как завершился запрос.

fun <T> requestFlow(requestFunc: suspend () -> T): Flow<Request<T>> {
    return flow<Request<T>> {
        emit(Request.Loading())
        try {
            emit(Request.Success(requestFunc()))
        } catch (e: Exception) {
            emit(Request.Error(e))
        }
    }
}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published