Android - LiveData 사용 방법

Jetpack 또는 AAC(Android Architecture Component)의 LiveData는 데이터를 저장하고 변화를 관찰 할 수 있는 객체입니다. UI객체는 LiveData에 옵저버를 등록할 수 있으며 데이터가 변경될 때 UI를 변경할 수 있습니다.

예를들어 아래처럼 LiveData를 생성하고 옵저버를 등록할 수 있습니다. 마치 RxJava의 Observable과 유사합니다. 데이터가 변경되면 옵저버에게 전달됩니다.

val elapsedTime = LiveData<Long>
elapsedTime.observe(this, androidx.lifecycle.Observer<Long> { time ->
        timer_textview.text = time.toString()
})

Observable과 차이점은 LiveData의 이름처럼 LifecycleOwner의 상태가 STARTEDRESUMED로 활성화 상태일 때만 옵저버에게 데이터 변화를 알려줍니다. 그리고 Lifecycle의 상태가 DESTROYED로 변경되면 LiveData도 자동으로 소멸이 됩니다. 그렇기 때문에 Memory leak 등의 문제를 신경쓰지 않아서 좋습니다.

특히, LiveData는 ViewModel에서 사용되도록 설계가 되었습니다. 액티비티나 프래그먼트가 재실행되도 ViewModel은 소멸되지 않기 때문에 LiveData도 소멸되지 않습니다. 또한 Lifecycle이 활성화(active)되었을 때만 데이터 변화를 알려주기 때문에, register, unregister 등의 쓸데없는 boilerplate(형식적인) 코드들이 많이 줄어듭니다.

ViewModel과 LiveData

라이브데이터는 특히 ViewModel에서 사용하도록 설계되었습니다. (항상 ViewModel과 함께 사용할 필요는 없습니다)

아래 코드는 코틀린으로 작성된 ViewModel 클래스입니다. ViewModel 안에 MutableLiveData 객체가 정의되어있습니다.

MutableLiveData은 LiveData를 상속하고 LiveData.setValueLiveData.postValue를 구현한 클래스입니다. (protected->public으로 변경)

  • setValue : 데이터를 즉시 변경합니다.
  • postValue : Runnable로 데이터를 변경을 요청합니다. Main thread에서서 Runnable이 실행될 때 데이터가 변경됩니다.
class LiveDataTimerViewModel : ViewModel() {

    companion object {
        private const val ONE_SECOND = 1000.toLong()
    }

    private val initialTime: Long = SystemClock.elapsedRealtime()
    private val elapsedTime = MutableLiveData<Long>()

    fun getElapsedTime() = elapsedTime

    init {
        val timer = Timer()
        // Update the elapsed time every second.
        timer.scheduleAtFixedRate(object : TimerTask() {
            override fun run() {
                val newValue = (SystemClock.elapsedRealtime() - initialTime) / 1000
                elapsedTime.postValue(newValue)
                Log.d(TAG, "updating real time")
            }
        }, ONE_SECOND, ONE_SECOND)
    }
}

타이머는 1초마다 MutableLiveData.postValue로 현재 시간으로 변경하고 있습니다. UI객체는 elapsedTime에 옵저버를 등록하여 데이터 변화를 관찰할 수 있습니다. 데이터가 변경되면 UI가 변경되도록 할 수 있습니다.

아래 코드는 액티비티의 코드입니다. androidx.lifecycle.Observer은 갱신된 시간을 UI에 출력하는 코드입니다. 그리고 getElapsedTime()?.observe로 옵저버를 LiveData에 등록하였습니다. observe는 LifecycleOwner과 Observer를 인자로 받습니다. 인자로 전달된 LifecycleOwner가 활성화 상태일 때만 LiveData는 데이터 변화를 옵저버에게 알려줍니다.

class ChronoActivity3 : AppCompatActivity() {
    companion object {
        const val TAG = "ChronoActivity3"
    }

    private var liveDataTimerViewModel: LiveDataTimerViewModel? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_chrono_3)
        liveDataTimerViewModel = ViewModelProviders.of(this).get(LiveDataTimerViewModel::class.java)

        subscribe()
    }

    private fun subscribe() {
        val elapsedTimeObserver = androidx.lifecycle.Observer<Long> { time ->
                timer_textview.text = time.toString()
                Log.d(TAG, "Updating timer")
        }

        // observe the ViewModel's elapsed time
        liveDataTimerViewModel?.getElapsedTime()?.observe(this, elapsedTimeObserver)
    }
}

ViewModelProviders는 android.arch.lifecycle:extensions:1.1.0에서 deprecated되었습니다. ViewModelProvider를 사용하여 ViewModel 객체를 생성해야 합니다. 자세한 내용은 Android - ViewModel을 생성하는 방법를 참고해주세요.

이제 앱을 실행해보세요. 코드를 보시면 타이머에서 시간을 갱신할 때 로그를 추가하였고, 옵저버에도 로그를 추가하였습니다. MutableLiveData의 데이터를 변경할 때 로그가 출력되고, 옵저버에서 UI를 변경할 때 로그가 출력되었습니다.

2018-12-15 21:54:59.766 5292-5324/com.codechacha.sample D/LiveDataTimerViewModel: updating real time
2018-12-15 21:54:59.767 5292-5292/com.codechacha.sample D/ChronoActivity3: Updating timer
2018-12-15 21:55:00.766 5292-5324/com.codechacha.sample D/LiveDataTimerViewModel: updating real time
2018-12-15 21:55:00.767 5292-5292/com.codechacha.sample D/ChronoActivity3: Updating timer
.....

LifecycleOwner가 활성화상태일 때만 이벤트가 전달되는지 확인해볼께요. Home key를 눌러 앱이 중지되도록 하였습니다. 로그를 보면 옵저버로 이벤트가 전달되지 않고 있습니다.

2018-12-15 22:09:32.769 5292-5324/com.codechacha.sample D/LiveDataTimerViewModel: updating real time
2018-12-15 22:09:33.806 5292-5324/com.codechacha.sample D/LiveDataTimerViewModel: updating real time
2018-12-15 22:09:34.783 5292-5324/com.codechacha.sample D/LiveDataTimerViewModel: updating real time
.....

액티비티의 상태가 STOPPED일 때 LiveData가 옵저버로 데이터 변화를 알려주지 않기 때문에 NullPointerException 등의 문제를 걱정할 필요는 없습니다. 다만, 액티비티를 사용하지 않는데 ViewModel의 타이머는 멈추지 않고 계속 동작하는데 쓸데 없이 동작하는 것이 마음에 들지 않네요.

만약 시스템의 비싼 자원을 얻는 것이라면 모두를 위해 필요할 때만 사용하는 것이 좋습니다. ViewModel은 Livecycle로부터 액티비티의 상태를 갱신받기 때문에 여기서 stop/start할 수 있습니다. 또한, LiveData도 Lifecycle의 상태를 알고 있기 때문에 이 객체 안에서 stop/start할 수 있습니다.

LiveData를 변경하여 stop/start를 구현해보겠습니다. LiveData<Long>를 상속하는 MyTimerLiveData 클래스를 만들었습니다. LiveData는 Lifecycle이 활성화일 때 onActive를 콜백해주고 비활성화일 때 onInactive를 콜백해줍니다. 그래서 onActive에 타이머를 실행시키는 코드를, onInactive에 타이머를 종료하는 코드를 넣었습니다.

타이머가 갱신되면 postValue로 자신의 데이터를 갱신하고, 등록된 옵저버에게도 이벤트를 전달합니다.

class MyTimerLiveData : LiveData<Long>() {
    companion object {
        private const val ONE_SECOND = 1000.toLong()
        const val TAG = "MyTimerLiveData"
    }

    private val initialTime: Long = SystemClock.elapsedRealtime()
    private var timer: Timer? = null

    override fun onActive() {
        Log.d(TAG, "onActive")
        timer = Timer()
        timer?.scheduleAtFixedRate(object : TimerTask() {
            override fun run() {
                val newValue = (SystemClock.elapsedRealtime() - initialTime) / 1000
                postValue(newValue)
                Log.d(TAG, "updating real time")
            }
        }, ONE_SECOND, ONE_SECOND)
    }

    override fun onInactive() {
        Log.d(TAG, "onInactive")
        timer?.cancel()
    }
}

ViewModel의 코드도 변경이 필요합니다. LiveData를 MyTimerLiveData로 변경해야합니다. 그리고 MyTimerLiveData로 옴겨진 타이머 코드는 제거해줍니다.

class LiveDataTimerViewModel : ViewModel() {

    companion object {
        const val TAG = "LiveDataTimerViewModel"
    }

    private val initialTime: Long = SystemClock.elapsedRealtime()
    private val elapsedTime = MyTimerLiveData()

    fun getElapsedTime() = elapsedTime
}

코드가 대부분 옴겨져 ViewModel은 매우 간단해졌습니다. 이제 앱을 실행해서 로그를 동작을 확인해보세요. 로그를 보면 액티비티가 실행되었을 때 타이머가 동작하고 종료되었을 때 타이머가 멈춥니다.

2018-12-15 22:12:56.403 6316-6316/com.codechacha.sample D/MyTimerLiveData: onActive
....
2018-12-15 22:13:01.404 6316-6347/com.codechacha.sample D/MyTimerLiveData: updating real time
2018-12-15 22:13:01.405 6316-6316/com.codechacha.sample D/ChronoActivity3: Updating timer
2018-12-15 22:13:01.953 6316-6316/com.codechacha.sample D/MyTimerLiveData: onInactive

setValue, postValue

setValuepostValue는 데이터를 변경하는 메소드입니다. 하지만 약간 차이가 있습니다. 차이를 알기 위해 LifeData.java의 코드를 잠시 보겠습니다.

setValue는 메인쓰레드에서 즉시 값을 변경하고 옵저버로 데이터 변경을 알려줍니다.

/**
 * Sets the value. If there are active observers, the value will be dispatched to them.
 * <p>
 * This method must be called from the main thread. If you need set a value from a background
 * thread, you can use {@link #postValue(Object)}
 *
 * @param value The new value
 */
@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
}

반면에, postValue는 Runnable로 데이터 변경을 예약하기 때문에 바로 변경이 되지 않습니다. 메인쓰레드에서 Runnable이 실행되기 전에 postValue가 여러번 호출되도 마지막으로 변경된 값만 옵저버로 전달됩니다.

/**
 * Posts a task to a main thread to set the given value. So if you have a following code
 * executed in the main thread:
 * <pre class="prettyprint">
 * liveData.postValue("a");
 * liveData.setValue("b");
 * </pre>
 * The value "b" would be set at first and later the main thread would override it with
 * the value "a".
 * <p>
 * If you called this method multiple times before a main thread executed a posted task, only
 * the last value would be dispatched.
 *
 * @param value The new value
 */
protected void postValue(T value) {
    boolean postTask;
    synchronized (mDataLock) {
        postTask = mPendingData == NOT_SET;
        mPendingData = value;
    }
    if (!postTask) {
        return;
    }
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

테스트 코드로 setValue를 확인해보았습니다. 아래처럼 1,2,3으로 데이터를 순차적으로 변경하였습니다.

override fun onActive() {
    Log.d(TAG, "onActive")
    setValue(1)
    setValue(2)
    setValue(3)
}

로그를 확인해보면 빠짐없이 옵저버 값 변경이 전달됩니다.

2018-12-16 04:09:06.127 3360-3360/com.codechacha.sample D/MyTimerLiveData: onActive
2018-12-16 04:09:06.127 3360-3360/com.codechacha.sample D/ChronoActivity3: Updating timer : 1
2018-12-16 04:09:06.127 3360-3360/com.codechacha.sample D/ChronoActivity3: Updating timer : 2
2018-12-16 04:09:06.127 3360-3360/com.codechacha.sample D/ChronoActivity3: Updating timer : 3

아래 코드처럼 postValue만 사용하면 마지막 변경된 것만 옵저버에게 전달하기 때문에 3 이전의 데이터 변경은 옵저버가 알 수 없습니다.

override fun onActive() {
    Log.d(TAG, "onActive")
    postValue(1)
    postValue(2)
    postValue(3)
}

예상한 것처럼 로그를 보면 옵저버에게 3만 전달되었습니다.

2018-12-15 23:15:10.416 9358-9358/com.codechacha.sample D/MyTimerLiveData: onActive
2018-12-15 23:15:10.426 9358-9358/com.codechacha.sample D/ChronoActivity3: Updating timer : 3

만약 아래 코드처럼, postValuesetValue를 섞어서 사용하면 결과를 예측하기가 어렵습니다.

override fun onActive() {
    Log.d(TAG, "onActive")
    postValue(1)
    postValue(2)
    setValue(3)
}

테스트 결과는 아래와 같습니다. 1은 setValue에 의해 3으로 덮어쓰여졌고, 2는 그 이후에 변경되서 옵저버로 전달된 것처럼 보입니다. 메인쓰레드가 데이터를 전달할 때 Runnable이 얼마나 처리되었는지에 따라서 결과가 달라질 것 같습니다.

2018-12-15 23:13:10.458 9238-9238/com.codechacha.sample D/MyTimerLiveData: onActive
2018-12-15 23:13:10.458 9238-9238/com.codechacha.sample D/ChronoActivity3: Updating timer : 3
2018-12-15 23:13:10.475 9238-9238/com.codechacha.sample D/ChronoActivity3: Updating timer : 2

observeForever, removeObserver

Livecycle이 활성화 상태가 아니라도 옵저버가 데이터를 받고 싶을 수 있습니다. 그럴 때는 observe대신에 observeForever를 사용하면 됩니다.

Lifecycle이 필요없기 때문에 인자에 LifecycleOwner는 없습니다.

liveDataTimerViewModel?.getElapsedTime()?.observeForever(elapsedTimeObserver)
2018-12-16 13:17:20.986 4551-4551/com.codechacha.sample D/MyTimerLiveData: onActive
2018-12-16 13:17:21.987 4551-4580/com.codechacha.sample D/MyTimerLiveData: updating real time
2018-12-16 13:17:21.988 4551-4551/com.codechacha.sample D/ChronoActivity3: Updating timer
....

옵저버를 제거할때는 removeObserver를 사용하면 됩니다.

liveDataTimerViewModel?.getElapsedTime()?.removeObserver(elapsedTimeObserver)

onActive, onInactive

Lifecycle이 활성화될 때 LifeData.onActive가, 비활성화될 때 LifeData.onInactive가 호출됩니다. 사용하라는데로 사용하면 되지만, 어떻게 콜백이 되는지 궁금하네요.

LifeData.observer 코드를 보면 우리가 생성한 observer를 LifecycleBoundObserver로 wrapping하고, 이 wrapper를 addObserver로 Lifecycle에 등록하고 있습니다.

public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
    assertMainThread("observe");
    if (owner.getLifecycle().getCurrentState() == DESTROYED) {
        // ignore
        return;
    }
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    if (existing != null && !existing.isAttachedTo(owner)) {
        throw new IllegalArgumentException("Cannot add the same observer"
                + " with different lifecycles");
    }
    if (existing != null) {
        return;
    }
    owner.getLifecycle().addObserver(wrapper);
}

LifecycleBoundObserver에 대해서 자세히 보면 ObserverWrapper를 상속하고 LifecycleEventObserver를 구현하였습니다. Lifecycle의 상태가 변경되면 옵저버에게 onStateChanged로 변경을 알려줍니다.

결국 activeStateChanged로 액티비티 상태가 전달되며 조건에 따라 onActive 또는 onInactive가 호출됩니다.

class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    ...
    @Override
    public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
        if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
            removeObserver(mObserver);
            return;
        }
        activeStateChanged(shouldBeActive());
    }
    ....
}

private abstract class ObserverWrapper {
  ...
  void activeStateChanged(boolean newActive) {
      if (newActive == mActive) {
          return;
      }
      // immediately set active state, so we'd never dispatch anything to inactive
      // owner
      mActive = newActive;
      boolean wasInactive = LiveData.this.mActiveCount == 0;
      LiveData.this.mActiveCount += mActive ? 1 : -1;
      if (wasInactive && mActive) {
          onActive();
      }
      if (LiveData.this.mActiveCount == 0 && !mActive) {
          onInactive();
      }
      if (mActive) {
          dispatchingValue(this);
      }
  }
  ....
}

정리

LifeData는 Lifecycle이 활성화되었을 때만 옵저버로 데이터 변화를 알려주기 때문에 Lifecycle을 신경쓰지 않아도 됩니다. 또한 Lifecycle의 상태가 DESTROYED로 되었을 때 LifeData와 Observer 객체는 함께 소멸되어 MemoryLeak에 대해서 신경쓰지 않아도 됩니다.

참고

Loading script...

Related Posts

codechachaCopyright ©2019 codechacha