본문 바로가기

개발일기

Retrofit - MVVM - Coroutine ㄲㄲㄲ

그렇다. async에 아주 많이 애를 먹었다. 어째서 두 개의 fun을 리스트로 묶어 awaitAll()을 실행했는데 자꾸 하나를 기다리지 않고 다음 fun을 진행해버리는가?!

 

일단 이 문제는 잠시 뒤로 미루고 프로세스가 진행될 수 있게, 두 fun을 각기 다른 fun 안에서 async 처리해주었다. 속성으로 공부해서 실무에 적용하려니 정말 힘들다 힘들어, 나는 설명을 해줘야 하고 나에게 설명해줄 사람은 없고! 1년차에 이래도 되는 겁니까 이거!!!

일단 이전에 url 앞부분 (Shop, Field, ...)에 따라 apiCall을 나눠놓는 것은 굉장히 비효율적이라는 결론을 내리고 하나의 인터페이스에 모두 몰아넣기로 하였다.

 

ApiCall.kt

interface ApiCall {

    @POST("/Login")
    suspend fun startLogIn(@Body op: Operator): Response<ApiResult>

    @POST("/Shop/haha")
    suspend fun haha(@Body reqData: RequestData): Response<ApiResult>

    @POST("/Shop/hoho")
    suspend fun hoho(@Body reqData: RequestData): Response<ApiResult>

        ...

    companion object {
        fun create() : ApiCall {
            val retrofit = Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(StaticValue.baseUrl)
                .build()

            return retrofit.create(ApiCall::class.java)
        }
    }
}

 

ViewModel에서 constructor의 인자로 사용하는 Repository는 이전과 거의 동일하다. Repository도 액티비티마다 다르게 두지 않고 전체가 하나의 Repository를 공유하게 설계하였다.

 

Repository.kt

class Repository constructor(private val apiCall: ApiCall) {

    suspend fun startLogin(op: Operator): Response<ApiResult> 
            = apiCall.startLogIn(op)

    suspend fun haha(requestData: RequestData) 
            = apiCall.haha(requestData)

    suspend fun hoho(requestData: RequestData) 
            = apiCall.hoho(requestData)

        ...
}

 

ViewModelFactory도 하나만 만들어서, 어떤 ViewModel::class.java를 받느냐에 따라 리턴 View를 다르게 받는 식으로 설정해두었다. 결국 인터페이스, 레포지토리, 그리고 뷰모델팩토리는 하나씩만 만들어놓고 액티비티별 ViewModel에서 필요한 것을 처리하게끔 한 것. 이게 MVVM 맞겠지..?

 

ViewModelFactory.kt

class ViewModelFactory constructor(private val repository: Repository) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {

        return when {
            modelClass.isAssignableFrom(LogInViewModel::class.java) -> {
                LogInViewModel(this.repository) as T
            }
            modelClass.isAssignableFrom(MainViewModel::class.java) -> {
                MainViewModel() as T
            }
            else -> {
                throw IllegalArgumentException("ViewModel Not Found")
            }
        }
    }
}

 

그리고 ApiCall에 대한 실제 response를 처리하는 ViewModel. epository를 인자로 받는 constructor가 default constructor로, repository에서 로그인 페이지에서 사용할 fun 들에 대한 response를 구현했다.

이건 아마 이전 포스트와 비슷할텐데, 달라진 점이 있다면 이전 fun에서 정보를 받아온 다음 실행해야 하는 fun의 경우 그 내부에 async로 해당 fun을 await() 시켜준 후 url에 쏴서 정보를 받아온다는 점이다. 그럴 필요 없이, 즉 받아오는 정보의 순서에 상관 없는 apiCall은 비동기로 쏘고 받아온다.

 

class LogInViewModel constructor(private val repository: Repository) : ViewModel() {
    val errorMessage = MutableLiveData<String>()
    var job: Job? = null

        ...    
        // startLogIn 다음 -> haha(reqData)

        fun haha(reqData: RequestData) {
        job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
            val response = repository.haha(reqData)
            ...
            if (response.isSuccessful) {
                if (response.body() != null) {
                    val resultCode = response.body()!!.resultCode
                    ...
                    val g = Gson()
                    StaticValue.playDate = response.body()!!.data.toString()
                                        ...
                                        // 한 번에 다 쏴준다. 다만,
                                        // 어떤 api Call로 받아온 데이터를 Static에 저장한 값을 활용하는 경우
                                        // 해당 데이터를 받는 fun 안에서 async & await()로 받아온다.                                        
                    val reqData2 = RequestData2.setBasicRequestData(true)
                    getDiscountEventList(reqData2)
                    getSaleItems(reqData2)
                    getMemberInfo(reqData2)
                    getAnnualGroup(reqData2)
                }
            }
            else {
                // Failed
            }

        }
    }

        // async & await() 필요한 함수 예시
        fun getAnnualGroup(reqData: RequestData) {
        job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
            val response = repository.getAnnualGroupList(reqData)
            ...
            if (response.isSuccessful) {
                if (response.body() != null) {
                    val type = object : TypeToken<MutableList<AnnualGroup>>(){}.type
                    val g= Gson()
                    val result = g.toJsonTree(response.body()!!.data, type)
                    val resultAnnualGroup = result.asJsonArray
                                        ...
                    val annualGroupList = ArrayList<AnnualGroup>()
                    ...
                    StaticValue.annualGroups = annualGroupList

                    // getRooms(reqData)에서 StaticValue.rooms 를 처리하기 때문에
                    // 해당 값을 받아올 때까지 await() 시켜준다.
                    val rooms = async(Dispatchers.IO) { getRooms(reqData) }
                    val roomResult = rooms.await()

                    if (StaticValue.rooms.isNotEmpty()) {
                        getRoomPrice(reqData)
                        getAccommodationMemberType(reqData)

                        val holiday = async(Dispatchers.IO){getHoliday(reqData)}.await()
                    }
                    else
                        getHoliday(reqData)
                }
            }
            else {
                // Failed
            }
        }
    }
        ...
        var readyToMoveOn = MutableLiveData<Int>()
        fun getHoliday(reqDataL RequestData) {
        ...
        // ActivityLogIn에서 observe 하고 있는 variable에게 value를 부여해준다.
        // 이 Int값을 받은 것을 observe 하여 MainActivity로 넘어간다.
        readyToMoveOn.postValue(response.body()!!.resultCode!!)
        }
}