ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • LiveData beyond the ViewModel
    Android 2021. 6. 4. 10:41

    LiveData beyond the ViewModel - Reactive patterns using Transformations and MediatorLiveData을 요약, 정리하였다.

     

    LiveData beyond the ViewModel — Reactive patterns using Transformations and MediatorLiveData

    Reactive architecture has been a hot topic in Android for years now. It’s been a constant theme at Android conferences, normally…

    medium.com

     

    LiveData는 Observer의 수명주기를 알고있는 simple observable이다. 

    data source 또는 repository에서 LiveData를 노출하는 것은 architecture를 보다 반응적으로 만드는 간단한 방법이지만 잠재적인 함정이 있다. traps를 피하고 몇 가지 패턴을 사용하여 LiveData를 사용하여 보다 반응적인 아키텍처를 구축해보자.

    LiveData의 목적

    Android에서 Activity, Fragment 및 View는 거의 언제든지 destory될 수 있으므로 이러한 구성 요소 중 하나에 대한 참조로 인해 누수 또는 NullPointerException이 발생할 수 있다.

     

    LiveData는 observer pattern을 구현하도록 설계되어 View controller(Activity, Fragment 등)와 UI Data source (일반적으로 ViewModel)간의 통신을 허용한다. LiveData는 lifecycle 인식 덕분에 데이터가 active 상태인 경우에만 view에서 수신된다. 요컨대, View와 ViewModel간의 구독을 수동으로 취소할 필요가 없다는 장점이 있다.

    View-ViewModel interactions

    observable paradigm은 View 컨트롤러와 ViewModel간에 잘 작동한다. 이를 사용하여 앱의 다른 구성 요소를 관찰하고 lifecycle 인식을 활용할 수 있다. 예를 들면 :

    • SharedPreferences의 변경 사항 관찰
    • Firestore에서 문서 또는 collection 관찰
    • FirebaseAuth와 같은 인증 SDK로 current user 관찰
    • Room에서 query 관찰 (즉시 LiveData를 지원함)

    이 패러다임의 장점은 데이터가 변경될 때 UI가 자동으로 업데이트된다는 것, 단점은 LiveData에는 Rx와 같이 data streams을 결합하거나 스레드를 관리하는 toolkit이 포함되어 있지 않다는 것이다. 일반적인 앱의 모든 계층에서 LiveData를 사용하는 것은 다음과 같다.

     

    LiveData를 사용하는 일반적인 App architecture

    구성 요소간에 데이터를 전달하려면 매핑 및 결합이 필요하다. MediatorLiveData는 Transformations 클래스의 method를 사용하여 매핑 및 결합을 한다.

    Patterns

    One-to-one static transformation - map

    ViewModel은 한 유형의 데이터를 관찰하고 다른 데이터를 노출한다.

    아래의 예에서 ViewModel은 repository에서 view로 데이터를 전달하고 UI 모델로 변환한다. 저장소에 새 데이터가 있을 때마다 ViewModel은 이를 매핑하면 된다. 

    class MainViewModel {
      val viewModelResult = Transformations.map(repository.getDataForUser()) { data ->
         convertDataToMainUIModel(data)
      }
    }

    이 변환은 매우 간단하다. 그러나 사용자가 변경될 수 있는 경우 switchMap이 필요하다.

    One-to-one dynamic transformation - switchMap

    User Manager는 결과를 노출하기 전에 Repository에 필요한 사용자 ID를 제공한다.

    사용자 ID를 즉시 사용할 수 없으므로 ViewModel 초기화시 연결할 수 없다. 이것을 switchMap으로 구현할 수 있다. 

    class MainViewModel {
      val repositoryResult = Transformations.switchMap(userManager.user) { user ->
         repository.getDataForUser(user)
      }
    }

    switchMap은 내부적으로 MediatorLiveData를 사용한다.

    LiveData의 여러 소스를 결합할 때 사용하므로 익숙해지는 것이 중요하다.

    One-to-many dependency - MediatorLiveData

    MediatorLiveData를 사용하면 single LiveData observable에 하나 이상의 data source를 추가할 수 있다.

     

    MediatorLiveData는 두 데이터 소스를 결합하는 데 사용된다.

    fun blogpostBoilerplateExample(newUser: String): LiveData<UserDataResult> {
    
        val liveData1 = userOnlineDataSource.getOnlineTime(newUser)
        val liveData2 = userCheckinsDataSource.getCheckins(newUser)
    
        val result = MediatorLiveData<UserDataResult>()
    
        result.addSource(liveData1) { value ->
            result.value = combineLatestData(liveData1, liveData2)
        }
        result.addSource(liveData2) { value ->
            result.value = combineLatestData(liveData1, liveData2)
        }
        return result
    }

    실제 데이터 조합은 combineLatestData()에서 수행된다.

    private fun combineLatestData(
            onlineTimeResult: LiveData<Long>,
            checkinsResult: LiveData<CheckinsResult>
    ): UserDataResult {
    
        val onlineTime = onlineTimeResult.value
        val checkins = checkinsResult.value
    
        // Don't send a success until we have both results
        if (onlineTime == null || checkins == null) {
            return UserDataLoading()
        }
    
        // TODO: Check for errors and return UserDataError if any.
    
        return UserDataSuccess(timeOnline = onlineTime, checkins = checkins)
    }

    값이 준비되었는지 또는 올바른지 확인하고 결과 (loading, error 또는 success)를 내보낸다.

     

    LiveData를 사용하지 않는 경우

     

    앱의 구성 요소가 UI에 연결되어 있지 않으면 LiveData가 필요하지 않을 수 있다.

     

    예를 들어 앱의 user manager는 auth provider (예 : Firebase Auth)의 변경 사항을 수신하고 고유한 token을 서버에 업로드한다.

     

    token uploader와 user manager간의 상호 작용이 reactive할 필요가 있을까?

     

    token uploader는 user manager를 관찰할 수 있지만 이 작업은 View와 전혀 관련이 없다. 또한 View가 파괴되면 user token이 업로드되지 않을 수 있다.

     

    UI와 관련이 없는 작업은 LiveData를 사용할 필요가 없다.

     

    Antipattern : LiveData 인스턴스 공유

     

    클래스가 LiveData를 다른 클래스에 노출할 때 동일한 LiveData 인스턴스를 노출할지 아니면 다른 인스턴스를 노출할지 신중하게 생각해야한다.

    class SharedLiveDataSource(val dataSource: MyDataSource) {
    
        // Caution: this LiveData is shared across consumers
        private val result = MutableLiveData<Long>()
    
        fun loadDataForUser(userId: String): LiveData<Long> {
            result.value = dataSource.getOnlineTime(userId)
            return result
        }
    }

    이 클래스가 singleton인 경우 항상 동일한 LiveData를 반환할 수 있을거라 생각하겠지만 반드시 그런 것은 아니다. 이 클래스의 consumer가 여러 개 있을 수 있다.

     

    예를 들어 

     

    첫번째 consumer

    sharedLiveDataSource.loadDataForUser("1").observe(this, Observer {
       // Show result on screen
    }) 

    두 번째 consumer도 사용

    sharedLiveDataSource.loadDataForUser("2").observe(this, Observer {
       // Show result on screen
    }) 

    첫 번째 consumer는 user "2"에 속한 데이터로 업데이트를 받는다.

    한 명의  consumer에게만 이 클래스를 사용한다고 하더라도 이 패턴을 사용하면 버그가 생길 수 있다. 

    이 문제에 대한 해결책은 각각의 consumer에 대해 새 LiveData를 반환하는 것이다. 

    class SharedLiveDataSource(val dataSource: MyDataSource) {
        fun loadDataForUser(userId: String): LiveData<Long> {
            val result = MutableLiveData<Long>()
            result.value = dataSource.getOnlineTime(userId)
            return result
        }
    }

     

    'Android' 카테고리의 다른 글

    ConstraintLayout(1)  (0) 2021.06.11
    Room And Coroutines  (0) 2021.06.10
    LiveData  (0) 2021.05.30
    Android Kotlin Fundamentals - ViewModel(2)  (0) 2021.05.30
    Android Kotlin Fundamentals - ViewModel(1)  (0) 2021.05.29
Designed by Tistory.