본문 바로가기

Android

[Android] LiveData

LiveData는 관찰 가능한 데이터 홀더 클래스로 Activity 나 Fragment 등 앱 구성요소의 수명주기를 인식하여 동작한다.

저는 비동기적인 작업(Corouintes + Flow)을 수행 시 주로 사용하며 완료 상태에 따라 UI의 데이터 업데이트에서 Activity 나 Fragment 또는 two-way binding을 통한 xml에서 직접 LiveData를 통한 Observing을 적용하고 있습니다.

LiveData 특징

  • UI(Activity, Fragment)와 데이터(ViewModel)의 상태 일치를 보장
    Observer 패턴을 따르 데이터라 변경될 때마다 관찰자에게 알림
  • LifeCycle를 통해 결합하므로 메모리 누수가 없음
  • 구성 변경 시 최신 데이터를 받아옴

LiveData 사용법

Android Architecture Components 가이드에 따라 LiveData는 ViewModel에서 사용한다.
(ViewModel : 앱의 LifeCycle를 고려하여 UI관련 데이터를 저장 및 관리하는 컴포넌트)

@HiltViewModel
class BookDetailViewModel @Inject constructor(
    private val bookRepository: BookRepository
) : ViewModel() {

  	// 1번
    private val _isBookSaved = MutableLiveData<Int>()
		// 2번
  	val isBookSaved: LiveData<Int>
        get() = _isBookSaved

    fun addBook(type: @BookType Int, bookInfo: BookInfo) {
        val book = bookInfo.asBook()
        book.type = type

        viewModelScope.launch(Dispatchers.Main) {
            if (!bookRepository.isSameBookSaved(bookInfo.title)) {
                bookRepository.addBook(book)
              	// 3번
                _isBookSaved.value = type
            } else {
              	// 3번
                _isBookSaved.value = BOOK_TYPE_UNKNOWN
            }
        }
    }
}

위 코드는 데이터베이스에서 같은 책이 저장되어 있는지 조회(비동기작업) 한 뒤 LiveData를 통해 처리하는 코드이다.
1번에서는 Mutable형태의 LiveData를 선언하여 값을 get/set 할 수 있도록 한다.
2번에서는 읽기 전용의 LiveData를 선언하여 UI에서 참조할 수 있도록 하자. (물론 getter를 선언하여 1번의 값이 변경되면 인식할 수 있도록 한다)
3번에서는 비동기작업이 완료된 후 해당 LiveData의 값을 변경한다.

 

이제는 Fragment에서 어떻게 해당 LiveData를 Observe 하는지 살펴보자.

// BookDetailFragment.kt
// 4번
bookDetailViewModel.isBookSaved.observe(viewLifecycleOwner) {
    when (it) {
        BOOK_TYPE_UNKNOWN -> Toast.makeText(
            requireContext(),
            "독서내역에 추가된 책 입니다!",
            Toast.LENGTH_SHORT
        ).show()
        BOOK_TYPE_WISH -> {
            findNavController().navigate(
                BookDetailFragmentDirections.actionBookDetailFragmentToHistoryFragment(
                    bookType = BOOK_TYPE_WISH
                )
            )
        }
    }
}

4번에서는 ViewModel에서 선언한 LiveData를 구독하고 있는것을 알 수 있다.
방법은 간단하다. 해당 Fragment의 viewLifecycleOwner를 추가하면 된다.
나머지는 LiveData가 내부적으로 수명주기를 고려하여 변경된 데이터를 전달해 줄 것이다.

 

Fragment는 두 개의 LifeCycle이 존재한다.
하나는 Fragment의 LifeCycle과 다른 하나는 Fragment View의 Lifecycle이다.
그렇다면 왜 4번에서는 Fragment View의 Lifecycle인 viewLifecycleOwner을 사용하는가?

 

그 이유는 Fragment의 생명주기와 관련이 있는데,
예를 들어 구성 변경 시 Fragment는 Activity와 다르게 onDestory()를 호출하지 않고 onCreateView()를 호출한다.
이런 현상이 반복되다 보면 LivaData의 Observer가 여러번 호출되는 것을 알 수 있다.
이를 방지하고자 LiveData 사용 시 viewLifecycleOwner를 강제화하고 있는 것이다.

Thread 주의

위의 코드 중 3번을 보면 Main Thread에서 코루틴을 실행하고 set 방식으로 LiveData의 값을 변경함을 알 수 있다.
그렇다. LiveData는 항상 MainThread의 값을 처리한다.

 

하지만 다른 Thread의 값 또한 처리할 수 있다.
방법은 postValue를 사용하는 것이다.

_isBookSaved.postValue(type)

postValue의 Java 내부 코드를 보면 결국 postToMainThread()를 호출하여 해당 값을 Main Thread에 전달하는 것을 알 수 있다.

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

하지만 Main Thread가 아닌 IO 나 Default Thread에서 LivaData를 setValue를 하게되면 아래와 같은 에러를 만날 것이니 주의하라.


참고

https://developer.android.com/topic/libraries/architecture/livedata

https://pluu.github.io/blog/android/2020/01/25/android-fragment-lifecycle/

ViewModel 코드

Fragment 코드