Android Weekly

Android Weekly #-601 "Flow collectors의 leak 문제"

베블렌 2023. 12. 18. 09:25

12월 3주차에는 Kotiln Weekly와 중복된 글이 많았습니다. 그래프 라이브러리, compose 애니메이션 등등 이 있었습니다. 눈에 띄는 글로는 Gemini와 관련된 글이 있었고, 이 글은 The ViewModel’s leaked Flow collectors problem 글입니다.

 

https://medium.com/adidoescode/the-viewmodels-leaked-flow-collectors-problem-239a327f4b56

 

The ViewModel’s leaked Flow collectors problem

I love Kotlin Flow, specially when used to chain the data from your data layer (or use cases if you got them) to your Ui. When done right…

medium.com

 

 

 

소개

이 글은 Kotlin Flow를 ViewModel에서 UI로 연결하는 방법과, 이 과정에서 발생할 수 있는 "유출된 Flow 수집기(Leaked Flow Collectors)" 문제에 대해 설명하고 있습니다. 제가 쓰는 코드에도 적용되는 문제여서 작성하게 되었습니다.

 

 

내용

  • Flow 사용의 장점: Kotlin Flow는 데이터를 데이터 레이어 또는 유스케이스에서 UI로 연결할 때 효과적입니다. 복잡한 화면도 데이터 변경에 따라 자동으로 업데이트 되어 코드의 이해와 유지 관리가 쉬워집니다.
  • Flow.collect() 사용의 위험성: ViewModel에서 Flow.collect()를 사용할 때는 주의가 필요합니다. 특히 "핫 Flow"에서는 유출된 수집기 문제가 발생할 수 있습니다.

문제

  • 유출된 수집기 문제: refresh() 함수와 같은 기능을 사용하여 데이터를 새로고침할 때마다 새로운 Flow 수집기가 생성되고, 이들은 ViewModel이 파괴될 때까지 지속됩니다. 이는 리소스 낭비와 성능 저하를 초래할 수 있습니다.

해결 방법

 

1. 기본 시나리오 - stateIn() 연산자 사용:

class MyVeryBasicViewModel(private val repository: Repository): ViewModel {
    val uiState = repository.getDataFlow()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), UiState())
}
data class UiState(val text: String? = null)

 

stateIn() 연산자를 사용하여 Flow를 StateFlow로 변환하고, 데이터 소스가 UI에 실제로 수집될 때까지 데이터 요청을 연기합니다.

 

2. UI로부터 새로고침 요청 시나리오:

class MyStandardViewModel(private val repository: Repository): ViewModel {
    private val trigger = MutableSharedFlow<Unit>()
    val uiState = trigger.flatMapLatest { _ ->
        repository.getDataFlow().map { it.mapToUiState() }
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), UiState())

    fun refresh() {
        viewModelScope.launch { trigger.emit(Unit) }
    }
}
data class UiState(val text: String)

 

MutableSharedFlow를 사용하여 UI로부터 데이터 새로고침 요청을 받고, flatMapLatest()를 통해 새로운 Flow를 생성합니다.

 

내 코드

private val _eventDetail = MutableStateFlow(getDefaultEventDetailData())
val eventDetail: StateFlow<EventIndexListResponseDTO> = _eventDetail   
   
   
   // 이벤트 자세히 보기
    fun getEventDetail(index: String) {
        viewModelScope.launch {
            eventRepository.findEvent(index).collect { response ->
                if (response.isSuccessful) {
                    _eventDetail.emit(response.body()!!)
                } else {
                    _errorMessage.emit(eventRepository.lastError ?: "알 수 없는 에러")
                }
            }
        }
    }

 

코드 변경

private val _eventDetail = MutableStateFlow(getDefaultEventDetailData())
val eventDetail: StateFlow<EventIndexListResponseDTO> = _eventDetail

private val refreshTrigger = MutableSharedFlow<String>()

init {
    viewModelScope.launch {
        refreshTrigger.flatMapLatest { index ->
            eventRepository.findEvent(index)
        }.collect { response ->
            if (response.isSuccessful) {
                _eventDetail.emit(response.body()!!)
            } else {
                _errorMessage.emit(eventRepository.lastError ?: "알 수 없는 에러")
            }
        }
    }
}

fun getEventDetail(index: String) {
    refreshTrigger.emit(index)
}

 

이 방식에서는 refreshTrigger SharedFlow를 사용하여 새로운 요청을 트리거합니다. flatMapLatest는 이전에 수행 중인 작업을 자동으로 취소하고 새로운 요청에 대한 Flow를 시작합니다. 따라서 getEventDetail 함수를 호출할 때마다 새로운 요청이 시작되고, 이전 요청은 자동으로 취소됩니다.

 

물론 이 코드는 게시글 자세히보기로 한 번만 호출되는 경우라 변경은 불필요하겠지만, 새로고침등의 가능성도 존재하니 알아두면 좋을것 같습니다.

 

 

정리

Kotlin Flow의 강력한 기능을 활용하여 ViewModel에서의 데이터 관리를 최적화하고, 유출된 Flow 수집기 문제를 방지할 수 있습니다. 이를 통해 더 안전하고 효율적인 애플리케이션을 구현할 수 있습니다.

 

 

 

https://androidweekly.net/

 

Android Weekly - Free weekly Android & Kotlin development newsletter

Android Weekly - Free weekly Android & Kotlin development newsletter

androidweekly.net