저장소 연결 - ViewModel - Android의 UI

이 글은 제 블로그 글의 카피here입니다.

우리 중 일부는 저장소 패턴을 viewmodel에 연결한 다음 View에서 관찰하는 방법을 고심하고 있습니다. MVVM에 대해 처음 배웠을 때 모든 논리를 ViewModel에 넣었습니다(저는 저장소를 사용하지 않습니다). 앱이 점점 커지면 관리하는 것이 정말 고통스럽습니다.

간단합니다. 논리를 저장소에 넣으면 됩니다. 그렇죠?



네, 그렇습니다. 하지만 그렇게 쉬운 일이 아닙니다. 많은 안드로이드 개발자가 로그인을 저장소로 옮기는 것을 보았습니다.
내가 본 첫 번째 접근 방식은 다음과 같습니다.

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

그러면 뷰 모델과 활동이 다음과 같이 보일 것입니다.

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



그런 다음 LiveData를 ViewModel 안에 넣으면 되지 않습니까?



좋아, 시도하자...

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
}

그런 다음 저장소에서 ...

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.



첫 번째 구원자: 콜백!



Kotlin Callback을 사용하여 이 문제를 해결할 수 있습니다. 콜백은 아마도 이 일을 완료하는 가장 빠른 방법일 것입니다.

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.



뷰 모델에서 다음과 같이 보일 것입니다.

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

}

다중 리포지토리, 다른 예상 결과 및 UI 상태와 결합



하나의 뷰 모델에 다른 저장소가 있다면 어떻게 될까요? 그리고 그것을 UI State와 결합하는 방법은 무엇입니까? 이 예에서는 봉인된 클래스를 사용하여 UI 상태를 관리합니다. 혹시 모르니 제 글How To Manage UI State using Sealed Class in Android을 보시면 됩니다.

우리는 두 개의 저장소를 만들 것입니다. 첫 번째는 StoreRepository이고 두 번째는 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)
            }
        }
    }
}

다음은 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)
            }
        }
    }
}

그리고 여기 뷰 모델이 있습니다

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()
}

다음은 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.



또 다른 구원자, 인터페이스



인터페이스는 Kotlin에서 강력한 기능입니다. 클래스에서 구현될 때 필요한 사항에 대한 계약일 뿐만 아니라 API 요청에 대한 동적 응답을 처리하는 데 사용할 수도 있습니다.

먼저 인터페이스를 만들어야 합니다.

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

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

위의 코드는 요청의 핸들 타입을 위한 것이므로 UserListResponse, UserResponse 등 모든 응답 타입을 생성할 필요는 없습니다. 아래의 json과 같은 데이터를 GET하고 싶다면..

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

...다음과 같이 APIService를 작성하십시오.

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

저장소는 어떻습니까?

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()))
            }
        }
    }
}


그리고 마지막으로 중요한 것은 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.



결론



이것이 내 프로젝트에서 repository-viewmodel-ui를 연결하는 방법입니다. 질문이 있는 경우 질문하세요. 여전히 구현 방법을 모르는 경우 항상 다음을 참조하세요.

좋은 웹페이지 즐겨찾기