안드로이드에서는 라이프사이클에 종속되어 있어 관리가 쉽고, 옵저버패턴을 구현할 수 있게 해주는 LiveData를 사용했다.
하지만 Flow는 LiveData 보다 여러 장점이 있기 때문에 Flow를 사용하는 것이 권장된다고 한다.
Flow를 살펴보면서 그 이유에 대해 알아보려고 한다.
https://kotlinlang.org/docs/flow.html#flows
의 코드를 참고하였습니다.
▶ 순차적으로 값을 배출하고 정상 또는 예외 처리하는 비동기 데이터 스트림
fun simple(): Flow<Int> = flow { // flow builder
for (i in 1..3) {
delay(100) // pretend we are doing something useful here
emit(i) // emit next value
}
}
fun main() = runBlocking<Unit> {
// Launch a concurrent coroutine to check if the main thread is blocked
launch {
for (k in 1..3) {
println("I'm not blocked $k")
delay(100)
}
}
// Collect the flow
simple().collect { value -> println(value) }
}
기본적인 Flow의 구조는 emit() 으로 데이터를 배출하고,
Collect 로 데이터를 받아오는 방식이다. 위 과정은 비동기로 처리된다.
위 과정을 살펴보면 마치 LiveData를 구독하여 관찰하는 것과 유사한 형태를 띈다는 것을 알 수 있다.
하지만 Flow는 Livedata와 달리 3가지 차이가 존재한다.
1️⃣ 상태(Value)를 가지지 않는다.
2️⃣ Cold Stream 방식이기 때문에, 연속으로 데이터를 처리할 때마다 Flow를 호출하게 된다.
3️⃣ 스스로 라이프사이클을 알지 못한다.
위 단점들을 보완하기 위해
1,2번 보완 => StateFlow, SharedFlow
3번 보완 => launchWhenStarted
를 사용할 수 있다.
StateFlow는 SharedFlow를 상속받고, SharedFlow는 Flow를 상속받는 관계이다.
또한 3번 문제를 보완하기 위해 Android Arctic fox 버전부터 데이터바인딩 시에 Flow를 LiveData와 동일하게 라이프사이클에 종속시킬 수 있다.
비교를 위하여 우선 LiveData를 사용해보자.
class MyViewModel : ViewModel() {
private val _liveData = MutableLiveData(100)
val liveData: LiveData<Int> = _liveData
fun changeLiveData(){
viewModelScope.launch {
repeat(10){
_liveData.value = _liveData.value?.plus(1)
delay(1000L)
}
}
}
}
뷰모델에서 LiveData를 생성하고
10번 반복해서 값을 바꿔주는 ChangeLiveData 메소드를 정의한다.
class MainActivity : AppCompatActivity() {
private val myViewModel : MyViewModel by viewModels()
private lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 데이터바인딩
binding = DataBindingUtil.setContentView(this,R.layout.activity_main)
// 뷰모델 연결
binding.vm = myViewModel
// 1. LiveData
myViewModel.liveData.observe(this){
binding.textView.text = it.toString()
Log.d("test5", "LiveData : $it")
}
}
}
메인액티비티에서 옵저버패턴을 통해 LiveData를 관찰하고, 바인딩을 통해 뷰를 갱신해준다.
잘 작동하는 것을 알 수 있다.
또한 LiveData는 데이터바인딩을 통해
xml 에서 LiveData를 연결하여
class MainActivity : AppCompatActivity() {
private val myViewModel : MyViewModel by viewModels()
private lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 데이터바인딩
binding = DataBindingUtil.setContentView(this,R.layout.activity_main)
// 뷰모델 연결
binding.vm = myViewModel
// 뷰모델을 LifeCycle 에 종속시킴, LifeCycle 동안 옵저버 역할을 함
binding.lifecycleOwner = this
}
}
lifecycleOwner를 정의해주면 별도로 UI를 갱신하는 코드를 작성하지 않아도
UI가 갱신되어 UI와 밀접하게 연결할 수 있다.
StateFlow는 Flow와 달리 상태(Value)를 가지고, Hot Stream 이다.
또한 초기값을 가진다.
StateFlow를 사용하여 LiveData에서 작성한 코드와 동일한 기능을 하는 코드를 작성해보자.
class MyViewModel : ViewModel() {
private val _stateFlow = MutableStateFlow(200)
val stateFlow = _stateFlow.asStateFlow()
fun changeStateFlow(){
viewModelScope.launch {
repeat(10){
_stateFlow.value = _stateFlow.value.plus(1)
delay(1000L)
}
}
}
}
뷰모델에서는 LiveData만 stateFlow로 바뀐 것 이외의 차이가 없다.
class MainActivity : AppCompatActivity() {
private val myViewModel : MyViewModel by viewModels()
private lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 데이터바인딩
binding = DataBindingUtil.setContentView(this,R.layout.activity_main)
// 뷰모델 연결
binding.vm = myViewModel
// 2. StateFlow
lifecycleScope.launchWhenStarted {
myViewModel.stateFlow.collectLatest {
binding.textView2.text = it.toString()
Log.d("test5", "StateFlow : $it")
}
}
}
}
메인액티비티에서는
Flow는 라이프사이클을 알지 못하기 때문에 lifeCycleScope에서 launchWhenStarted 를 사용해 라이프사이클 동안 구독하도록 하고,
collectLatest를 통해 마지막에 변경된 값을 관찰해 갱신해서 보여주도록 한다.
동일하게 작동하는 것을 알 수 있다.
LiveData는 Flow 와 달리 데이터바인딩과 긴밀하게 연결할 수 있는 장점이 있었으나,
Android Arctic fox 버전부터 LiveData처럼 Flow도 데이터바인딩을 통해 직접 바인딩해줄 수 있다.
class MainActivity : AppCompatActivity() {
private val myViewModel : MyViewModel by viewModels()
private lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 데이터바인딩
binding = DataBindingUtil.setContentView(this,R.layout.activity_main)
// 뷰모델 연결
binding.vm = myViewModel
// 뷰모델을 LifeCycle 에 종속시킴, LifeCycle 동안 옵저버 역할을 함
binding.lifecycleOwner = this
}
}
LiveData 처럼 xml에서 바인딩해주고 lifecycleOwner만 정의해주면
LiveData와 동일하게 UI가 자동으로 갱신된다.
위 과정을 살펴보면, StateFlow는 LiveData와 차이가 없어보인다.
하지만 클린아키텍처 관점에서 큰 차이가 있다.
클린아키텍처에 대해 간단히 서술하자면
안드로이드에서의 클린아키텍처의 구조는
1️⃣ UI(Presentation) 계층 - View, ViewModel...
2️⃣ Domain 계층 - Repository (interface), UseCase...
3️⃣ Data 계층 - Repository (implement), DateSource
로 나뉜다. 의존성 관계를 명확히하여 유지보수, 동작구조 파악을 쉽게 하기 위함으로
UI -> Domain
Data -> Domain
의 의존성을 가진다.
Domain 계층은 UI, Data 계층으로의 의존성을 가지면 안되기에 순수한 Kotlin 코드로만 구성되어야 한다.
즉, Repository이 속한 Domain계층에서 LiveData 를 사용하는 것은 구조상 어긋난다.
LiveData는 안드로이드 플랫폼의 기능이기 때문에 플랫폼 종속성을 가지고 있고, UI 계층에서 관리되기 때문이다.
따라서 UI계층이 아니라면 LiveData대신 Flow를 사용해야만 한다.
https://kotlinlang.org/docs/flow.html
또한 Flow는 여러 함수들을 사용하여 데이터를 다양하게 처리할 수 있다.
자세한 내용은 위 링크에서 확인할 수 있다.
이제 마지막으로 SharedFlow에 대해 알아보자.
StateFlow의 일반화된 버전으로 볼 수 있으며, SharedFlow 또한 Flow 와 달리 Hot Stream 이다.
초기값을 가지지 않고, 여러 설정이 가능하기 때문에 주로 이벤트 처리에 사용할 수 있다.
class MyViewModel : ViewModel() {
private val _sharedFlow = MutableSharedFlow<String>(
replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val sharedFlow = _sharedFlow.asSharedFlow()
fun changeSharedFlow(){
viewModelScope.launch {
_sharedFlow.emit("안녕하세요")
}
}
}
sharedFlow 생성 시에
와 같은 설정을 재정의 하여 사용할 수 있다.
class MainActivity : AppCompatActivity() {
private val myViewModel : MyViewModel by viewModels()
private lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 데이터바인딩
binding = DataBindingUtil.setContentView(this,R.layout.activity_main)
// 뷰모델 연결
binding.vm = myViewModel
// 3. SharedFlow
lifecycleScope.launchWhenStarted {
myViewModel.sharedFlow.collect {
Toast.makeText(this@MainActivity, it, Toast.LENGTH_SHORT).show()
}
}
}
}
이제 collect로 방출된 결과를 받아와 이벤트를 발생시킬 수 있다.
전체 코드는 여기서 확인할 수 있습니다.
https://github.com/HanYeop/AndroidStudio-Practice2/tree/master/FlowEx
LiveData는 UI와 밀접한 관계이고, Flow는 데이터 통신과 밀접한 관계이기 때문에 직관적으로도 Flow를 사용하는 것이 좋고, 많은 데이터 처리에도 유리하다. 또한 이 글에서 서술하지 않은 여러 장점이 있겠지만, 크게 정리해보면
로 정리할 수 있다.
또한 Android Arctic fox 버전부터 Flow를 라이프사이클에 종속시킬수 있고, JetPack Compose 에서는 xml 에서 벗어난 선언형 프로그래밍으로 바인딩 개념이 점차 옅어지고 있으므로 LiveData는 이제 Flow로 대체가 가능할 것이라고 본다.
참고