본 게시글의 Coroutine 개념은 Android 내에서 사용되는 것을 전제로 작성되었다.
#1 Coroutine이 쓰인 LiveData 예제 - 전통적인 방법
#1은 LiveData 값 할당에 Coroutines가 쓰인 샘플 프로젝트다.
#1-1 build.gradle.kts (Module)
plugins {
...
}
android {
...
}
dependencies {
...
// Coroutines
val coroutinesVersion = "1.7.3"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
val lifecycleVersion = "2.6.2"
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
// LiveData
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
}
Coroutines, ViewModel, LiveData 라이브러리를 추가한다.
#1-2 User.kt 데이터 클래스
// package com.example.livedatabuilder.model
data class User(val id: Int, val name: String)
2개의 프로퍼티를 가진 data class를 만들었다.
#1-3 UserRepository.kt
// package com.example.livedatabuilder.model
import kotlinx.coroutines.delay
class UserRepository {
suspend fun getUsers(): List<User> {
delay(8000)
val users: List<User> = listOf(
User(1, "Park Bom"),
User(2, "Park Sandara"),
User(3, "CL"),
User(4, "Gong Minji")
)
return users
}
}
Repository 역할을 할 클래스도 만든다. 이 클래스의 getUsers() 메소드는 방금 만들었던 data class 인스턴스의 배열을 반환한다. Repository에서 값을 꺼내오는 데에 걸리는 시간을 delay() 함수로 표현했다.
#1-4 MainActivityViewModel.kt
// package com.example.livedatabuilder
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.livedatabuilder.model.User
import com.example.livedatabuilder.model.UserRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivityViewModel : ViewModel() {
private var usersRepository = UserRepository()
var users: MutableLiveData<List<User>?> = MutableLiveData()
fun getUsers() {
viewModelScope.launch {
var result: List<User>? = null
withContext(Dispatchers.IO) {
result = usersRepository.getUsers()
}
users.value = result
}
}
}
ViewModel과 그 프로퍼티로 LiveData를 만든다. LiveData에 값을 할당해주기 위해서, viewModelScope에서 Repository.getUsers()를 호출한다. Dispatchers.IO 스레드(이 링크의 #3-2 참조)는 주로 입/출력을 담당하는 스레드다. Repository의 역할 또한 데이터 입/출력이므로 withContext() 함수를 통해서 스레드를 임시 전환해 getUsers()를 수행시켰다.
#1-5 MainActivity.kt
// package com.example.livedatabuilder
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
class MainActivity : AppCompatActivity() {
private lateinit var mainActivityViewModel: MainActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mainActivityViewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)
mainActivityViewModel.getUsers()
mainActivityViewModel.users.observe(this, Observer { myUsers ->
myUsers?.forEach {
Log.i("interfacer_han", "name is ${it.name}")
}
})
}
}
/* Log 출력 결과
name is Park Bom
name is Park Sandara
name is CL
name is Gong Minji
*/
Activity에서 LiveData에 값을 할당하는 작업 후 해당 LiveData를 observe한다. 이제 8초 후에 Coroutines 코드가 완료되면 Log 메시지가 뜰 것이다.
#2 Coroutine이 쓰인 LiveData 예제 - LiveData Builder 이용
#2-1 개요
#1의 코드를 수정해 메모리 누수와 코드량 둘 다 줄일 수 있는 방법이 있는데, LiveData Builder를 사용하는 것이다. lifecycle-livedata-ktx가 2.4.0 버전 이상이면 LiveData Builder를 사용할 수 있다. #1에서 이미 해당 버전 이상의 lifecycle-livedata-ktx를 모듈 수준 build.gradle.kts에서 추가했으므로 build.gradle.kts는 수정하지 않아도 된다.
#2-2 MainActivityViewModel.kt에서 LiveData에 LiveDataBuilder 사용
// package com.example.livedatabuilder
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import com.example.livedatabuilder.model.UserRepository
import kotlinx.coroutines.Dispatchers
class MainActivityViewModel : ViewModel() {
private var usersRepository = UserRepository()
var users = liveData(Dispatchers.IO) {
val result = usersRepository.getUsers()
emit(result) // LiveData에 새로운 값을 발행하고 이를 구독(LiveData.observe())하고 있는 관찰자(Observer)들에게 알림
}
/*
var users: MutableLiveData<List<User>?> = MutableLiveData()
fun getUsers() {
viewModelScope.launch {
var result: List<User>? = null
withContext(Dispatchers.IO) {
result = usersRepository.getUsers()
}
users.value = result
}
}
*/
}
LiveData는 LiveData가 들어가 있는 LifecycleOwner 인터페이스에 영향을 받으며 작동한다 (이 링크의 #2-3 참조). LifecycleOwner의 상태에 따라 LiveData 값의 '갱신 후 후속 작업'을 할 지 안할 지를 결정하는 것이다. 하지만, #1-4에서 LiveData는 viewModelScope 즉, ViewModel의 생명주기를 따르는 코루틴 영역에 의해 실행된다. 반면, LiveData Builder는 LiveData가 활성화되면 실행을 시작하고, 비활성화되면 자동으로 취소되는 코루틴 영역을 만든다. 따라서 LiveData값을 갱신하는 데에 관련된 Coroutines의 동작/취소를 ViewModel이 아닌, LiveData가 주체적으로 판단하게 된다. 이는 오동작을 예방하면서 동시에 컴퓨팅 자원 절약으로도 이어진다.
추가로, 코드 또한 더 간결해졌다. 이 장점은 ViewModel에서 뿐만 아니라 Activity에서도 그렇다. 아래 코드를 보자.
#2-3 MainActivity.kt에서 getUsers() 삭제
// package com.example.livedatabuilder
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
class MainActivity : AppCompatActivity() {
private lateinit var mainActivityViewModel: MainActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mainActivityViewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)
// mainActivityViewModel.getUsers()
mainActivityViewModel.users.observe(this, Observer { myUsers ->
myUsers?.forEach {
Log.i("interfacer_han", "name is ${it.name}")
}
})
}
}
/* Log 출력 결과
name is Park Bom
name is Park Sandara
name is CL
name is Gong Minji
*/
#1-5에서 LiveData를 observe하기 전에 초깃값을 할당하는 작업을 할 필요가 없어졌다. 왜냐하면 LiveData가 활성화되면 즉, LiveData가 담긴 Activity(여기선 MainActivity)의 LifecycleOwner의 상태가 활성화 상태면, LiveData Builder가 암시적으로 수행될 것이기 때문이다.
#3 주의점
"그렇다면, ViewModelScope.launch { ... }를 liveData { ... }로 몽땅 다 바꿔버리면 좋은 게 아닌가?"라는 생각이 들었다면 LiveData Builder를 잘못 이해하고 있는 것이다. liveData { ... }를 더 우월한, 업그레이드 버전의 ViewModelScope라고 생각하면 안 된다. 이 둘은 서로 그 용도가 다르다.
ViewModelScope는 이 게시글에서 보듯 CoroutineScope를 암시적으로 ViewModel의 생명주기에 종속되게 만드는 것에 목적을 둔다. 반면, LiveData Builder는 초기화에 Coroutine을 사용하는 경우의 LiveData를 간편하고 메모리효율적으로 초기화하는 것이 목적이다. 본 게시글은 초기화에 Coroutine을 사용하는 경우의 LiveData를 초기화할 때에 한정하여 ViewModelScope 외에 더 좋은 방법이 있다는 것을 알려주는 글이지, ViewModelScope를 LiveData Builder로 대체할 수 있음을 보이는 글이 아니다.
본 게시글에 쓰인 LiveData Builder를 살펴보면 뷰모델(#2-2)의 emit()이, 액티비티(#2-3)의 mainActivityViewModel.getUsers()를 대체하게 만들었고, ViewModelScope보다 메모리적으로 이점이 있는 CoroutineScope인 liveData { ... }를 사용하게 만들었다. 이게 전부다. 이 역할을 넘어 liveData { ... } 안에 해당 LiveData의 초기화 외 어떤 다른 코드를 넣으면 컴파일은 될지 몰라도 에러가 날 여지가 생기고 만다. 아니, 오히려 컴파일은 되니까 에러가 생겼을 때 문제점을 찾기가 더 어렵다. 정리하자면, 초기화에 Courotine을 사용하는 경우의 LiveData 외에는 전부 ViewModelScope를 쓴다고 생각하자.
#4 요약
초기화에 Coroutine을 사용하는 경우의 LiveData를 간편하고 메모리효율적으로 초기화한다.
#5 완성된 앱
'깨알 개념 > Android' 카테고리의 다른 글
[Android] Room - Entity, DAO, Database (0) | 2024.02.24 |
---|---|
[Android] Room - 기초, INSERT와 DELETE 연습 (0) | 2024.02.23 |
[Kotlin] Coroutines - LifecycleScope (0) | 2024.02.21 |
[Android] RecyclerView - Adapter에 인자(Argument) 전달 (0) | 2024.02.03 |
[Android] RecyclerView - 기초 (0) | 2024.02.02 |