지난번에 mutableStateOf을 살펴보다가, 공식문서의 다음과 같은 글을 읽었다.
Compose doesn't require that you use MutableState to hold state; it supports other observable types. Before reading another observable type in Compose, you must convert it to a State so that composables can automatically recompose when the state changes.
Compose ships with functions to create State from common observable types used in Android apps. Before using these integrations, add the appropriate artifact(s) as outlined below:
- Flow: collectAsStateWithLifecycle()
- collectAsStateWithLifecycle() collects values from a Flow in a lifecycle-aware manner, allowing your app to conserve app resources. It represents the latest emitted value from the Compose State. Use this API as the recommended way to collect flows on Android apps.
요컨데 다른 observable도 있다는 것이다.
그 중에 mutableStateFlow와 LiveData를 발견했고, 둘이 무엇인지 알아보려고 한다.
뭔 소리야? 다행히도 문서가 요약을 해준다.
Flow는 값의 비동기 시퀀스입니다.
Flow는 네트워크 요청, 데이터베이스 호출, 기타 비동기 코드와 같은 값을 생성하는 비동기 작업에서 한번에 한번씩(한번에 모두를 생성하는 대신) 값을 생성합니다. Flow는 API 전체에서 코루틴을 지원하므로 코루틴을 사용하여 Flow를 변환할 수 있습니다.
이젠 알 것 같기도 하고 모르는 것 같기도 하다. 문서를 좀 더 살펴보자
fun makeFlow() = flow {
println("sending first value")
emit(1)
println("first value collected, sending another value")
emit(2)
println("second value collected, sending a third value")
emit(3)
println("done")
}
scope.launch {
makeFlow().collect { value ->
println("got $value")
}
println("flow is completed")
}
결과는 다음과 같다.
sending first value
got 1
first value collected, sending another value
got 2
second value collected, sending a third value
got 3
done
flow is completed
위 코드에서
1. flow는 emit이 호출될 때 collect가 값을 받을 때까지 중단suspend한다.
2. emit의 값은 collect에서 출력한다.
3. 처리가 끝나면 중단된 부분에서 재개한다.
4. flow 빌더가 완료되면 collect는 "flow is completed"를 출력한다.
이렇게 collect를 통해서 flow의 값을 받아갈 수 있다. flow는 collect가 호출되면 실행된다. flow를 정의함으로써 실행되지 않음에 주의하자.
하지만 재생성 횟수와는 상관 없이 데이터에 계속 접근해야하는 경우가 있다. collect로 값을 가져오는 게 아닌 일종의 버퍼가 필요할 때, StateFlow를 사용한다.
이를 위해 StateFlow가 있다.
StateFlow는 컬렉터가 없더라도 데이터를 보관. 또 모든 업데이트를 받아 최신 데이터를 저장
관찰 가능한 변경 가능 상태를 유지해야하는 클래스에 아주 적합
상태를 업데이트하고 흐름에 전송할 때 MutableStateOf을 사용
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
// 다른 클래스에서 상태를 업데이트하는 걸 방지
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
// UI가 상태를 업데이트하기 위해 이 StateFlow에서 값을 가져감
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
// 최신 뉴스로 View를 업데이트
// MutableStateFlow에 값을 작성
// flow에 값을 더하고 관련 있는 모든 컬렉터에 업데이트
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
// 다른 상태의 스크린을 표현
sealed class LatestNewsUiState {
data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
data class Error(val exception: Throwable): LatestNewsUiState()
}
해당 viewModel은 다음과 같이 적용할 수 있다.
class LatestNewsActivity : AppCompatActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
...
// 라이프사이클 범위에서 코루틴 시작
lifecycleScope.launch {
// repeatOnLifecycle은 라이프사이클이 STARTED거나 그 이상일 때
// 새 코루틴에 해당 블록을 실행, STOPPED일 때 취소
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Flow를 시작하고 계속 값을 관찰
latestNewsViewModel.uiState.collect { uiState ->
// 새 값을 받았을 때
when (uiState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
}
}
}
}
여기서 StateFlow와 많이 비교되는 것이 LiveData다.
수명주기가 STARTED나 RESUMED 상태의 관찰자에게만 업데이트 정보를 알린다. 이 덕에 메모리 누수가 없고, 알아서 최신 데이터를 보장해준다.
빠른 이해를 위해 사용 방법을 알아보자.
class NameViewModel : ViewModel() {
// LiveData 생성
val currentName: MutableLiveData<String> by lazy {
MutableLiveData<String>()
}
// Rest of the ViewModel...
}
class NameActivity : AppCompatActivity() {
private val model: NameViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 기타 코드...
// 관찰자를 생성
val nameObserver = Observer<String> { newName ->
// TextView의 UI를 변경
nameTextView.text = newName
}
// 이 액티비티를 LifecycleOwner이자 관찰자로 전달, LiveData 관찰
model.currentName.observe(this, nameObserver)
}
}
아래와 같은 식으로 업데이트되면 nameTextView가 업데이트된다.
button.setOnClickListener {
val anotherName = "John Doe"
model.currentName.setValue(anotherName)
}
문서를 통틀어 보면 가장 큰 차이점은 두 개다.
1. StateFlow는 초기값을 반드시 생성해야한다.
2. LiveData는 Observer를 반드시 생성해야한다.
어느 것이 더 낫다! 는 지금으로는 판단하기가 힘들다. 다만 글들을 찾아보면 Flow로 LiveData같은 동작을 할 수 있다고 한다. (더불어 LiveData는 Flow로 대체할 수 있지만 Flow는 LiveData로 대체할 수 없다는 글이 많다.... (참고글: 스택오버플로우의 LiveData vs StateFlow)
이 점은 좀 더 공부해보고 이야기하려고 한다
출처:
- https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/
- https://developer.android.com/codelabs/advanced-kotlin-coroutines#7
- https://medium.com/hongbeomi-dev/%EC%A0%95%EB%A6%AC-%EC%BD%94%ED%8B%80%EB%A6%B0-flow-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-android-dev-summit-2021-3606429f3c5f
- https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ko
- https://developer.android.com/topic/libraries/architecture/livedata?hl=ko
- https://velog.io/@ghddbwns9808/Android-Kotlin-Coroutine%EA%B3%BC-Flow-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B04-State-Flow