Android LiveData의 사용법과 내부 코드를 통한 UI 처리, 중복 처리 등 살펴보기



개인 광고 영역

Android 개발 시 자주 활용하는 LiveData. LiveData의 데이터 처리를 알아보는 글이다.

이 글에서 도움 될 문서는 아래와 같다.


이 글에서 알아볼 내용

  • LiveData 사용법을 알아본다.
  • MutableLiveData와 LiveData의 구분?
  • LiveData는 항상 UI로 처리하는 코드를 살펴본다.
  • 중복 데이터의 위험성을 알아본다.


LiveData?

LiveData는 RxJava와 같은 observable 형태로 사용하며, 안드로이드 Lifecycle에 따라 데이터를 관리한다.

Activity, Fragment의 라이프 사이클을 따르기에 활동에 대한 처리를 알아서 관리해 준다.

구글 문서를 참고하면 아래와 같은 장점을 확인할 수 있다.

  • LiveData는 observable 패턴을 사용하기에 데이터의 변화를 구독한 곳으로 통지하고, 업데이트한다.
  • 메모리 누수 없는 사용을 보장한다.
  • Lifecycle에 따라 LiveData의 이벤트를 제어한다.
  • 항상 최신 데이터를 유지한다.
  • 기기 회전이 일어나도 최신 데이터를 처리할 수 있도록 도와준다.(AAC-ViewModel과 함께 활용 시)
  • LiveData의 확장도 지원한다.


LiveData 사용법

오래전부터 RxJava를 이용한 View와 ViewModel 간의 데이터처리를 한 적이 있다. RxJava는 Lifecycle을 보장하는 코드가 따로 없다.

그래서 직접 구현하거나 또는 Trello - RxLifecycle을 사용할 수 있다.

RxJava는 글의 주요 내용이 아니니 LiveData로 돌아가, LiveData의 주요 코드를 확인해보자.

class LoadDetailViewModel : ViewModel() {

    private val _liveData = MutableLiveData<String>()
    val liveData: LiveData<String> get() = _liveData

    fun loadData() = viewModelScope.launch {
        _liveData.value = "value"
        _liveData.postValue("value")
    }
}

// 사용 시 - kotlin 1.3.x 버전까지는 Observer을 명시해야 한다.
// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
loadDetailViewModel.liveData.observe(this, Observer {
    // View 처리
})

// kotlin 1.4.x 버전부터는 SAM 지원으로 아래와 같다.
// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
loadDetailViewModel.liveData.observe(this) {
    // View 처리
}

위 코드에서 한 줄 한 줄 살펴보자.

MutableLiveData와 LiveData의 구분?

MutableLiveData와 LiveData를 구분해서 사용하는 이유를 알아보자.

  • MutableLiveData : 값의 get/set 모두를 할 수 있다.
  • LiveData : 값의 get()만을 할 수 있다.

Mutable은 코틀린 언어에서 흔하게 볼 수 있는 내용인데, 값의 get/set 모두를 허용하기에, 값의 변경과 읽기 모두 가능하다.

반대로 LiveData는 읽기 전용의 데이터를 만들기에 값의 변경은 불가능하다.

이런 기법은 ViewModel과 View의 역할을 분리하기 위함이라 이렇게 사용한다. ViewModel은 언제나 새로운 값의 변경이 일어나고, 다시 읽을 수 있는 형태로 사용하는 것이고, View는 값의 입력이 아닌 읽기만을 허용하는 것이다.

그렇다고 해서 항상 View에서는 LiveData만 봐야 하는 것은 아니다. 필요에 따라 View에서도 값의 변경이 일어나야 하는 케이스라면 허용할 수 있는데, checkBox의 값을 변경하는 케이스라면 가능하다.

상황에 잘 맞게 써야 한다는 말이다.


get()을 사용하는 경우

코틀린에서는 Property를 활용해서 get()을 구현한다. 그럼 직접 변수에 치환하면 안되는 것인가?

물론 이것도 가능하다. 다만 자바 기준 변수 2개가 만들어지는 단점은 있고, 반대로 get()을 활용치 않은 경우라면 호출할 때마다 치환하는 과정이 들어간다.

근데 보통 observe 시점이 1:1 관계를 벗어나지 않는데, 한번 등록하고 나면 재구독 할 일이 많지 않다.

그래서 어떻게 쓰든 사실 큰 문제는 없다. 어차피 Observe는 N 번 할 수 있기에 초기 생성 비용을 늘리고, 변수 2개를 만들 것인지, 호출 시 치환하는 비용을 늘릴 것인지는 선택일 수 있다.

위 코드를 자바로 변경하면 아래와 같다.

private final MutableLiveData _liveData;

@NotNull
public final LiveData getLiveData() {
   return (LiveData)this._liveData;
}

그리고 get()을 제거한 경우라면 아래와 같다.

private final MutableLiveData _liveData;
@NotNull
private final LiveData liveData;

public LoadDetailViewModel() {
   super();
   this._liveData = new MutableLiveData();
   this.liveData = (LiveData)this._liveData;
}


LiveData는 항상 UI로 처리한다.

LiveData는 항상 MainThread로 값을 처리한다. 그래서 아래와 같이 사용하는 경우는 좋은 방법이 아니다.

  • ViewModel보다 더 안쪽인 Repository(UseCase)와 같이 내부에서 LiveData로 값을 가져오는 경우
  • View 업데이트가 없는 코드에서 LiveData를 활용하는 경우

이 두 가지 케이스는 모두 UI를 업데이트하지 않는데 LiveData를 활용하는 경우에 해당한다.

내부 코드를 보면 알 수 있는데, set과 postValue 2 가지 방식으로 값을 주입한다.

  • set : MainThread(UI)가 보장될 경우에는 set을 활용한다.
  • postValue : MainThread가 아닌 IO 스케쥴러를 활용하는 경우 postValue를 활용한다.

결국 두 방법 모두 UI로 값을 전달하기 위해 만들어져있음을 알 수 있다.

내부 코드를 조금 보면 명확하게 @MainThread로 지정되어 있음을 알 수 있다. MainThread에서 setValue를 호출하지 않으면 워닝이 뜨는 정도이긴 하지만, 런타임에 assertMainThread 값에서 오류가 발생한다.

@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
}

이번엔 postValue이다. postValue는 별도의 값 처리는 없고, 맨 마지막에 postToMainThread을 호출하여 MainThread에서 값의 전달이 가능하도록 처리함을 알 수 있다.

protected void postValue(T value) {
    boolean postTask;
    synchronized (mDataLock) {
        postTask = mPendingData == NOT_SET;
        mPendingData = value;
    }
    if (!postTask) {
        return;
    }
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

runnable에는 mPendingData가 값을 들고 있고, run이 호출되면 setValue를 다시 부름을 알 수 있다.

private final Runnable mPostValueRunnable = new Runnable() {
    @SuppressWarnings("unchecked")
    @Override
    public void run() {
        Object newValue;
        synchronized (mDataLock) {
            newValue = mPendingData;
            mPendingData = NOT_SET;
        }
        setValue((T) newValue);
    }
};

정리하면 setValue를 하든 postValue를 하든 LiveData는 항상 MainThread에서 사용할 값을 보증한다.

결국 데이터의 처리가 IO에서 발생해야 하는 경우라면 LiveData 활용은 맞지 않는 것이다. 이 경우라면 Thread를 활용하거나, RxJava, Coroutines을 활용해 처리해야 한다.


LiveData는 항상 최신 데이터만 보증한다.

LiveData는 항상 최신 데이터만을 보증하는데, 이유는 위에서 보았던 setValue를 보면 알 수 있다.

setValue는 어떠한 처리도 없는 단순 변수이다. 변수는 항상 마지막 데이터만을 들고 있을 수 있다.

그러니 mData에 값은 항상 최신, 마지막 데이터만을 유지한다.

private volatile Object mData;

그래서 아래와 같이 연속 데이터를 2번 날리면 항상 2번의 데이터만을 받는다.

fun loadData() = viewModelScope.launch {
  _liveData.value = "value1"
  _liveData.value = "value2"
}

postValue를 2번 보내도 역시 2번째 값이 최신화된다.

fun loadData() = viewModelScope.launch {
  _liveData.postValue("value1")
  _liveData.postValue("value2")
}

아래와 같은 경우라면 1/2 모두 값을 볼 수 있는데, 타이밍 이슈가 생길 수 있기에 아래와 같이 쓰고, 문제가 없다고 생각할 수 있으나, 기기 성능에 따라 보일 수도 아닐 수도 있다.

fun loadData() = viewModelScope.launch {
    _liveData.value = "value1"
    _liveData.postValue("value2")
}

정리하면 setValue든 postValue든 마지막의 값만을 update 하는 것을 알 수 있다.

만약 연속적인 처리를 setValue나 postValue로 처리해야 하는 경우라면 2개의 LiveData를 생성하는 것을 권장한다.


마무리

간단하게 LiveData에 대해서 알아보았다. Observe 처리하는 부분은 별도의 글을 통해 작성해보려고 한다.

아무래도 값을 입력하는 것보단 언제 써지며, 어떠한 타이밍에 따라 값을 불러오고, 가져오는 케이스를 아는 것이 더 중요하다.

observe에 넘겨주는 LifecycleOwner을 어떤 것을 넘겨주는지에 따라 다른데, Activity는 하나뿐이지만, Fragment에서는 2개의 LifecycleOwner을 가지고 있다.

이런 값에 따라서 동작하는 방법이 달라지는데 아래 pluu 님이 작성한 글을 참고하시면 되겠다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments