본문으로 바로가기
반응형

이 글은 아래 포스팅을 기반으로 작성되었으며, 상세한 내용은 링크에서 확인하시기 바랍니다.

2019/11/14 - [개발이야기/Android] - Easy Coroutines in Android: viewModelScope

https://developer.android.com/topic/libraries/architecture/coroutines#livedata

https://proandroiddev.com/the-death-of-presenters-and-the-rise-of-viewmodels-aac-f14d54b419a

 

ViewModel도 Activity나 Fragment처럼 생명주기를 갖습니다.

따라서 ViewModel의 생명 주기에 맞춘 Coroutine Scope을 설정한다면 coroutine의 호출이나 동작을 자동으로 취소할 수 있는 편의성을 제공할 수 있습니다.

 

이 포스팅은 앞선 글과 연속성을 갖습니다.

따라서 아래 글 먼저 확인을 추천드립니다.

2020/08/04 - [개발이야기/Android] - [Android, MVVM, Coroutine] 활용 #1 - Android에서 Coroutine 사용

2020/08/05 - [개발이야기/Android] - [Android, MVVM, Coroutine] 활용 #2 - Room에서 Coroutine 사용

 

ViewModelScope

ViewModel에서도 앞선 Activity처럼 CoroutineScope을 implement할수 있으나, 친절하게도 이미 extension property를 추가하여 바로 사용할 수 있도록 library를 제공합니다.
따라서 아래의 두개의 library를 gradle에 추가합니다.
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'

livedata는 아래에서 언급할 예정이니 미리 추가해 둡니다.

라이브러리를 추가함에 따라 ViewModel에서도 쉽게 coroutine을 사용할 수 있습니다.

추가적으로 앞으로 사용될 예제에서 기본적으로 호출할 간단한 Dao를 정의합니다.

@Dao
interface UsersDao {
    @Query("SELECT * FROM users")
    suspend fun getUsers(): List<User>
    @Query("SELECT * FROM users WHERE _id = :userId")
    suspend fun getUser(): User?
}

viewModel에서 제공하는 coroutine scope은 viewModelScope 입니다.

따라서 아래와 같이 접근할 수 있습니다.

class MyViewModel @Inject constructor(private val dBRepository: DBRepositoy) : ViewModel() {
    fun launchDataLoad() {
        viewModelScope.launch {
            val userList = dBRepository.getUsers().sortList()
            // do something.      
        }
    }

    suspend fun sortList(userList) = withContext(Dispatchers.Default) {
        //sorting.
    }
}

viewModelScope을 이용하여 생성된 coroutine 들은 viewModel의 생명주기와 같이 합니다.

즉 ViewModel이 종료되면 clear()가 호출되면 내부에서 viewModelScope context의 job을 cancel() 합니다.

clear() 함수는 onCleared() 호출 직전에 호출되는 함수 입니다.

viewModelScope의 내부 동작 분석은 상단에 링크된 "Easy Coroutines in Android..."에서 확인할 수 있습니다.

 

LiveData builder

위에서 추가한 livedata library로 인하여 coroutine으로 LiveData를 생성할수 있습니다.
liveData builder는 아래와 같이 사용합니다.
class MyViewModel @Inject constructor(private val dBRepository: DBRepositoy) : ViewModel() {
    val users: LiveData<List<User>> = liveData    {
        val userList = dBRepository.getUsers()
        emit (userList)
    }
}

만약 특정 user를 조회하고 싶다면 아래와 같이 LiveData의 Transformation을 이용할 수 있습니다.

class MyViewModel @Inject constructor(private val dBRepository: DBRepositoy) : ViewModel() {
    val userId = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData {
            val user = dBRepository.getUser(id)
            emit(user)
        }
    }
}

호출부분에서는 userId에 setValue로 id를 넘겨주고, user에 observer를 받아 결과를 처리하면 됩니다.

livedata builder의 장점은 emit() 함수를 이용하여 livedata에 여러번 값을 emit()할수 있습니다.

이는 Flow의 transfrom operator와 유사합니다.

 

Livedata loading stauts

위에서 user정보를 가져올때 실제 호출부에서는 아래와 같이 코드를 작성합니다.
 
override fun onCreate() {
    super.onCreate()
    ....
    observeUser()
    ...
}  // 요청된 user 결과를 처리한다.

private fun observeUser() { //init userInfoObserver
    myViewModel.user.observer(this, Observer { user -> showUserInfo(user) })
}

// 화면에서 user list중 하나를 선택했을때 호출된다.
private fun onUserListClick(user: User) {
    myViewModel.userId.setValue = user.id
}

 

User list 화면에서 한명을 선택하는 경우 onUserListClick()가 호출된다고 가정합니다.

livedata에 데이터를 set 해주고 나면, Room에서 작업후 등록된 observer가 호출되면서 결과값을 넘겨줍니다.

방대한 DB가 아니라면, 빠르게 값이 return되겠지만, DB가 방대하거나, viewModel이 DBRepository가 아닌 NetworkRepository에서 가져온다면, 시간이 걸릴 수 있으므로 가져오는 시간동안 loading progress를 보여주고 싶다면 아래와 같은 방법으로 처리 합니다.

 

onUserListClick()이 발생하면 showProgressDialog()를 해주고, observer 내부에서 hideProgressDialog()를 해줄수도 있습니다.

하지만, show/hide를 해주는 pair가 서로 맞지 않는 경우가 발생 할수 있어 권장되는 형태는 아닙니다.

이를 해결하기 위해 결과를 wrapping하는 Resource 클래스를 하나 추가합니다.

class Resource<out T> constructor(val state: ResourceState, val data: T? = null, val message: String? = null) {
    companion object {
        const val INVALID_MESSAGE_RES_ID = -1
    }

    sealed class ResourceState {
        object LOADING : ResourceState() {
            override fun toString() = "loading"
        }

        object SUCCESS : ResourceState() {
            override fun toString() = "success"
        }

        object ERROR : ResourceState() {
            override fun toString() = "error"
        }
    }
}

그리고 ViewModel을 아래와 같이 변경합니다.

class MyViewModel @Inject constructor(private val dBRepository: DBRepositoy) : ViewModel() {
    val userId = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData {
            emit(Resource(Resource.ResourceState.LOADING))
            val user = dBRepository.getUser(id)              if (user == null) {
            emit(Resource(Resource.ResourceState.ERROR, message = "There is no user."))
        } else {
            emit(Resource(Resource.ResourceState.SUCCESS, data = user))
        }
        }
    }

DB를 조회하기전에 LOADING 상태를 emission 하고 에러가 발생하면 ERROR 상태와 메시지를 전달합니다.

또한 성공일 경우 SUCCESS 상태와 결과값을 emission 합니다.

마지막으로 호출부분을 수정합니다.

...
// 요청된 user 결과를 처리한다.
private fun observeUser() {     //init userInfoObserver
    myViewModel.user.observer(this, Observer { resource ->
        when (resource.state) {
            Resource.ResourceState.LOADING -> showProgressDialog()
            Resource.ResourceState.SUCCESS -> {
                hideProgressDialog() showUserInfo (resource.data)
            }
            Resource.ResourceState.ERROR -> {
                hideProgressDialog() showToast (resource.message)
            }
        })
    }
}
...

 

 

반응형