MVVM architecture with Interactors/UseCases - android

Context
So, I've been working with the MVVM architecture just for a couple of projects. I'm still trying to figure out and improve how the architecture works. I always worked with the MVP architecture, using the usual toolset, Dagger for DI, usually multi-module projects, the Presenter layer being injected with a bunch of Interactors/UseCases, and each Interactor being injected with different Repositories to perform the backend API calls.
Now that I've moved into MVVM I changed the Presenter layer by the ViewModel, the communication from the ViewModel to the UI layer is being done through LiveData instead of using a View callback interface, and so on.
Looks like this:
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)
}
}
There's no rocket science here, sometimes I change the implementation to use more than one LiveData property to keep track of the changes. By the way, this is just an example that I wrote on the fly, so don't expect it to compile. But It's just this, the ViewModel is injected with a bunch of UseCases, it implements the UseCases callback interfaces and when I get the results from the UseCases I communicate that to the UI layer through LiveData.
My UseCases usually look like this:
// 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 classes are usually wrappers for the Retrofit instance and they take care of setting the proper Scheduler so everything runs on the proper thread and mapping the backend responses into model classes. By backend responses I mean classes mapped with Gson (for example
a list of ApiProductResponse) and they get mapped into model classes (for example a List of Product which I use across the App)
Question
My question here is that since I started working with the MVVM architecture all the articles and all the examples, people is either injecting the Repositories right into the ViewModel (duplicating code to handle errors and mapping the responses) or either using the Single Source of Truth pattern (getting the information from Room using Room's Flowables). But I haven't seen anyone use UseCases with a ViewModel layer. I mean it's pretty handy, I get to keep things separated, I do the mapping of the backend responses within the UseCases, I handle any error there. But still, feels odds that I don't see anyone doing this, is there some way to improve the UseCases to make them more friendly to the ViewModels in terms of API? Perform the communication between the UseCases and the ViewModels with something else than a callback interface?
Please let me know if you need any more info about this. Sorry for the examples, I know that these are not the best, I just came out with something simple for sake of explaining it better.
Thanks,
Edit #1
This is how my Repository classes look like:
// 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

Update your use case so that it returns Single<List<Product>>:
class GetProducts #Inject constructor(private val repository: ApiProductRepository) {
operator fun invoke(): Single<List<Product>> {
return repository.fetchProducts()
}
}
Then, update your ViewModel so that it subscribes to the products stream:
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()
}
One thing I'm leaving out it is the adaptation of List<ApiProductResponse>> to List<Product> but that can be handled by mapping the list with a helper function.

I have just started using MVVM for the last 2 of my projects. I can share with you my process of dealing with REST APIs in ViewModel. Hope it will help you and others.
Make a Generic Retrofit Executer Class with their callbacks. which will take a retrofit call object and gives you data.
Make a repository for Your particular package or module where you can handle all API request. in my case, I am getting one user by its id from API.
Here is User Repository.
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)
}
})
}
}
}
Then Use this Repository in your ViewModel. In my case here is my LoginViewModel code.
class LoginViewModel : ViewModel() {
var userRepo = UserRepository()
fun getUserById(id :Int){
var diposable = userRepo.getUserbyId(id).subscribe({
//OnNext
},{
//onError
})
}
}
I hope this approach can help you to reduce some of your boilerplate code.
Thanks

I had the same question when I started using MVVM a while ago. I came up with the following solution, based on Kotlin suspend functions and coroutines:
Change ApiProductRepositoryImpl.fetchProducts() to run synchronously. To do this, change your retrofit interface to return Call<...> and then change the repository implementation to
// error handling omitted for brevity
override fun fetchProducts() = retrofitApi.fetchProducts().execute().body()
Make your use cases implement the following interface:
interface UseCase<InputType, OutputType> {
suspend fun execute(input: InputType): OutputType
}
so your GetProductsUseCase would look like this:
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()
}
Execute the use case in your ViewModel
launch {
state.value = ProductDetailViewState.ProductListFetched(getProductsUseCase.execute())
}
See https://github.com/snellen/umvvm for more info and examples.

Related

Passing errors coming from the API call

I am using 2 separate liveData exposed to show the error coming from the API. I am basically checking if there is an exception with the API call, pass a failure status and serverErrorLiveData will be observed.
So I have serverErrorLiveData for error and creditReportLiveData for result without an error.
I think I am not doing this the right way. Could you please guide me on what is the right way of catching error from the API call. Also, any concerns/recommendation on passing data from repository on to view model.
What is the right way of handing loading state?
CreditScoreFragment
private fun initViewModel() {
viewModel.getCreditReportObserver().observe(viewLifecycleOwner, Observer<CreditReport> {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
})
viewModel.getServerErrorLiveDataObserver().observe(viewLifecycleOwner, Observer<Boolean> {
if (it) {
showScoreUI(false)
showToastMessage()
}
})
viewModel.getCreditReport()
}
MainActivityViewModel
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
var creditReportLiveData: MutableLiveData<CreditReport>
var serverErrorLiveData: MutableLiveData<Boolean>
init {
creditReportLiveData = MutableLiveData()
serverErrorLiveData = MutableLiveData()
}
fun getCreditReportObserver(): MutableLiveData<CreditReport> {
return creditReportLiveData
}
fun getServerErrorLiveDataObserver(): MutableLiveData<Boolean> {
return serverErrorLiveData
}
fun getCreditReport() {
viewModelScope.launch(Dispatchers.IO) {
val response = dataRepository.getCreditReport()
when(response.status) {
CreditReportResponse.Status.SUCCESS -> creditReportLiveData.postValue(response.creditReport)
CreditReportResponse.Status.FAILURE -> serverErrorLiveData.postValue(true)
}
}
}
}
DataRepository
class DataRepository #Inject constructor(
private val apiServiceInterface: ApiServiceInterface
) {
suspend fun getCreditReport(): CreditReportResponse {
return try {
val creditReport = apiServiceInterface.getDataFromApi()
CreditReportResponse(creditReport, CreditReportResponse.Status.SUCCESS)
} catch (e: Exception) {
CreditReportResponse(null, CreditReportResponse.Status.FAILURE)
}
}
}
ApiServiceInterface
interface ApiServiceInterface {
#GET("endpoint.json")
suspend fun getDataFromApi(): CreditReport
}
CreditScoreResponse
data class CreditReportResponse constructor(val creditReport: CreditReport?, val status: Status) {
enum class Status {
SUCCESS, FAILURE
}
}
It's creates complexity and increased chances for a coding error to have two LiveData channels for success and failure. You should have a single LiveData that can offer up the data or an error so you know it's coming in orderly and you can observe it in one place. Then if you add a retry policy, for example, you won't risk somehow showing an error after a valid value comes in. Kotlin can facilitate this in a type-safe way using a sealed class. But you're already using a wrapper class for success and failure. I think you can go to the source and simplify it. You can even just use Kotlin's own Result class.
(Side note, your getCreditReportObserver() and getServerErrorLiveDataObserver() functions are entirely redundant because they simply return the same thing as a property. You don't need getter functions in Kotlin because properties basically are getter functions, with the exception of suspend getter functions because Kotlin doesn't support suspend properties.)
So, to do this, eliminate your CreditReportResponse class. Change your repo function to:
suspend fun getCreditReport(): Result<CreditReport> = runCatching {
apiServiceInterface.getDataFromApi()
}
If you must use LiveData (I think it's simpler not to for a single retrieved value, see below), your ViewModel can look like:
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
val _creditReportLiveData = MutableLiveData<Result<CreditReport>>()
val creditReportLiveData: LiveData<Result<CreditReport>> = _creditReportLiveData
fun fetchCreditReport() { // I changed the name because "get" implies a return value
// but personally I would change this to an init block so it just starts automatically
// without the Fragment having to manually call it.
viewModelScope.launch { // no need to specify dispatcher to call suspend function
_creditReportLiveData.value = dataRepository.getCreditReport()
}
}
}
Then in your fragment:
private fun initViewModel() {
viewModel.creditReportLiveData.observe(viewLifecycleOwner) { result ->
result.onSuccess {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
}.onFailure {
showScoreUI(false)
showToastMessage()
}
viewModel.fetchCreditReport()
}
Edit: the below would simplify your current code, but closes you off from being able to easily add a retry policy on failure. It might make better sense to keep the LiveData.
Since you are only retrieving a single value, it would be more concise to expose a suspend function instead of LiveData. You can privately use a Deferred so the fetch doesn't have to be repeated if the screen rotates (the result will still arrive and be cached in the ViewModel). So I would do:
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
private creditReportDeferred = viewModelScope.async { dataRepository.getCreditReport() }
suspend fun getCreditReport() = creditReportDeferred.await()
}
// In fragment:
private fun initViewModel() = lifecycleScope.launch {
viewModel.getCreditReport()
.onSuccess {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
}.onFailure {
showScoreUI(false)
showToastMessage()
}
}

Kotlin Flow: callbackFlow with lazy initializer of callback object

I want to use reactive paradigm using Kotlin Flow in my Android project. I have an external callback-based API so my choice is using callbackFlow in my Repository class.
I've already read insightfully some proper docs with no help:
callbackFlow documentation
Callbacks and Kotlin Flows by Roman Elizarov
What I want to achieve:
Currently my Repository class looks like this (simplified code):
lateinit var callback: ApiCallback
fun someFlow() = callbackFlow<SomeModel> {
callback = object : ApiCallback {
override fun someApiMethod() {
offer(SomeModel())
}
}
awaitClose { Log.d("Suspending flow until methods aren't invoked") }
}
suspend fun someUnfortunateCallbackDependentCall() {
externalApiClient.externalMethod(callback)
}
Problem occurs when someUnfortunateCallbackDependentCall is invoked faster than collecting someFlow().
For now to avoid UninitializedPropertyAccessException I added some delays in my coroutines before invoking someUnfortunateCallbackDependentCall but it is kind of hack/code smell for me.
My first idea was to use by lazy instead of lateinit var as this is what I want - lazy initialization of callback object. However, I couldn't manage to code it altogether. I want to emit/offer/send some data from someApiMethod to make a data flow but going outside of callbackFlow would require ProducerScope that is in it. And on the other hand, someUnfortunateCallbackDependentCall is not Kotlin Flow-based at all (could be suspended using Coroutines API at best).
Is it possible to do? Maybe using some others Kotlin delegates? Any help would be appreciated.
To answer your question technically, you can of course intialise a callback lazyily or with lateinit, but you can't do this AND share the coroutine scope (one for the Flow and one for the suspend function) at the same time - you need to build some kind of synchronisation yourself.
Below I've made some assumptions about what you are trying to achieve, perhaps they are not perfect for you, but hopefully give some incite into how to improve.
Since it is a Repository that you are creating, I will first assume that you are looking to store SomeModel and allow the rest of your app to observe changes to it. If so, the easiest way to do this is with a MutableStateFlow property instead of a callbackFlow:
interface Repository {
val state: Flow<SomeModel>
suspend fun reload()
}
class RepositoryImpl(private val service: ApiService) : Repository {
override val state = MutableStateFlow(SomeModel())
override suspend fun reload() {
return suspendCoroutine { continuation ->
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
state.value = data
if (continuation.context.isActive)
continuation.resume(Unit)
}
})
}
}
}
interface ApiCallback {
fun someApiMethod(data: SomeModel)
}
data class SomeModel(val data: String = "")
interface ApiService {
fun callBackend(callback: ApiCallback)
}
The downside to this solution is that you have to call reload() in order to actually make a call to your backend, collecting the Flow alone is not enough.
myrepository.state.collect {}
myrepository.reload()
Another solution, again depending on what exactly you are trying to achieve, is to provide two ways to call your backend:
interface Repository {
fun someFlow(): Flow<SomeModel>
suspend fun reload(): SomeModel
}
class RepositoryImpl(private val service: ApiService) : Repository {
override fun someFlow() = callbackFlow<SomeModel> {
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
offer(data)
}
})
awaitClose {
Log.d("TAG", "Callback Flow is closed")
}
}
override suspend fun reload(): SomeModel {
return suspendCoroutine<SomeModel> { continuation ->
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
if (continuation.context.isActive)
continuation.resume(data)
}
})
}
}
}
interface ApiCallback {
fun someApiMethod(data: SomeModel)
}
data class SomeModel(val data: String = "")
interface ApiService {
fun callBackend(callback: ApiCallback)
}
Now you can either call reload() or someFlow() to retrieve SomeModel() and the Repository holds no "state".
Note that the reload() function is simply a 'coroutine' version of the callbackFlow idea.

How to observe the return value from a Repository class in a ViewModel?

I have an android application using an MVVM architecture. On a button click, I launch a coroutine that calls a ViewModel method to make a network request. In my ViewModel, I have a LiveData observable for the return of that request, but I'm not seeing it update. It seems that my repository method isn't being called and I'm not sure why.
UI Click Listener
searchButton.setOnClickListener{
CoroutineScope(IO).launch{
viewModel.getUser(username.toString())
}
}
ViewModel - Observables and invoked method
private var _user: MutableLiveData<User?> = MutableLiveData<User?>()
val user: LiveData <User?>
get() = _user
...
suspend fun getUser(userId:String) {
_user = liveData{
emit(repository.getUser(userId))
} as MutableLiveData<User?>
}
...
When I debug through, execution goes into the getUser method of the ViewModel but doesn't go into the liveData scope to update my _user MutableLiveData observable and I'm not sure what I'm doing wrong.
There is no need to use liveData coroutine builder because the getUser is a suspended function and you are already calling it in a coroutine. Just post the result simply on _user.
suspend fun getUser(userId: String) {
_user.postValue(repository.getUser(userId))
}
What you did on your code caused assigning a new instance of LiveData to _user, while the observer in the fragment is observing on previous LiveData which is instantiated by private var _user: MutableLiveData<User?> = MutableLiveData<User?>(). So, the update gets lost.
A better solution is to handle the creation of coroutines in your ViewModel class to keep track of them and prevent execution leak.
fun getUser(userId: String) {
viewModelScope.launch(IO) {
_user.postValue(repository.getUser(userId))
}
}
And in the fragment:
searchButton.setOnClickListener{
viewModel.getUser(username.toString())
}
It doesn't work because your "MVVM structure" is not following the MVVM recommendations, nor the structured concurrency guidelines provided for coroutines.
searchButton.setOnClickListener{
CoroutineScope(IO).launch{ // <-- should be using a controlled scope
viewModel.getUser(username.toString()) // <-- state belongs in the viewModel
}
}
Instead, it is supposed to look like this
searchButton.setOnClickListener {
viewModel.onSearchButtonClicked()
}
username.doAfterTextChanged {
viewModel.updateUsername(it)
}
And
class MyViewModel(
private val application: Application,
private val savedStateHandle: SavedStateHandle
): AndroidViewModel(application) {
private val repository = (application as CustomApplication).repository
private val username = savedStateHandle.getLiveData("username", "")
fun updateUsername(username: String) {
username.value = username
}
val user: LiveData<User?> = username.switchMap { userId ->
liveData(viewModelScope + Dispatchers.IO) {
emit(repository.getUser(userId))
}
}
}
Now you can do user.observe(viewLifecycleOwner) { user -> ... } and it should work. If you really do need to fetch only when the button is clicked, you might want to replace the liveData { with a regular suspend fun call, calling from viewModelScope.launch {, and save the value to a LiveData.

Repository Pattern in Android. Is there any way better than this?

I have an app based on MVVM architecture.
I have two modules that repository and datasource.
And using coroutines. I came across some projects on Github they applied different ways.
My implementation like this;
You can think naming as login, profile etc. instead of X letter
datasource interface
interface IXDatasource{
suspend fun fetchData(): LiveData<XResponse>
}
datasource implementation
class XDatasourceImp #Inject constructor(private val apiService: ApiService) : IXDatasource{
suspend fun fetchData(): LiveData<XResponse> {
// getting data
return xResponse
}
}
repository interface
interface XRepository {
suspend fun getXData(): LiveData<XResponse>
}
repository implementation
class XRepositoryImp #Inject constructor(private val iDatasource: IXDatasource): XRepository {
override suspend fun getXData(): LiveData<XResponse> {
return withContext(Dispatchers.IO) {
iDatasource.fetchData()
}
}
}
I called this in my ViewModel
class XViewModel #Inject constructor(xRepository: XRepository) : BaseViewModel() {
val xModel by lazyDeferred {
xRepository.getXData()
}
}
And I use in my activity/fragment
private fun init() = launch {
val xModel = viewModel.xModel.await()
xModel.observe(this#MyActivity, Observer {
if (it == null) {
return#Observer
}
// filling views or doing sth
})
}
It works but I wonder all of them is required or not? I can apply the room database to my datasource. Is there any way better than this? I know it can change by the case of the app. I try to find the best way. I will be happy if you suggest anything or share anything about repository pattern implementation.

Clean Architecture: ViewModel with multiple UseCases on Android

This is more of an Architecture question than a bug fixing one.
Let's assume this app lets users mark a Bus and/or Bus Stations as a favourite. My question is, should I have a ViewModel with both UseCases or should I build a UseCase that encapsulates the current logic?
Also for the question part, I'm not entirely sure the way I should expose the combined data to the UI layer (see favouritesExposedLiveData)
Thanks in advance any feedback is welcome, here's my ViewModel you can assume each UseCase is passing the correct data from the data source(s).
open class FavouritesViewModel #Inject internal constructor(
private val getFavouriteStationsUseCase: GetFavouriteStationsUseCase,
private val getFavouriteBusesUseCase: GetFavouriteBusesUseCase,
private val favouriteMapper: FavouriteMapper,
private val busMapper: BusMapper,
private val stationMapper: StationMapper) : ViewModel() {
private val favouriteBusesLiveData: MutableLiveData<Resource<List<BusView>>> = MutableLiveData()
private val favouriteStationsLiveData: MutableLiveData<Resource<List<StationView>>> = MutableLiveData()
private lateinit var favouritesMediatorLiveData: MediatorLiveData<List<FavouriteView>>
private lateinit var favouritesExposedLiveData: LiveData<Resource<List<FavouriteView>>>
init {
fetchFavourites()
}
override fun onCleared() {
getFavouriteStationsUseCase.dispose()
getFavouriteBusesUseCase.dispose()
super.onCleared()
}
fun getFavourites(): LiveData<Resource<List<FavouriteView>>> {
return favouritesExposedLiveData
}
private fun fetchFavourites() {
favouritesMediatorLiveData.addSource(favouriteStationsLiveData, { favouriteStationListResource ->
if (favouriteStationListResource?.status == ResourceState.SUCCESS) {
favouriteStationListResource.data?.map {
favouriteMapper.mapFromView(it)
}
}
})
favouritesMediatorLiveData.addSource(favouriteBusesLiveData, { favouriteBusesListResource ->
if (favouriteBusesListResource?.status == ResourceState.SUCCESS) {
favouriteBusesListResource.data?.map {
favouriteMapper.mapFromView(it)
}
}
})
getFavouriteStationsUseCase.execute(FavouriteStationsSubscriber())
getFavouriteBusesUseCase.execute(FavouriteBusesSubscriber())
}
inner class FavouriteStationsSubscriber : DisposableSubscriber<List<Station>>() {
override fun onComplete() {}
override fun onNext(t: List<Station>) {
favouriteStationsLiveData.postValue(Resource(ResourceState.SUCCESS, t.map { stationMapper.mapToView(it) }, null))
}
override fun onError(exception: Throwable) {
favouriteStationsLiveData.postValue(Resource(ResourceState.ERROR, null, exception.message))
}
}
inner class FavouriteBusesSubscriber : DisposableSubscriber<List<Bus>>() {
override fun onComplete() {}
override fun onNext(t: List<Bus>) {
favouriteBusesLiveData.postValue(Resource(ResourceState.SUCCESS, t.map { busMapper.mapToView(it) }, null))
}
override fun onError(exception: Throwable) {
favouriteBusesLiveData.postValue(Resource(ResourceState.ERROR, null, exception.message))
}
}
}
Note: Currently the MediatorLiveData (favouritesMediatorLiveData)is not binding the data back to the favouritesExposedLiveData since at this time, I'm not sure this is the correct way to go ;).
Ideally a ViewModel would only have the view state for its view. By using the MediatorLiveData you could aggregate all sources of state into one that represents the view state over time.
What you can have is a data class that represents your ViewState that you construct on your view model and is your exposed LiveData
data class FavouritesViewState(val favoriteStations: List<Station>, val favoritBuses: List<Bus>)
However you know depend on the ViewModel to construct the final ViewState which kinda breaks the single responsibility principle and also makes you dependent of an Android framework.
I would approach it using a composite UseCase that had both station and bus use cases and returns the composed data that you can then easily expose from the ViewModel.
The whole point of a ViewModel is that it is a model of what the view is using. It should be as close to that as possible.. Unless you are presenting stations and buses in the same view list (seems ugly), otherwise, they are separate views, and should get separate models.

Categories

Resources