경험의 기록

2021.05.13 - [Android/AAC, MVVM] - [Android] Room + LiveData + ViewModel + DataBinding 사용하여 MVVM 패턴 사용하기

 

[Android] Room + LiveData + ViewModel + DataBinding 사용하여 MVVM 패턴 사용하기

2021.04.19 - [안드로이드/AAC, MVVM] - [Android] 안드로이드 AAC & MVVM [Android] 안드로이드 AAC & MVVM 액티비티, 프래그먼트에 너무 많은 코드를 넣게 되면 점점 무거워져 다루기 힘들어지게 된다. 앱이 카..

hanyeop.tistory.com

안드로이드에서는 라이프사이클에 종속되어 있어 관리가 쉽고, 옵저버패턴을 구현할 수 있게 해주는 LiveData를 사용했다.

하지만 Flow는 LiveData 보다 여러 장점이 있기 때문에 Flow를 사용하는 것이 권장된다고 한다.

Flow를 살펴보면서 그 이유에 대해 알아보려고 한다.

 

https://kotlinlang.org/docs/flow.html#flows

 

Asynchronous Flow | Kotlin

 

kotlinlang.org

의 코드를 참고하였습니다.

 


🔴 Flow 란 ?

▶ 순차적으로 값을 배출하고 정상 또는 예외 처리하는 비동기 데이터 스트림

 

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️⃣ 스스로 라이프사이클을 알지 못한다.

 

  • Cold Stream : 하나의 소비자가 구독 시 생성되며, 소비할 때마다 생성되어 데이터가 발행된다. 
  • Hot Stream : 하나 이상의 소비자들이 구독할 수 있고, 데이터 발행이 시작되면 모든 소비자들에게 같은 데이터를 발행한다.

 

 

위 단점들을 보완하기 위해

 

1,2번 보완 => StateFlow, SharedFlow

3번 보완 => launchWhenStarted

 

를 사용할 수 있다.

 

StateFlow는 SharedFlow를 상속받고, SharedFlow는 Flow를 상속받는 관계이다.

 

또한 3번 문제를 보완하기 위해 Android Arctic fox 버전부터 데이터바인딩 시에 Flow를 LiveData와 동일하게 라이프사이클에 종속시킬 수 있다.

 

 

 

🔴 사용해보기

1. 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와 밀접하게 연결할 수 있다.

 

 

2. StateFlow

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)
            }
        }
    }
}

뷰모델에서는 LiveDatastateFlow로 바뀐 것 이외의 차이가 없다.

 

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

 

Asynchronous Flow | Kotlin

 

kotlinlang.org

또한 Flow는 여러 함수들을 사용하여 데이터를 다양하게 처리할 수 있다.

자세한 내용은 위 링크에서 확인할 수 있다.

 

 

3. SharedFlow

이제 마지막으로 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 생성 시에

 

  • replay : 새로운 구독자들에게 이전 이벤트를 몇개 방출할지 지정 (Integer)
  • extraBufferCapacity : 추가 버퍼를 몇개 생성할지 지정 (Integer)
  • onBufferOverflow : 버퍼 초과시 처리 여부 (DROP_OLDEST = oldest 데이터 drop)

 

와 같은 설정을 재정의 하여 사용할 수 있다.

 

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

 

GitHub - HanYeop/AndroidStudio-Practice2: (2021.05.20~) 안드로이드 학습 내용 저장소

(2021.05.20~) 안드로이드 학습 내용 저장소. Contribute to HanYeop/AndroidStudio-Practice2 development by creating an account on GitHub.

github.com

 

 


LiveData는 UI와 밀접한 관계이고, Flow는 데이터 통신과 밀접한 관계이기 때문에 직관적으로도 Flow를 사용하는 것이 좋고, 많은 데이터 처리에도 유리하다. 또한 이 글에서 서술하지 않은 여러 장점이 있겠지만, 크게 정리해보면

 

  1. 클린아키텍처 관점에서 LiveData는 플랫폼 종속적이므로 Domain 계층에 사용할 수 없지만 Flow는 사용할 수 있다.
  2. 결과를 필터링하는 등의 다양한 기능을 하는 함수들을 사용할 수 있다.

 

로 정리할 수 있다.

또한 Android Arctic fox 버전부터 Flow를 라이프사이클에 종속시킬수 있고, JetPack Compose 에서는 xml 에서 벗어난 선언형 프로그래밍으로 바인딩 개념이 점차 옅어지고 있으므로 LiveData는 이제 Flow로 대체가 가능할 것이라고 본다.

 

 

참고

https://developer.android.com/jetpack/guide?hl=ko 

https://kotlinlang.org/docs/flow.html#flows

반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading