DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 966,155 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Wiring Your Repository - ViewModel - UI in Android
Prieyudha Akadita S
Prieyudha Akadita S

Posted on

Wiring Your Repository - ViewModel - UI in Android

This article is a copy of my blogpost here

Some of us struggling how to wiring-up a repository pattern into viewmodel and then observe it from View. When I was learn about MVVM for the first time, I put all the logics inside ViewModel (I'm not using repository). It'is really painful to manage when the app get bigger and bigger.

It is easy, just put the logic inside repository, right?

Well, yes. But it's not that easy. I see a lot of android developer moving out their login into repository.

First approach that I saw is look like this:

class ProductRepository(private val api: ApiService){
    fun getAllProducts(token: String) : MutableLiveData {
         //logic   
    }
}

then the viewmodel and activity will look like this

class MyStoreViewModel(private val productRepo: ProductRepository) : ViewModel(){
    fun getAllProduct(token: String) = productRepo.getAllProducts(token)
}

//the view
class MyStoreActivity ... {
    private val myStoreViewModel....

    onCreate(){
        myStoreViewModel.getAllProducts(myToken).observe(this, Observer{....})
    }

}

Let's talk about this. In my opinion, this approach have some cons, for example: the viewmodel doesn't hold live data, it is just returning from the repository, and also if we have so many children (fragments...) that needs shared viewmodel from the parent, every fragments might have a different result

Then, just put the LiveData inside ViewModel, isn't it?

Okay, let's try...

class MyStoreViewModel(private val productRepo: ProductRepository) : ViewModel(){
    private val products = MutableLiveData<List<Product>>()

    fun getAllProducts(token: String){
        //will return a list
        val result = productRepo.getAllProducts(token)
        products.postValue(result)
    }

    //Activity/Fragment will observe this,
    //myStoreViewModel.listenToProducts().observe(this, Observer{...})
    fun listenToProducts() = products
}

then, in the repository...

class ProductRepository(private val api: ApiService){
    fun getAllProducts(token: String) : List<Product> {
        var temps = mutableListOf()
        api.all_products(token).enqeue()....{
            onFailure(){}
            onSuccess(){
                temps = response.body()
            }
        }
        return temps
    }
}

What's gonna happen? The function getAllProducts() will always return empty list! Because, we know, that api request is an async method so, the code will run to the bottom and then return immediately temps variable while the temps variable is still empty.

The First Savior: Callback!

We can use Kotlin Callback to solve this problem. Callback probably the fastest way to get this thing done.

class ProductRepository(private val api: ApiService) {
    //completion is a calbback
    //you dont need to fill it when call this method
    //we need to use this as return param instead

    fun getAllProducts(token:String, completion: (List<Product>?, Error?) -> Unit) {
        api.all_prod(token).enqeue....{
            onFailure(t: Throwable){
                completion(null, Error(t.message.toString))
            }
            onSuccess(response){
                completion(response.body, null)
            }
        }
    } 
}

You can modify List? and Error as you need, in this example I will get a list of products so I expect the return should be list of products or Error if failed to get data.

In your viewmodel will look like this

class MyStoreViewModel(private val productRepo: ProductRepository): ViewModel(){
    private val products = MutableLiveData<List<Product>()

    fun getAllProd(token: String){
        //use the param completion for handle the result
        productRepo(token){ listProduct, error ->
            error?.let{ it.message?.let{ message -> println(message) } }
            listProduct?.let{ it ->
                products.postValue(it)
            }
        }
    }

    fun listenToProducts() = products

}

Multiple Repository, Different Expected Result and Combine with UI State

What if we have different repository in one viewmodel? And how to combine it with UI State? For this example, I will use sealed class to manage UI State. If you don't know about it, you can always see my article about How To Manage UI State using Sealed Class in Android

We are going to create two repository, first is StoreRepository and the second is ProductRepository.

class StoreRepository(private val api: ApiService) {
    fun getStoreInfo(token: String, storeId: String, completion: (Store?, Error?) -> Unit){
        api.getMyStoreInfo(token, storeId).enqeue...{
            onFailure(t: Throwable){
                completion(null, Error(t.message.toString()))
            }

            onSuccess(){
                //we expect the return is store data
                completion(response.body, null)
            }
        }
    }
}

Here is the productRepository

class ProductRepository(private val api: ApiService) {
    fun getAllProducts(token: String, storeId: String, completion: (List<Product>?, Error?) -> Unit){
        api.all_prods(token, storeId).enqeue...{
            onFailure(t){
                completion(null, t.message.toString())
            }

            onSuccess(){
                //expect response.body is a List<Product>
                completion(response.body, null)
            }
        }
    }
}

And here is the viewmodel

class MyStoreViewModel(private val productRepo: ProductRepository, private val storeRepo: StoreRepository) : ViewModel(){
    private val state : SingleLiveEvent<MyStoreState> = SingleLiveEvent()
    private val store = MutableLiveData<Store>()
    private val products = MutableLiveData<List<Product>>()

    private fun setLoading(){
        state.value = MyStoreState.Loading(true)
    }

    private fun hideLoading(){
        state.value = MyStoreState.Loading(false)
    }

    private fun toast(message: String){
        state.value = MyStoreState.ShowToast(message)
    }

    fun getStoreInfo(token: String, storeId: String){
        //menampilkan loading atau progressbar
        setLoading()
        storeRepo.getStoreInfo(token, storeId){ resultStore, e ->
            //hilangkan progressbar
            hideLoading()
            e?.let{ it.message?.let { message -> toast(message) }}
            resultStore?.let{ it -> 
                store.postValue(it)
            }
        }
    }

    fun getAllProducts(token: String, storeId: String){
        //menampilkan loading atau progressbar
        setLoading()
        productRepo.getAllProducts(token, storeId){ resultProducts, e ->
            //hilangkan progressbar
            hideLoading()
            e?.let{ it.message?.let { message -> toast(message) }}
            resultProducts?.let{ it -> 
                products.postValue(it)
            }
        }
    }

    fun listenToUIState() = state
    fun listenToProducts() = products()
    fun listenToStore() = store

}

sealed class MyStoreState {
    data class Loading(var state : Boolean) : MyStoreState()
    data class ShowToast(var message: String): MyStoreState()
}

And here is the how we consume it in View

class MyStoreActivity : AppCompat....{
    //Im using Koin
    private val myStoreViewModel : MyStoreViewModel by viewModel()

    onCreate(){
        myStoreViewModel.listenToUIState().observer(this, Observe{ handleUI(it) })
        myStoreViewModel.listenToProducts().observe(this, Observe{ handleProducts(it) }
        myStoreViewModel.listenToStore().observe(this, Observe{ handleStore(it) })
        myStoreViewModel.getAllProducts(yourToken, yourStoreId)
        myStoreViewModel.getStoreInfo(yourToken, yourStoreId)
    }

    //misalnya saja ya
    private fun handleUI(it: MyStoreState){
        when(it){
            is MyStoreState.Loading {
                if(it.state){
                    //show progressbar
                }else{
                    //hide progressbar
                }
            }
            is MyStoreState.ShowToast -> Toast.makeText(this, it.message, Toast.LENGTH_SHORT).show()            
        }
    }

    private fun handleProducts(it: List<Product>){
        //attach to your recycler view
    }

    private fun handleStore(it: Store){
        //attach store to your view
    }

}

This approach will helps you to get the data from a server inside repository class, then transfer it to viewmodel. But still, callback is not flexible to use, It is hard when we have dynamic data.

Another savior, Interface

Interface is a powerful thing in Kotlin, not only they can be a contract about what needs to when implemented on class but also we can use it to handle dynamic response for api request.

First, we need to create the interface

interface OnSingleResponse<T>{
    fun onSuccess(data: T?)
    fun onFailure(error: Error)
}

interface OnArrayResponse<T>{
    fun onSuccess(datas: List<T>?)
    fun onFailure(error: Error)
}

The code above is for handle type of request, So you don't need to create every response type like UserListResponse, UserResponse, etc.. If you want to GET data like the json below..

[
{
   "id":1,
    "name":"Prieyudha Akadita S",
    "age": 22
},
{
    "id":2,
    "name":"Raline Shah",
    "age":33
}
]

...just write the APIService like this

interface ApiService {
    @GET("api/users")
    fun getUsers() : Call<List<User>>
}

How about the repository?

interface UserContract {
    fun getUsers(listener: OnArrayResponse<User>)
}

class UserRepository (private val api: ApiService) : UserContract {
    override fun getUsers(listener: OnArrayResponse<User>){
        api.getUsers().enqeue.....{
            onResponse(....){
                //do magic here
                listener.onSuccess(response.body())
            }

            onFailure(...){
                //handle your error here
                listener.onFailure(Error(t.message.toString()))
            }
        }
    }
}

And last but not least, here is the ViewModel

class UserViewModel(private val userRepository: UserRepository) : ViewModel {
    private val users = MutableLiveData(List)
    private val state .....


    fun fetchAllUsers(){
        showLoading()
        userRepository.getUsers(object: OnArrayResponse<User>{
            override fun onSuccess(datas: List<User>?){
                hideLoading()
                datas?.let{
                    users.postValue(it)
                }
            }
            override fun onFailure(error: Error){
                hideLoading()
                showToast(error.message.toString())
            }
        })
    }

}

In many cases, using Interface are better and save than using callback. It also easy to read so that will be good for your team.

Conclusion

This is how I wiring my repository-viewmodel-ui in my projects. If you have any questions, just ask, and if you still don't know how to implement this, you can always look to this video

Top comments (1)

Collapse
 
gfrgomes profile image
Gustavo Gomes

I think you should’ve also mixed in Room database or some kind of persistence, even Shared Preferences, because normally we don’t just go to the API to get the data or we tend to store it in our database for later. But overall it was a great article!

πŸŒ™ Dark Mode?!

Turn it on in Settings