Архитектура MVVM с взаимодействующими элементами / сценариями использования

Контекст

Итак, я работал с архитектурой MVVM всего в паре проектов. Я все еще пытаюсь понять и улучшить, как работает архитектура. Я всегда работал с архитектурой MVP, используя обычный набор инструментов, Dagger для DI, обычно многомодульные проекты, уровень Presenter вводился с кучей Interactors / UseCases, и каждый Interactor вводился с разными репозиториями для выполнения вызовов backend API .

Теперь, когда я перешел в MVVM, я изменил уровень Presenter с помощью ViewModel, связь от ViewModel со слоем пользовательского интерфейса осуществляется через LiveData вместо использования интерфейса обратного вызова View и так далее.

Выглядит так:

class ProductDetailViewModel @inject constructor(
    private val getProductsUseCase: GetProductsUseCase,
    private val getUserInfoUseCase: GetUserInfoUseCase,
) : ViewModel(), GetProductsUseCase.Callback, GetUserInfoUseCase.Callback {
    // Sealed class used to represent the state of the ViewModel
    sealed class ProductDetailViewState {
        data class UserInfoFetched(
            val userInfo: UserInfo
        ) : ProductDetailViewState(),
        data class ProductListFetched(
            val products: List<Product>
        ) : ProductDetailViewState(),
        object ErrorFetchingInfo : ProductDetailViewState()
        object LoadingInfo : ProductDetailViewState()
    }
    ...
    // Live data to communicate back with the UI layer
    val state = MutableLiveData<ProductDetailViewState>()
    ...
    // region Implementation of the UseCases callbacks
    override fun onSuccessfullyFetchedProducts(products: List<Product>) {
        state.value = ProductDetailViewState.ProductListFetched(products)
    }

    override fun onErrorFetchingProducts(e: Exception) {
        state.value = ProductDetailViewState.ErrorFetchingInfo
    }

    override fun onSuccessfullyFetchedUserInfo(userInfo: UserInfo) {
        state.value = ProductDetailViewState.UserInfoFetched(userInfo)
    }

    override fun onErrorFetchingUserInfo(e: Exception) {
        state.value = ProductDetailViewState.ErrorFetchingInfo
    }

    // Functions to call the UseCases from the UI layer
    fun fetchUserProductInfo() {
        state.value = ProductDetailViewState.LoadingInfo
        getProductsUseCase.execute(this)
        getUserInfoUseCase.execute(this)
    }
}

Здесь нет ракетостроения, иногда я меняю реализацию, чтобы использовать более одного свойства LiveData для отслеживания изменений. Кстати, это всего лишь пример, который я написал на лету, поэтому не ждите, что он скомпилируется. Но просто ViewModel вводится с кучей UseCases, он реализует интерфейсы обратного вызова UseCases, и когда я получаю результаты от UseCases, я передаю их на уровень пользовательского интерфейса через LiveData.

Мои варианты использования обычно выглядят так:

// UseCase interface
interface GetProductsUseCase {
    interface Callback {
        fun onSuccessfullyFetchedProducts(products: List<Product>)
        fun onErrorFetchingProducts(e: Exception)
    }
    fun execute(callback: Callback) 
}

// Actual implementation
class GetProductsUseCaseImpl(
    private val productRepository: ApiProductRepostory
) : GetProductsUseCase {
    override fun execute(callback: Callback) {
        productRepository.fetchProducts() // Fetches the products from the backend through Retrofit
            .subscribe(
                {
                    // onNext()
                    callback.onSuccessfullyFetchedProducts(it)
                },
                {
                    // onError()
                    callback.onErrorFetchingProducts(it)
                }
            )
    }
}

Классы My Repository обычно являются оболочками для экземпляра Retrofit, и они заботятся о настройке правильного планировщика, чтобы все работало в правильном потоке, и отображении ответов серверной части в классы модели. Под ответами серверной части я подразумеваю классы, сопоставленные с Gson (например, список ApiProductResponse), и они сопоставляются с классами модели (например, Список продуктов, который я использую в приложении)

Вопрос

Мой вопрос здесь в том, что с тех пор, как я начал работать с архитектурой MVVM, все статьи и все примеры, люди либо вводят репозитории прямо в ViewModel (дублируя код для обработки ошибок и сопоставления ответов), либо используют единый источник истины. шаблон (получение информации из комнаты с помощью Flowables комнаты). Но я не видел, чтобы кто-нибудь использовал варианты использования со слоем ViewModel. Я имею в виду, что это довольно удобно, я могу держать вещи разделенными, я занимаюсь отображением ответов серверной части в рамках UseCases, я обрабатываю любые ошибки там. Но все же есть вероятность, что я не вижу, чтобы кто-то этим занимался, есть ли способ улучшить UseCases, чтобы сделать их более удобными для ViewModels с точки зрения API? Осуществлять связь между вариантами использования и моделями просмотра с помощью чего-то еще, кроме интерфейса обратного вызова?

Пожалуйста, дайте мне знать, если вам понадобится дополнительная информация об этом. Извините за примеры, я знаю, что они не самые лучшие, я просто придумал что-то простое, чтобы лучше это объяснить.

Спасибо,

Изменить №1

Вот как выглядят мои классы репозитория:

// ApiProductRepository interface
interface ApiProductRepository {
    fun fetchProducts(): Single<NetworkResponse<List<ApiProductResponse>>>
}

// Actual implementation
class ApiProductRepositoryImpl(
    private val retrofitApi: ApiProducts, // This is a Retrofit API interface
    private val uiScheduler: Scheduler, // AndroidSchedulers.mainThread()
    private val backgroundScheduler: Scheduler, // Schedulers.io()
) : GetProductsUseCase {
    override fun fetchProducts(): Single<NetworkResponse<List<ApiProductResponse>>> {
        return retrofitApi.fetchProducts() // Does the API call using the Retrofit interface. I've the RxAdapter set.
            .wrapOnNetworkResponse() // Extended function that converts the Retrofit's Response object into a NetworkResponse class
            .observeOn(uiScheduler)
            .subscribeOn(backgroundScheduler)
    }
}

// The network response class is a class that just carries the Retrofit's Response class status code

person 4gus71n    schedule 30.03.2019    source источник
comment
Вы нашли правильное решение?   -  person Viktor Vostrikov    schedule 29.05.2019
comment
Вы нашли причину / случай не использовать UseCase / Interactor? Я начинаю с подхода MVVM и не уверен, что это ненужный слой.   -  person Abbas    schedule 17.09.2019
comment
@Abbas Нет, на самом деле я использовал варианты использования в своем последнем проекте с MVVM. Единственное отличие состоит в том, что вместо использования обратных вызовов для передачи вариантов использования ViewModel я использую RxJava.   -  person 4gus71n    schedule 17.09.2019
comment
@ 4gus71 Вы имеете в виду использование RxJava с наблюдениями? Итак, в ViewModel мы подписываемся на наблюдаемую переменную, передаем ее методу Interactor и обновляем значение в ViewModel посредством обновления наблюдаемой переменной в Interactor. Поправьте меня, если я не прав.   -  person Igor Levkivskiy    schedule 04.10.2019
comment
@IgorLevkivskiy На самом деле в моем Interactor у меня есть только функции, возвращающие Observable. В ViewModel я вызываю эти функции, подписываюсь на наблюдаемое и делаю все, что мне нужно. Например, Interactor для получения списка заказов будет иметь функцию: fun fetchOrders(): Observable<List<Order>> {... Затем в ViewModel я выполняю: fetchOrdersInteractor.fetchOrders().subscribe {....} Что-то вроде этого.   -  person 4gus71n    schedule 07.10.2019


Ответы (3)


Обновите свой вариант использования, чтобы он возвращал Single<List<Product>>:

class GetProducts @Inject constructor(private val repository: ApiProductRepository) {
    operator fun invoke(): Single<List<Product>> {
        return repository.fetchProducts()
    }
}

Затем обновите свой ViewModel, чтобы он подписался на поток продуктов:

class ProductDetailViewModel @Inject constructor(
    private val getProducts: GetProducts
): ViewModel() {

    val state: LiveData<ProductDetailViewState> get() = _state
    private val _state = MutableLiveData<ProductDetailViewState>()

    private val compositeDisposable = CompositeDisposable()

    init {
        subscribeToProducts()
    }

    override fun onCleared() {
        super.onCleared()
        compositeDisposable.clear()
    }

    private fun subscribeToProducts() {
        getProducts()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.main())
            .subscribe(
                {
                    // onNext()
                    _state.value = ProductListFetched(products = it)
                },
                {
                    // onError()
                    _state.value = ErrorFetchingInfo
                }
            ).addTo(compositeDisposable)
    }

}

sealed class ProductDetailViewState {
    data class ProductListFetched(
        val products: List<Product>
    ): ProductDetailViewState()
    object ErrorFetchingInfo : ProductDetailViewState()
}

Одна вещь, которую я опускаю, - это адаптация List<ApiProductResponse>> к List<Product>, но с этим можно справиться, сопоставив список с помощью вспомогательной функции.

person Ivan    schedule 11.05.2021
comment
Отлично!!! Я не знал, что вы можете переопределить invoke в таком классе, это выглядит супер чисто! ???? - person 4gus71n; 01.07.2021

Я только начал использовать MVVM для последних двух своих проектов. Я могу поделиться с вами своим процессом работы с REST API в ViewModel. Надеюсь, это поможет вам и другим.

  • Создайте класс Generic Retrofit Executer с их обратными вызовами. который примет объект вызова модернизации и предоставит вам данные.
  • Создайте репозиторий для вашего конкретного пакета или модуля, где вы можете обрабатывать все запросы API. в моем случае я получаю одного пользователя по его идентификатору из API. Вот репозиторий пользователей.

class UserRepository {


    @Inject
    lateinit var mRetrofit: Retrofit

    init {
        MainApplication.appComponent!!.inject(this)
    }

    private val userApi = mRetrofit.create(UserApi::class.java)

    fun getUserbyId(id: Int): Single<NetworkResponse<User>> {
        return Single.create<NetworkResponse<User>>{
            emitter ->
            val callbyId = userApi.getUserbyId(id)
            GenericReqExecutor(callbyId).executeCallRequest(object : ExecutionListener<User>{
                override fun onSuccess(response: User) {
                    emitter.onSuccess(NetworkResponse(success = true,
                            response = response
                            ))
                }

                override fun onApiError(error: NetworkError) {
                    emitter.onSuccess(NetworkResponse(success = false,
                            response = User(),
                            networkError = error
                            ))
                }

                override fun onFailure(error: Throwable) {
                    emitter.onError(error)
                }

            })
        }
    }

}

  • Затем используйте этот репозиторий в своей модели просмотра. В моем случае это мой код LoginViewModel.

 class LoginViewModel : ViewModel()  {

     var userRepo = UserRepository()

     fun getUserById(id :Int){
         var diposable = userRepo.getUserbyId(id).subscribe({

             //OnNext

         },{
             //onError
         })
     }
}

Я надеюсь, что этот подход поможет вам сократить часть вашего шаблонного кода. Спасибо

person Happy Singh    schedule 30.03.2019
comment
Привет, спасибо, что оставили свой отзыв. Не думаю, что я так сильно борюсь с вызовами API. Я имею в виду, что вы можете избавиться от шаблонного кода, просто используя RxAdapter Retrofit. Не нужно самостоятельно упаковывать ответы в одиночные сообщения. Я добавил дополнительную информацию в пост. - person 4gus71n; 30.03.2019
comment
Кроме того, мне немного любопытно, как вы делаете, чтобы сообщить свои результаты слою пользовательского интерфейса через LiveData. Не могли бы вы привести краткий пример этого? - person 4gus71n; 30.03.2019

У меня был тот же вопрос, когда я некоторое время назад начал использовать MVVM. Я придумал следующее решение, основанное на функциях приостановки и сопрограммах Kotlin:

  1. Измените ApiProductRepositoryImpl.fetchProducts () на синхронный запуск. Для этого измените интерфейс модернизации так, чтобы он возвращал Call ‹...>, а затем измените реализацию репозитория на
// error handling omitted for brevity
override fun fetchProducts() = retrofitApi.fetchProducts().execute().body()
  1. Сделайте так, чтобы ваши варианты использования реализовали следующий интерфейс:
interface UseCase<InputType, OutputType> {
    suspend fun execute(input: InputType): OutputType
}

так что ваш GetProductsUseCase будет выглядеть так:

class GetProductsUseCase: UseCase<Unit, List<Product>> {
    suspend fun execute(input: Unit): List<Product> = withContext(Dispatchers.IO){
        // withContext causes this block to run on a background thread 
        return@withContext productRepository.fetchProducts() 
}
  1. Выполните вариант использования в вашей ViewModel
launch {
   state.value = ProductDetailViewState.ProductListFetched(getProductsUseCase.execute())
}

См. https://github.com/snellen/umvvm для получения дополнительной информации и примеров.

person Snellius    schedule 09.04.2020