LiveData 에서 Kotlin Flow 로 마이그레이션하기

eunsong·2024년 2월 26일

Android

목록 보기
3/9

[정독]
https://medium.com/androiddevelopers/migrating-from-livedata-to-kotlins-flow-379292f419fb

LiveData 는 2017년에 우리에게 필요한 것이였다. Observer 패턴은 우리의 삶을 더 쉽게 만들어 주었지만 RxJava 같은 옵션은 비기너에겐 매우 복잡했다.

아키택쳐 컴포넌트 팀은 Android 를 위해 설계된 매우 독창적인 observable data holder 클래스인 Livedata를 만들었었다. 이것은 매우 쉽게 시작할수 있도록 단순하였고, 보다 복잡한 반응형 stream 케이스엔 RxJava 사용으로 둘사이의 통합 이점을 활용할 것을 권장되었습니다.

DeadData?

LiveData는 여전히 자바 개발자, 비기너, 간단한 상황을 위한 솔루션이다.

나머지의 경우, 코틀린 Flow로 이동하는 것이 좋다. Flow는 여전히 가파른 학습 곡선을 가지고 있다. 그러나 jetbrain이 지원하는 코틀린 언어의 일부는 반응형 모델과 잘 맞는 Compose 가 다가오고있다.

이제 우린 Android UI에서 flow를 수집하는 더 안전한 방법이 있으므로 완전한 마이그레이션 가이드를 만들 수 있습니다.

이 게시물에서는 flow를 뷰에 노출하는 방법, flow를 수집하는 방법 그리고 특정 요구에 맞는 대처를 배울 수 있습니다.

Flow: Simple things are harder and complex things are easier

LiveData는 한 가지를 잘 수행했다. 최신 값을 캐싱하고 Android 의 생명주기를 이해하면서 데이터를 노출 시켰습니다. 나중에 우리는 코루틴을 시작하고 복잡한 변환을 생성할 수 있다는 것도 알게 되었지만, 이는 좀더 복잡했다.

일부 LiveData 패턴과 flow 패턴을 살펴보자.

#1: Expose the result of a one-shot operation with a Mutable data holder

Mutable data holder 를 사용하여 일회성 작업 결과를 노출한다.

이것은 코루틴 결과로 상태 홀더를 변경하는 고전적인 패턴이다.

class MyViewModel {
    private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)
    val myUiState: LiveData<Result<UiState>> = _myUiState

    // Load data from a suspend fun and mutate state
    init {
        viewModelScope.launch { 
            val result = ...
            _myUiState.value = result
        }
    }
}

Flow 에서도 동일한 작업을 수행하기 위해 (Mutable)StateFlow 를 사용합니다.

class MyViewModel {
    private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)
    val myUiState: StateFlow<Result<UiState>> = _myUiState

    // Load data from a suspend fun and mutate state
    init {
        viewModelScope.launch { 
            val result = ...
            _myUiState.value = result
        }
    }
}

StateFlow는 LiveData와 가장 가까운 SharedFlow(Flow의 특별한 유형)의 특별한 종류입니다.

  • 항상 값을 가진다.
  • 오직 하나의 값을 가진다
  • 여러 observer 를 지원하므로 흐름이 공유된다.
  • 활성화된 observer 수와 관계없이 항상 구독의 최신 값을 재연한다.

UI 상태를 뷰에 노출할 때, StateFlow 를 사용하세요. UI 상태를 유지하도록 설계된 안전하고 효율적인 observer 이다.

#2: Expose the result of a one-shot operation

변경가능한 지원 속성 없이 코루틴 호출의 결과를 노출하는 이전 코드 조각과 동일하다.

LiveData 에서는 이를 위해 liveData 코루틴 빌더를 사용했다.

class MyViewModel(...) : ViewModel() {
    val result: LiveData<Result<UiState>> = liveData {
        emit(Result.Loading)
        emit(repository.fetchItem())
    }
}

state holder 에는 항상 값이 있으므로, Loading, Success, Error 와 같은 상태를 지원하는 일종의 Result 클래스로 UI 상태를 래핑하는 것이 좋다.

Flow 와 동등한 기능은 몇가지 구성을 해야하기 때문에 좀 더 복잡하다.

class MyViewModel(...) : ViewModel() {
    val result: StateFlow<Result<UiState>> = flow {
        emit(repository.fetchItem())
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), // Or Lazily because it's a one-shot
        initialValue = Result.Loading
    )
}
  • stateIn 은 Flow 를 StateFlow로 변환하는 Flow 연산자이다.

#3: One-shot data load with parameters

매개변수로 일회성 데이터를 로드한다

사용자 ID에 따라 일부데이터를 로드하고, Flow를 노출하는 AuthManager 로 부터의 정보를 얻는다고 가정해보자.

LiveData 를 사용하면 다음과 유사한 작업을 수행할 수 있다.

class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: LiveData<String?> = 
        authManager.observeUser().map { user -> user.id }.asLiveData()

    val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->
        liveData { emit(repository.fetchItem(newUserId)) }
    }
}
  • switchMap 은 transformation 의 본문이 실행되고, UserId 가 변경될때 결과가 구독된다.

userId가 LiveData일 이유가 없다면 이에 더 나은 대안은 stream 을 Flow 와 결합하고 마지막으로 노출된 결과를 LiveData 로 변환하는 것이다.

class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }

    val result: LiveData<Result<Item>> = userId.mapLatest { newUserId ->
       repository.fetchItem(newUserId)
    }.asLiveData()
}

Flow 사용하여 수행하는 경우 아래와 유사하다.

class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }

    val result: StateFlow<Result<Item>> = userId.mapLatest { newUserId ->
        repository.fetchItem(newUserId)
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.Loading
    )
}

좀더 유연하게 개발하고싶은 경우, transformLatest 를 사용하고 명시적으로 아이템을 emit 내보낼 수도 있다.

val result = userId.transformLatest { newUserId ->
        emit(Result.LoadingData)
        emit(repository.fetchItem(newUserId))
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.LoadingUser // Note the different Loading states
    )

#4: Observing a stream of data with parameters

매개변수를 사용하여 데이터 스트림을 관찰

이제 예제를 좀더 반응형으로 만들어 보자. 데이터를 가져오는 것이 아니라 관찰하므로 우리는 데이터 소스의 변경사항을 UI에 자동적으로 전파한다.

예제를 계속하면 data source 에 fechItem을 호출하는 대신 우리는 Flow를 반환하는 가상의 observeItem 연산자를 사용합니다.

LiveData 를 사용하면 flow를 LiveData 로 변환하고 모든 업데이트를 방출할 수 있다.

class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: LiveData<String?> = 
        authManager.observeUser().map { user -> user.id }.asLiveData()

    val result = userId.switchMap { newUserId ->
        repository.observeItem(newUserId).asLiveData()
    }
}

또는 flatMapLatest 를 사용하여 두 flow를 합쳐서 출력을 LiveData 로 바꾸는 것이 좋다.

class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<String?> = 
        authManager.observeUser().map { user -> user?.id }

    val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->
        repository.observeItem(newUserId)
    }.asLiveData()
}

Flow 구현은 유사하지만 LiveData 변환이 없다.

class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<String?> = 
        authManager.observeUser().map { user -> user?.id }

    val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->
        repository.observeItem(newUserId)
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.LoadingUser
    )
}

노출된 StateFlow는 사용자가 변경되거나, 저장소의 사용자 데이터가 변경될 때마다 업데이트를 받는다.

#5 Combining multiple sources: MediatorLiveData -> Flow.combine

여러소스의 결합

MediatorLiveData 를 사용하면 하나 이상의 업데이트 소스(LiveData observables)를 관찰하고 새 데이터를 얻을 때 작업을 수행할 수 있다. 일반적으로 MediatorLiveData 의 값을 업데이트 한다.

val liveData1: LiveData<Int> = ...
val liveData2: LiveData<Int> = ...

val result = MediatorLiveData<Int>()

result.addSource(liveData1) { value ->
    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}
result.addSource(liveData2) { value ->
    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}

Flow 는 훨신 더 간단하다.

val flow1: Flow<Int> = ...
val flow2: Flow<Int> = ...

val result = combine(flow1, flow2) { a, b -> a + b }

또한 combineTransform 함수 혹은 zip 을 사용할 수 있다.

profile
A place to study and explore my GitHub projects: github.com/freeskyES

0개의 댓글