Контекст
Итак, я работал с архитектурой MVVM только для нескольких проектов. Я все еще пытаюсь понять и улучшить то, как работает архитектура. Я всегда работал с архитектурой MVP, используя обычный набор инструментов, Dagger для DI, обычно многомодульные проекты, слой Presenter внедрялся с кучей Interactors/UseCases, и каждый Interactor внедрялся с разными репозиториями для выполнения внутренних вызовов 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 внедряется множество вариантов использования, он реализует интерфейсы обратного вызова вариантов использования, и когда я получаю результаты от вариантов использования, я передаю их на уровень пользовательского интерфейса через 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)
}
)
}
}
Классы моего репозитория обычно являются оболочками для экземпляра Retrofit, и они заботятся о настройке правильного планировщика, чтобы все выполнялось в правильном потоке, и сопоставлении ответов бэкэнда с классами модели. Под ответами бэкенда я подразумеваю классы, сопоставленные с Gson (например, список ApiProductResponse), и они сопоставляются с классами моделей (например, список продуктов, который я использую в приложении)
Вопрос
Мой вопрос заключается в том, что с тех пор, как я начал работать с архитектурой MVVM, все статьи и все примеры, люди либо внедряют репозитории прямо в ViewModel (дублируя код для обработки ошибок и сопоставления ответов), либо используют единый источник правды. паттерн (получение информации из Room с помощью Room's Flowables). Но я не видел, чтобы кто-нибудь использовал варианты использования со слоем ViewModel. Я имею в виду, что это очень удобно, я могу держать вещи разделенными, я делаю сопоставление ответов бэкэнда в UseCases, я обрабатываю там любую ошибку. Но все же есть вероятность, что я не вижу, чтобы кто-то этим занимался, Есть ли способ улучшить UseCases, чтобы сделать их более удобными для ViewModels с точки зрения API? Выполнять связь между UseCases и ViewModels с помощью чего-то еще, кроме интерфейса обратного вызова?
Пожалуйста, дайте мне знать, если вам нужна дополнительная информация об этом. Извините за примеры, я знаю, что они не самые лучшие, я просто придумал что-то простое, чтобы лучше объяснить.
Спасибо,
Изменить №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
Нашли ли вы причину/случай не использовать UseCase/Interactor? Я начинаю с подхода MVVM и не уверен, что они не нужны.
@Abbas Нет, на самом деле я использовал варианты использования в своем последнем проекте с MVVM. Единственное отличие состоит в том, что вместо использования обратных вызовов для передачи вариантов использования в ViewModel я использую RxJava.
@ 4gus71 Вы имеете в виду использование RxJava с наблюдаемыми? Итак, во ViewModel мы подписываемся на наблюдаемую переменную, передаем ее методу Interactor и обновляем значение во ViewModel через обновление наблюдаемой переменной в Interactor. Поправьте меня, если я не прав.
@IgorLevkivskiy На самом деле в моем Interactor у меня есть только функции, возвращающие Observable. В ViewModel я вызываю эти функции, подписываюсь на наблюдаемое и делаю все, что мне нужно. Например, Interactor для получения списка заказов будет иметь функцию: fun fetchOrders(): Observable<List<Order>> {... Затем на ViewModel я делаю: fetchOrdersInteractor.fetchOrders().subscribe {....} Что-то вроде этого.
Я только начал использовать MVVM для последних двух моих проектов. Я могу поделиться с вами своим процессом работы с REST API в ViewModel. Надеюсь, это поможет вам и другим.
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)
}
})
}
}
} class LoginViewModel : ViewModel() {
var userRepo = UserRepository()
fun getUserById(id :Int){
var diposable = userRepo.getUserbyId(id).subscribe({
//OnNext
},{
//onError
})
}
}Я надеюсь, что этот подход поможет вам сократить часть вашего шаблонного кода. Спасибо
Эй, спасибо, что оставили свой отзыв. Я не думаю, что я так сильно борюсь с вызовами API. Я имею в виду, что вы можете избавиться от некоторого шаблонного кода, просто используя RxAdapter Retrofit. Не нужно самостоятельно оборачивать ответы в синглы. Добавил немного информации в пост.
Кроме того, мне немного любопытно, как вы передаете свои результаты на уровень пользовательского интерфейса через LiveData. Не могли бы вы добавить краткий пример этого?
У меня был тот же вопрос, когда я начал использовать MVVM некоторое время назад. Я придумал следующее решение, основанное на функциях приостановки Kotlin и сопрограммах:
// error handling omitted for brevity
override fun fetchProducts() = retrofitApi.fetchProducts().execute().body()
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()
}
launch {
state.value = ProductDetailViewState.ProductListFetched(getProductsUseCase.execute())
}
См. https://github.com/snellen/umvvm для получения дополнительной информации и примеров.
Обновите свой вариант использования, чтобы он возвращал 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>, но с этим можно справиться, сопоставив список с помощью вспомогательной функции.
Хороший!!! Я не знал, что вы можете переопределить invoke на таком уроке, он выглядит очень чистым! ?
Вы нашли правильное решение?