본문 바로가기

안드로이드 Android

안드로이드 개발 (36) 심플한 Flow Sample

마침, 최근에 리딩중인 사이드 프로젝트에서 과제로 Flow 를 사용하는 법을 간단하게 정리하기로 하여서,

이 글을 작성하게 되었습니다.

[개요]

예전 Jatpack LiveData가 등장하였을 때 더 이상 observe 한 객체의 라이프사이클 처리를 개발자가 직접적으로 처리할 필요가 없어서 매우 편해짐에 따라 이후 안드로이드 개발에서 LiveData를 거의 필수적으로 사용하게 되었습니다. 하지만 LiveData 의 경우 LifeCycle 종속적이라는 문제가 생깁니다.


대표적으로 따라 data Layer, Domain Layer 에서 Room 라이브러리를 사용할 때 반환하는 객체가 LiveData<T> 일 경우 import androidx. 종속이 생깁니다. 

 

또한 LiveData는 간편함을 제공하는 훌륭한 data holder 지만, Reactive programming 스럽게 코드를 짤 때 데이터를 변환하는 것이 map, switchMap이 다라고 할정도로 간단한 기능만을 제공하였습니다. (LiveData는 안드로이드 개발자가 LifeCycle 처리를 직접적으로 하지 않는것에 더 집중하였기 때문이라고 생각합니다.)

 

그래서 Google에서 only Kotlin을 추구하고 있고, Kotlin에서 지원하는 스마트한 비동기 처리인 Coroutine 을 활용하는 방법을 공식적으로 추가로 지원하기 시작합니다. (단점으로 Java의 활용도를 생각하지 않는 점에서 Java는 많이 배제가 되고 있습니다.)

Coroutine에 데이터 스트림을 만드는 Flow 가 있습니다. 생산자, 중개자(데이터 변환), 소비자 3단계로 나뉘어서 사용할 수 있게되며 기본적으로 Cold Stream 으로 되어있어 생산자, 중개자(옵션) 에서 방출하는 데이터를 준비시켜놓고, 1명의 소비자가 발생시 그 소비자에게만 데이터가 방출 됩니다.

이런 식으로 수도꼭지를 틀고 닫듯이 사용하는것이 Flow 입니다. 하지만 Cold Stream 특성이 사실 고정된 값을 보내기만 합니다. 그래서 State Pattren(State에 따라 UI에 반영하는) 을 주로 사용하는 클라단에서 사용하기 매우 불편한 상황입니다. 그래서 Flow를 Hot Stream 으로 사용할 수 있도록 State Flow,  Shared Flow를 별도로 지원 합니다.

 

[과제 설명]

 

공통 사항

사이드 팀 개발 인원 각자 개인 블로그에 Android Flow 관련 코드를 작성하여,  때 제출합니다. 공통적으로 아래 상황에서 3가지 주제를 진행합니다.

//Blog <- data ,domain 에서 사용
data class Blog(
   val title:String ,
   val content:String,
   // 블로그 출처
   val type:???
)

UiBlog <- view, viewModel 사용
data class UiBlog(
   val blog:Blog,
   val isSelected:Boolean
)

/**data/domainModule 에서 ui 모듈로 데이터 이동시 
*ViewModel에서 UiBlog로 변환하여 사용하기*/

 

 

1) Flow를 이용하여 데이터 상태에 따라 버튼 활성화 비활성화 변경하기

//예시: 맛집을 추가할 때 필수 항목을 적어야 완료버튼이 활성화 한다는 가정
/**예시는 여러분들이 마음대로 만들어내셔서 하면 좋을것 같습니다.*/
class ViewModel(val useCase:UseCase...){
   val requireDatas:StateFlow<T>()..
   val enableBtn = requireDatas.map{..}.filter{..}
or 
   val title:StateFlow<String>()..
   val image:StateFlow<Image>()...
   val enableBtn = title.combine(image){...}...
}
  • 다양한 예시를 만들어서 많은 코드를 작성 할수록 좋습니다

2) Flow에서 Error Catch 및 onCompletion() 를 이용한 예제

// 예시: 맛집 후기(블로그) 목록을 조회할 때 페이징처리를 할때, 페이징 도중에 에러가 났는지 체크
// 와 마지막 페이지의 마지막 데이터를 조회할때
class ViewModel(val useCase:UseCase ...){
   val blog:StateFlow<UiBlog>() = 
useCase.toUiBlogModel.error{..}
                     .onCompetion{ 
                       if(isLast){ 
                         showToast("마지막 페이지 입니다.")
                       }
                    }
}

3) StateFlow, SharedFlow, Chnnel 이 3가지를 활용한 코드 작성

//예시: StateFlow 를 통해서 아이템 목록 Ui 보여주려고 할때 등등.
val uiState:StateFlow<T>...
Activity{ viewModel.uiState.collect { ... }}

//예시: SharedFlow 를 이용해서 이벤트 처리 하려고 할때 등등..
val uiEvent:SharedFlow<T>...

//예시: Channel 를 이용해서 이벤트 처리 하려 할때 등등..
val event:Channel<T>...

위 3가지 숙제를 위해 추가 설정을 임의로 붙이셔도 상관 없습니다.

 

[과제 제출]

전체 코드: https://github.com/LonerStayle/SimpleFlowSample

 

GitHub - LonerStayle/SimpleFlowSample: 사이드 프로젝트 과제 제출용

사이드 프로젝트 과제 제출용. Contribute to LonerStayle/SimpleFlowSample development by creating an account on GitHub.

github.com

 

1) Flow를 이용하여 데이터 상태에 따라 버튼 활성화 비활성화 변경하기

제출: 네이버 , 다음 Api를 통해 각각 데이터를 불러와서 UI로 보여주고, 각각 아이템마다 선택 할 수 있는 체크박스가 있습니다. 어떤 아이템을 클릭했는지에 따라 버튼 활성화 여부가 달라집니다.

//1번째 숙제 화면에서만 사용하는 FirstHomeWorkUiBlogState ui모델을 정의하여 사용했습니다.
private val _uiBlogState = MutableStateFlow(FirstHomeWorkUiBlogState.initData)
val firstHomeWorkUiBlogState: StateFlow<FirstHomeWorkUiBlogState> = _uiBlogState

 

- 네이버 블로그를 선택 했을 때

  val completeOnlyNaverSelect: StateFlow<Boolean> = firstHomeWorkUiBlogState.map {
        val resultList = it.uiBlogGroup.value.filter { uiBlog ->
            uiBlog.isSelected && uiBlog.blog.type == BlogType.Naver
        }
        resultList.isNotEmpty()
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)

-------------------------------------------------------------------------------------------------------------------------------------------

 

 

- 다음 블로그를 선택 했을 때

val completeOnlyDaumSelect: StateFlow<Boolean> = firstHomeWorkUiBlogState.map {
        val resultList = it.uiBlogGroup.value.filter { uiBlog ->
            uiBlog.isSelected && uiBlog.blog.type == BlogType.Daum
        }
        resultList.isNotEmpty()
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)

------------------------------------------------------------------------------------------------------------------

 

 

- 다음, 네이버 블로그 둘다 선택했을 때

  val completeComplexSelect: StateFlow<Boolean> = firstHomeWorkUiBlogState.map { uiBlog ->
        val selectUiBlogs = uiBlog.uiBlogGroup.value.filter { it.isSelected }
        selectUiBlogs.find { it.blog.type == BlogType.Naver } != null
                && selectUiBlogs.find { it.blog.type == BlogType.Daum } != null
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)

-----------------------------------------------------------------------------------------------------

 

 

- 블로그 전체 4개이상 선택 했을 때, 

val completeFourCountSelect: StateFlow<Boolean> = firstHomeWorkUiBlogState.map {
        it.uiBlogGroup.value.filter { it.isSelected }.size >= 4
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)

2) Flow에서 Error Catch 및 onCompletion() 를 이용한 예제

제출: onCompletion()은 Job이 완료 되어야 실행 됩니다. flow는 collect를 하고 있어서 잠시 테스트를 위해 onPause()에서 job.cancle 처리를 해주었습니다.

 

   private val job = lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            viewModel.blogList.onCompletion {
                viewModel.showToast(SecondHomeWorkViewModel.ShowMsg("Job 처리 완료."))
            }.collect {
                binding.rvBlogs.adapter = SecondHomeWorkAdapter(it)
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        setAdapter()
        ..
    }
    
     private fun setAdapter() {
        job.start()
    }
    
     override fun onPause() {
        ...
        job.cancel()
    }

 

3) StateFlow, SharedFlow, Chnnel 이 3가지를 활용한 코드 작성

제출: 1,2번 과제에 녹아들어있으므로 따로 작성하지 않았습니다.