Android/Flow

[Android] Flow와 StateFlow 사용해보기

O_Gyong 2024. 3. 26.

LiveData와 Flow

LiveData는 안드로이드 컴포넌트의 생명 주기와 결합되어 있어 UI 데이터를 관리하기 편하다는 장점 때문에 자주 사용한다. 하지만 아키텍처의 관점에서 볼 때 LiveData는 비동기 데이터 스트림을 처리하도록 설계되지 않았고, 안드로이드 프레임워크에 의존하기 때문에 도메인 계층과 데이터 계층에서 사용하기에는 적합하지 않다.

 

반면 Flow의 경우 순수 코틀린 언어로 되어있을 뿐만 아니라 비동기로 계산이 가능한 데이터 스트림이다. 이런 이유로 안드로이드에서는 데이터 계층에서 Flow를 사용한 다음 asLiveData()를 통해 ViewModel에서 LiveData로 변환하는 것을 추천하고 있다. (참고)

 

StateFlow가 등장하면서 위와 같은 작업을 할 필요가 없어졌다. StateFlow는 상태 값을 Observe하여 UI를 관리하는 데 사용할 수 있다. StateFlow가 LiveData와 유사한 점이 많고 위에 언급한 내용으로 대체하여 사용한다지만 생명 주기를 인식하지 않는다는 큰 차이점이 있어서 각 도구를 선택할 때 충분히 고려해야 할 것 같다. (참고)


Flow 사용해보기

Flow는 안드로이드 서드 파티 라이브러리인 다수의 Jetpack 라이브러리에 통합됐다. Room을 사용할 때 DAO에서 Flow 유형을 지정하여 실시간 업데이트를 받을 수 있다. LiveData 대신 Flow와 StateFlow을 사용해봤다.


/**
 * SampleDao
 */

@Dao
interface FlowDao {
    @Query("SELECT * FROM flow_db")
    fun selectFlow(): Flow<List<String>>
    ...
}

@Dao
interface StateFlowDao {
    @Query("SELECT * FROM state_flow_db")
    fun selectStateFlow(): Flow<List<String>>
    ...
}

Dao에서 반환 타입을 Flow로 하면 flow_db와 state_flow_db 테이블에 변화가 생길때 마다 데이터베이스의 새로운 값을 전달한다. 이를 통해 실시간으로 업데이트를 할 수 있다.


@Singleton
class SampleRepository @Inject constructor(
    private val flowDao: FlowDao,
    private val stateFlowDao: StateFlowDao
) {

    fun selectFlow() : Flow<List<String>> = flowDao.selectFlow()
    fun selectStateFlow() : Flow<List<String>> = stateFlowDao.selectStateFlow()
 
    ...
}

※ StateFlow를 사용할 때 Repository에서 Flow를 StateFlow로 변환해야 하지 않을까 고민을 했는데, StateFlow는 UI 상태를 관리하기 때문에 ViewModel에서 하는 것이 더 적합한 것 같다. 그래서인지 안드로이드의 codelab을 봐도 ViewModel에서 처리하고 있었다.

 

/**
 * MainViewModel
 */
@HiltViewModel
class MainViewModel @Inject constructor(private val sampleRepository: SampleRepository) : ViewModel() {

    /**
     * Flow
     */
    fun selectFlow() : Flow<List<String>> = sampleRepository.selectFlow()

    /**
     * StateFlow
     */
    // Case 1
    private var _stateFlowData = MutableStateFlow<List<String>>(emptyList())
    val stateFlowData: StateFlow<List<String>> = _stateFlowData

    fun selectStateFlow() {
        viewModelScope.launch {
            sampleRepository.selectStateFlow().collectLatest {
                _stateFlowData.value = it
                // _stateFlowData.emit(it)
            }
        }
    }
    
    // Case 2
    fun stateFlowData() = sampleRepository.selectStateFlow().stateIn(
        scope = viewModelScope,
        started = SharingStarted.Lazily,
        initialValue = emptyList()
    )
    
    ...
}

◾ Case 1
Flow 값을 collectLatest로 수집하여 그 값을 StateFlow에 옮겼다. emit()을 사용하여 _stateFlowData에 값을 전달할 수도 있다.

StateFlow는 초기 값이 필요하기 때문에 emptyList로 초기화를 한다.
(collectLatest는 데이터를 수집하는 도중에 새로운 값이 수집되면 기존의 코드 블록을 취소하고 다시 시작하여 연산이 쌓이는 것을 막는다.)

◾ Case 2

Flow에는 stateIn()이라는 기능이 있다. stateIn은 Cold인 Flow를 Hot인 StateFlow로 변환하는 함수다.

- scope
  해당 StateFlow의 생명주기를 지정하는 코루틴 스코프

- started
  startedSharingStarted 클래스의 인스턴스로 StateFlow를 언제부터 공유할 것인지를 지정
  Lazily는 첫 번째 구독자가 나타날 때 시작하여 멈추지 않는 속성

- initialValue
  StateFlow의 초기 값

    /**
     * MainActivity
     */
    override fun onCreate(savedInstanceState: Bundle?) {
		...
        
        mViewModel.selectStateFlow() // Case 1로 할 경우

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Flow
                launch {
                    mViewModel.selectFlow().collectLatest {
                        println("     Flow: $it")
                        mBinding.tvFlow.text = it.toString()
                    }
                }

                // StateFlow
                launch {
                    mViewModel.stateFlowData.collectLatest {
                        println("StateFlow: $it")
                        mBinding.tvStateFlow.text = it.toString()
                    }
                }
            }
        }
    }

lifecycleScope는 Lifecycle 객체에서 정의된다. 해당 스코프로 실행된 코루틴은 생명 주기가 끝나면 함께 제거된다. Flow는 생명 주기를 알지 못하는 특성으로 LiveData와 비교되었는데, repeatOnLifecycle은 생명 주기의 상태에 따라서 동작하는 스코프를 만들어 낸다.

참고로 위 상황처럼 repeatOnLifecycle에서 여러 flow 값을 collect 해야하는 상황이라면 다른 스코프를 만들어줘야 한다. (참고

 


Flow와 StateFlow는 같아 보이지만 로그를 확인해 보면 차이가 있다. 최신 데이터를 입력했을 때 직전 데이터와 중복되는 경우 StateFlow는 collect를 하지 않는다. StateFlow를 사용할 때 이런 부분을 생각해두자.


전체 코드

 

Android_Study/Flow Sample at master · OhGyong/Android_Study

안드로이드 개발 공부. Contribute to OhGyong/Android_Study development by creating an account on GitHub.

github.com

 

댓글