ABOUT ME

-

  • Android) 안드로이드에서 Coroutine의 ViewModelScope와 LiveData Builder 알아보기
    Android 2021. 1. 21. 16:37

     

    일반적으로 코루틴을 안드로이드 ViewModel과 사용할 때 SupervisorJob을 이용하여 생명주기와 맞물려 사용합니다.

    ViewModel은 UI에서 보여줄 데이터를 관리하기 위해 사용됩니다.

    Activity나 Fragment 같은 컴포넌트의 생명주기를 따라가 destroy 됐을 때 onCleared()를 호출하며 해제되는 특징이 있습니다.

     

    (SupervisorJob = Job은 모든 자식들을 취소 시킬 수 있지만, SupervisiorJob은 취소가 아래 방향(부모->자식)으로만 전파됩니다.)

     

    ViewModelbussiness logic(기획적인 부분)을 가지고 있기 때문에 비용이 큰 작업을 처리해야 하는 경우가 있습니다.

    onCleared()가 호출되고 ViewModel이 메모리에서 해제되면 더이상 실행중이던 작업들은 필요가 없어집니다.

    그래서 Memory Leak을 방지하기 위해 생명주기에 맞춰 onCleard() 함수에서 scope의 취소를 구현하도록 합니다.

    따라서 ViewModel의 생명주기에 의해 코루틴도 생성하고, 소멸되도록 아래의 코드처럼 구현가능합니다.

     

    SampleViewModel

    class SampleViewModel : ViewModel() {
        private val job = SupervisorJob()
        private val uiScope = CoroutineScope(Dispatchers.Main + job)
    
        private val _toastMsg = MutableLiveData<String>()
        val toastMsg: LiveData<String>
            get() = _toastMsg
    
        fun loadData() = uiScope.launch {
            _toastMsg.value = "start" // main thread
            sortList() // background thread
            // Update UI 
            _toastMsg.value = "end" // main thread
        }
    
    
        private suspend fun sortList() = withContext(Dispatchers.Default) {
            // Heavy work
            delay(5000)
        }
    
        override fun onCleared() {
            super.onCleared()
            job.cancel()
        }
    }

    위의 예제는 "start"가 먼저 시작되고, 5초 후에 "end"가 표시되는 코드입니다.

     

    RxJava와 ViewModel을 함께 사용할때 생명주기에 맞춰 생성과 소멸을 해주는 코드와 유사합니다.

    class SampleViewModel : ViewModel() {
        private val compositeDisposable = CompositeDisposable()
    
        fun addDisposable(disposable: Disposable) {
            compositeDisposable.add(disposable)
        }
    
        override fun onCleared() {
            super.onCleared()
            compositeDisposable.clear()
        }
    }
    

     

    이런 코드들을 각각 ViewModel에 포함해서 사용하기 귀찮다면, BaseViewModel을 만들어 사용해주면 될 것 같습니다.

     

    BaseViewModel

    open class BaseViewModel : ViewModel() {
    
        private val job = SupervisorJob()
    
        protected val uiScope = CoroutineScope(Dispatchers.Main + job)
    
        override fun onCleared() {
            super.onCleared()
            job.cancel()
        }
    }

    SampleViewModel

    class SampleViewModel : BaseViewModel() {
        fun launchDataLoad() = uiScope.launch {
            sortList()
            // Update UI
        }
    
    
        suspend fun sortList() = withContext(Dispatchers.Default) {
            // Heavy work
        }
    }

    ViewModelScope

    이것도 귀찮다면, 구글에서는 더욱 편하게 사용하기 위한 viewModelScope라는 확장함수를 제공합니다.

    viewModelScopelifecycle을 인식하는 코루틴 스코프를 만들 수 있습니다.

    그래서 viewModelScope 블럭에서 실행되는 작업은 별도의 처리를 하지 않아도 ViewModelclear되는 순간 자동으로 취소 됩니다.

     

    build.gradle

    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"

     

    SampleViewModel

    class SampleViewModel : ViewModel() {
        fun loadData() {
            viewModelScope.launch {
                sortList()
                // Update UI
            }
        }
    
        suspend fun sortList() = withContext(Dispatchers.Default) {
            // Heavy work
        }
    }

    기존에 uiScope를 만들고 onCleard에서 취소해주는 로직들이 viewModelScope로 인해 불필요하게 되었습니다.

     

    viewModelScope는 어떤 원리로 이러한 작업을 가능하게 할까요?

    ViewModel 내부의 viewModelScope는 아래와 같이 구현되어 있습니다.

    private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
    
    public val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if (scope != null) {
                return scope
            }
            return setTagIfAbsent(
                JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
            )
        }
    
    internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
        override val coroutineContext: CoroutineContext = context
    
        override fun close() {
            coroutineContext.cancel()
        }
    }

    CoroutineScope을 저장하면 getTag(JOB_KEY)로 꺼내서 사용하고 있습니다.

    만약 생성된 CoroutineScope이 없다면 setTagIfAbsent() CloseableCoroutineScope 인스턴스를 인자로 넘겨,

    ViewModel 필드에 있는 HashMap에 코루틴 객체를 저장후 사용합니다. 

     

    public abstract class ViewModel {
        @Nullable
        private final Map<String, Object> mBagOfTags = new HashMap<>();
        private volatile boolean mCleared = false;
    
        @MainThread
        final void clear() {
            mCleared = true;
          
            if (mBagOfTags != null) {
                synchronized (mBagOfTags) {
                    for (Object value : mBagOfTags.values()) {
                        // see comment for the similar call in setTagIfAbsent
                        closeWithRuntimeException(value);
                    }
                }
            }
            onCleared();
        }
        
        private static void closeWithRuntimeException(Object obj) {
            if (obj instanceof Closeable) {
                try {
                    ((Closeable) obj).close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

    ViewModel이 해제되면 onCleared()가 호출되기 전에 clear()가 수행됩니다.

    clear() 함수에는 viewModelScope취소시키는 코드가 들어 있습니다.

    ViewModel에서 관리하던 HashMap 객체들을 모두 Closeable로 변환 후 close()를 호출 함으로써

    생명주기가 끝났을때의 처리를 하고 있습니다.


    LiveData Builder

    마찬가지로 구글에서 LiveData의 value를 비동기적으로 설정해야 할 때 도움을 주는 liveData builder를 제공하고 있습니다.

    liveData 블럭안의 코드는 LiveDataactivie 됐을 때 실행되고, inActive가 되면 자동으로 취소됩니다.

     

    val user: LiveData<User> = liveData {
        val data = database.loadUser() // loadUser is a suspend function.
        emit(data)
    }

    LiveData observer에게 제공하기 위해 emit()emitSource() 함수를 사용합니다.

    liveData에 코루틴 컨텍스트를 따로 설정하지 않으면 기본적으로 Main Thread에서 실행됩니다. 

    만약 Background Thread에서 실행하고 싶다면 다음 코드처럼 상황에 맞게 Dispathers를 지정해줘야 합니다.

    liveData(Dispatchers.Default) {
       // ..
    }

     

    liveData의 내부는 다음과 같습니다.

    @UseExperimental(ExperimentalTypeInference::class)
    fun <T> liveData(
        context: CoroutineContext = EmptyCoroutineContext,
        timeoutInMs: Long = DEFAULT_TIMEOUT,
        @BuilderInference block: suspend LiveDataScope<T>.() -> Unit
    ): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)

     

    CoroutineContext - 코루틴의 쓰레드를 지정하는 인자

    timeout - LiveDatainactive 상태가 됐을 때, 실행이 취소되기 전 대기 시간을 설정해줄 수 있습니다.

    예를들어 Configuration 변경같은 화면 회전 시에 대기 시간을 줌으로서 앱이 중단되는 일을 막을 수 있습니다.


    References

    ViewModelScope, LiveData Builder 사용하기

    Easy Coroutines in Android

    반응형

    댓글

Designed by Me.