[Android] viewModelScope.launch() 간단하게 바꿔보기
Android

[Android] viewModelScope.launch() 간단하게 바꿔보기

728x90

 

 

 

  ViewModel에서 코루틴을 사용할 때는 androidx-lifecycle에서 제공하는 viewModelScope를 많이 사용합니다. viewModelScope는 ViewModel의 extension property로 ViewModel이 destroy 될 때 자식 코루틴들을 자동으로 취소하는 기능을 제공합니다. 이번 포스팅에서는 viewModelScope를 보다 간단하게 사용할 수 있는 방법을 알아보겠습니다.

 

 

확장 함수 사용하여 개선하기

 ViewModel에서 viewModelScope을 사용해 코루틴을 실행할 때는 일반적으로 아래와 같은 방식으로 사용합니다.

class MainViewModel : ViewModel() {

    init {
        viewModelScope.launch {
            // ...
        }
        
       	viewModelScope.launch(Dispatchers.IO) {
            // ...
        }
    }
}

 

 위의 코드는 큰 문제는 없지만, 다음의 확장 함수를 사용하면 viewModelScope의 반복을 방지할 수 있으며 훨씬 간단하고 읽기 쉬운 코드를 작성할 수 있습니다.

inline fun ViewModel.onMain(
    crossinline body: suspend CoroutineScope.() -> Unit
) = viewModelScope.launch {
    body(this)
}

inline fun ViewModel.onIO(
    crossinline body: suspend CoroutineScope.() -> Unit
) = viewModelScope.launch(Dispatchers.IO) {
    body(this)
}

inline fun ViewModel.onDefault(
    crossinline body: suspend CoroutineScope.() -> Unit
) = viewModelScope.launch(Dispatchers.Default) {
    body(this)
}

 

 사용법은 다음과 같습니다. 이전 방식과 비교하여 훨씬 간단해졌음을 알 수 있습니다.

class MainViewModel : ViewModel() {

    private fun doDefaultOperation() = onDefault {
        // ...
    }
    
    private lateinit var job: Job
    
    private fun cancelPreviousJobAndStartNewOne() {
        if (::job.isInitialized) {
            job.cancel()
        }
        
        job = onIO {
            // ...
        }
    }
}

 

 

 

Testable 하게 개선하기

 위의 설명에서는 Dispatchers를 하드 코딩하여 바로 지정해주는 방식을 사용했었습니다. 하지만 이와 같은 방식은 좋은 방식이 아닙니다. 안드로이드 구글 공식 사이트의 코루틴 권장사항에 따르면 새로운 코루틴을 만들 때 Dispatcher를 주입시켜주는 것이 좋습니다.

 Dispatcher를 하드 코딩 방식으로 지정해주면 테스트 코드를 작성할 수 없습니다. (참고)  따라서 Dispatcher를 외부에서 주입하는 방식으로 코드를 수정해보겠습니다.

 

 

 

 

 우선 기본 Dispatcher를 가지고 있는 DispatcherProvider 인터페이스를 정의해줍니다. 이 인터페이스의 구현체는 총 2가지로 하나는 프로덕션 코드용이고, 하나는 테스트 코드용입니다.

interface DispatcherProvider {
    val main: CoroutineDispatcher
    val io: CoroutineDispatcher
    val default: CoroutineDispatcher
}
@Module
@InstallIn(ViewModelComponent::class)
interface DispatcherModule {

    @Binds
    fun bindDispatcherProvider(provider: DispatcherProviderImpl): DispatcherProvider
}

@Singleton
class DispatcherProviderImpl @Inject constructor() : DispatcherProvider {
    override val main: CoroutineDispatcher
        get() = Dispatchers.Main

    override val io: CoroutineDispatcher
        get() = Dispatchers.IO

    override val default: CoroutineDispatcher
        get() = Dispatchers.Default
}

 

 그 후, DispatcherProvider를 외부에서 주입받아 BaseViewModel를 구현해줍니다. 기존의 만들어뒀던 확장 함수들도 DispatcherProvider를 사용하여 Dispatcher를 설정해줍니다.

open class BaseViewModel(dispatcherProvider: DispatcherProvider) : ViewModel(),
    DispatcherProvider by dispatcherProvider

inline fun BaseViewModel.onMain(
    crossinline body: suspend CoroutineScope.() -> Unit
) = viewModelScope.launch {
    body(this)
}

inline fun BaseViewModel.onIO(
    crossinline body: suspend CoroutineScope.() -> Unit
) = viewModelScope.launch(io) {
    body(this)
}

inline fun BaseViewModel.onDefault(
    crossinline body: suspend CoroutineScope.() -> Unit
) = viewModelScope.launch(default) {
    body(this)
}

 

사용법은 다음과 같습니다.

@HiltViewModel
class MainViewModel @Inject constructor(
    dispatcherProvider: DispatcherProvider
) : BaseViewModel(dispatcherProvider) {

    var isSaved = false

    fun saveDataOnIO() = onIO {
        delay(5000)
        isSaved = true
    }
}

 

 

 

테스트 코드 작성

 이제 ViewModel을 테스트할 수 있는 모든 준비가 끝났습니다. 간단한 테스트 코드를 하나 작성해보겠습니다.

 테스트 코드에서만 사용할 TestDispatcherProvider를 구현해줍니다. 생성자로 주입받는 testDispatcher는 kotlinx.coroutines.test에서 제공하는 TestCoroutineScope를 사용합니다. 

class TestDispatcherProvider(
    private val testDispatcher: CoroutineDispatcher
) : DispatcherProvider {
    override val main: CoroutineDispatcher
        get() = testDispatcher
    override val io: CoroutineDispatcher
        get() = testDispatcher
    override val default: CoroutineDispatcher
        get() = testDispatcher
}

 

 

 모든 테스트 코드에서 공통적으로 사용할 Rule을 만들어줍니다.

@ExperimentalCoroutinesApi
class MainCoroutineRule(val testCoroutineDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) :
    TestRule, TestCoroutineScope by TestCoroutineScope() {

    private val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)

    override fun apply(base: Statement, description: Description?) = object : Statement() {
        @Throws(Throwable::class)
        override fun evaluate() {
            Dispatchers.setMain(testCoroutineDispatcher)

            base.evaluate()

            Dispatchers.resetMain()
            testCoroutineScope.cleanupTestCoroutines()
        }
    }

    fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
        testCoroutineScope.runBlockingTest { block() }
}

 

@ExperimentalCoroutinesApi
class MainViewModelTest : TestCase() {

    private var mainCoroutineRule = MainCoroutineRule()

    private val mainViewModel = MainViewModel(
        TestDispatcherProvider(mainCoroutineRule.testCoroutineDispatcher)
    )

    fun test01_save_data() {
        mainCoroutineRule.runBlockingTest {
            mainViewModel.saveDataOnIO()
            advanceUntilIdle()

            assertTrue(mainViewModel.isSaved)
        }
        mainCoroutineRule.testCoroutineDispatcher.cleanupTestCoroutines()
    }
}

 

 

예제 소스

https://github.com/tkdgusl94/blog-source/tree/master/ViewModelExtensions

 

GitHub - tkdgusl94/blog-source: https://leveloper.tistory.com/ 에서 제공하는 예제 source

https://leveloper.tistory.com/ 에서 제공하는 예제 source. Contribute to tkdgusl94/blog-source development by creating an account on GitHub.

github.com

 

 

참고

https://kapta.medium.com/simplify-android-viewmodels-by-using-these-kotlin-extenstions-part-1-dcee2424e397

 

Simplify Android Viewmodels by using these kotlin extenstions (part 1)

Every day I’am surprised with kotlin (extenstions) power. I’am going to share a set of extenstions that it’s currently used in our…

kapta.medium.com

 

728x90