android

[안드로이드] LiveData, MutableLiveData 사용법

sieunju 2022. 6. 26. 00:20
반응형

요새 안드로이드에서 인기 있는 디자인 패턴이 MVVM 패턴이고 LiveData에 대해서 정확히 알아야 하는 클래스 입니다.

정의야 다른 블로그에 글들이 많으니까 배제하고

실제로 적용해보고 어떤 문제가 있고 어떻게 풀어나가는지..
그리고 몇가지 유의사항이 있는데 그런 이슈는 어떻게 풀어내야 하는지 알아보도록 하겠습니다. 🤔

  1. LiveData, MutableLiveData 차이
  2. LiveData 의 UI 업데이트는 언제 실행 하는지
  3. RecyclerView -> ViewHolder 에서 ViewModel 의 LiveData 를 어떻게 처리하는지? 또한 이러한 구조는 지양 해야 하는지?
  4. List 형식을 LiveData 에서 처리하는 방법
  5. Fragment in Fragment 구조에서 LiveData 사용시 유의 사항
  6. 끝으로
  7. 참고 문헌

 

1. LiveData, MutableLiveData 차이

LiveData 외부에서 데이터 읽기만 가능합니다.
setValue or postValue 를 protected 로 보호 했기 때문에 읽기만 가능합니다.
MutableLiveData 외부에서 데이터 읽기/쓰기가 가능합니다.

- 여담이지만, 안드로이드도 코틀린처럼 네이밍을 맞추려는거 같습니다. Like List (읽기만 가능) MutableList (읽/쓰기 가능)

  • postValue 함수란?
    • WorkThread, UI Thread 둘다 사용가능한 함수 입니다. 
    • 해당 함수를 호출하게 되면 아래 캡처처럼 데이터를 메인 쓰레드로 보냅니다.

  • setValue 함수란?
    • UI Thread 에서만 동작 가능한 함수입니다.
  • 실제로 사용하면 알겠지만, UI 의 상태를 표시할때 LiveData 를 사용합니다. (다른곳에서 사용할 이유는..있을리가..없습니다.)
    • 웬만하면 postValue 는 지양하는것이 개인적으로 좋습니다. 그 이유로 간단한 화면같은 경우에는 맘대로 써도 되지만 조금 복잡한 화면에서는 같은 LiveData 를 여러 군대에서 사용하는 경우 데이터가 생각대로 원하는 값이 업데이트 안될때가 있습니다. 
    • UI 화면 업데이트는 확실하게 setValue 를 통해서 처리하는걸 지향 합니다. 
    • 아래 예제 처럼 실제로 저렇게 하지는 않지만, "postValue" 를 습관적으로 사용하는 경우 엄청난 이슈를 야기 할 수 있습니다.
      실제로 디버그 모드에서는 저 조건문에 들어갈수 있더라도, 실제 프로덕션 모드에서는 99.99% 저 조건문은 안들어 갑니다.
      (이유는 프로덕션 모드에서는 난독화 및 실행하는 속도가 빠르기 때문입니다.)
    private val _testTitle: MutableLiveData<String> by lazy { MutableLiveData() }
    val testTitle: LiveData<String> get() = _testTitle
    
    fun doWork(){
    	// 300ms 이후 _testTitle 에 값을 입력합니다.
        _testTitle.postValue("Hellow")
        if(testTitle.value = "Hellow") {
        	// 데이터가 확실하지 않습니다.
        }
    }

 

2. LiveData 의 UI 업데이트는 언제 실행 하는지

  • LiveData 는 기본적으로 LifecycleOwner 를 의존합니다. 
    • 데이터를 postValue or setValue 를 호출하면 해당 LiveData 에서는 데이터를 가지고 있습니다.
    • Activity 에서 onStart, onResume 상태일때 "observe" 함수를 호출한 곳에서 콜백을 받습니다. 
    • 또한, Lifecycle 에서 onDestroy 상태일때 알아서 "observe" 호출한 리스너를 제거 합니다. 
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
            if (currentState == DESTROYED) {
            	// 여기서 제거합니다!
                removeObserver(mObserver);
                return;
            }
            Lifecycle.State prevState = null;
            while (prevState != currentState) {
                prevState = currentState;
                activeStateChanged(shouldBeActive());
                currentState = mOwner.getLifecycle().getCurrentState();
            }
        }

 

3. RecyclerView -> ViewHolder 에서 ViewModel 의 LiveData 를 어떻게 처리하는지? 또한 이러한 구조는 지양 해야 하는지?

  • RecyclerView -> ViewHolder 에서 ViewModel 에 있는 LiveData를 Observer 를 합니다.
ViewHolder.kt

init {
	binding.setVariable(BR.vm, viewModel)
    itemView.doOnAttach {
    	binding.lifecycleOwner = ViewTreeLifecycleOwner.get(it)
    }

    itemView.doOnDetach {
    	binding.lifecycleOwner = null
    }
}

ViewHolder.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="vm"
            type="com.hmju.livedata.testrecyclerview.TestRecyclerViewModel" />

        <variable
            name="model"
            type="com.hmju.livedata.testrecyclerview.TestModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="300dp">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:padding="15dp"
            android:text="@{model.title}"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:padding="15dp"
            android:text="@{vm.rvTitle}"
            app:layout_constraintBottom_toBottomOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

ViewModel.kt
val ranArr = listOf<String>("가","나","다","라","마","바","사")
viewModelScope.launch(Dispatchers.Main) {
	repeat(100) {
        _rvTitle.value = ranArr[Random.nextInt(ranArr.size)
        delay(1500)
    }
}
  • 1.5 초 단위로 랜덤으로 ViewModel 에서 텍스트 값을 변경하는 처리를 하고 ViewHolder에서는 이를 받아서 "setText" 한다고 쳤을때 binding.lifecycleOwner 를 적절한 위치에 추가 하고 적절한 위치에 메모리 해제를 합니다.
  • 실행한 영상입니다.
    •  
  • 이렇게 간단한 구조에서는 잘 됩니다. 따지고 보면 ViewHolder 에서도 LifecycleOwner 를 binding 에 잘 집어 넣어주면 정상적으로 처리 됩니다...만, 지양하는 이유갸 있습니다. 
    • RecyclerView 는 화면에 보여지는 부분과 약간의 여유가 있는 부분 까지 onBindView 함수를 호출하여 데이터를 갱신 합니다.
    • 즉, LiveData 에서 데이터를 갱신 했지만,  업데이트할 ViewHolder 가 onDetach 상태를 한동안 있다가 다시 onAttach 상태로 전환 되는 경우 제대로 업데이트가 안될수도 있습니다. (경험담..)
      ex.) RecyclerView in RecyclerView in Tab 처럼 보이는 ViewHolder 에서 탭 인디게이터 처리를 "Pull On Refresh"  할때 제대로 이루어지지 않는 경우가 있었습니다. 
    • 제 개인적인 생각이지만, ViewHolder 에서 데이터 갱신처리는 notifyItemChange or DiffUtil 를 사용하는게 정신건강에 좋습니다..

4. List 형식을 LiveData 에서 처리하는 방법

  • LiveData 에서 List 형식은 무조건 사용합니다.. 목록 화면을 만든다면 무조건 사용해야 합니다. 일반적으로 아래와 같이 구성이 될겁니다.
  •  
    private val _listTest : MutableLiveData<MutableList<String>> by lazy { MutableLiveData() }
    val listTest : LiveData<MutableList<String>> get() = _listTest
  • 여기서 List 를 addAll 를 해서 처리 했을때 제대로 동작이 안하는 이슈가 있습니다. 
  • 보통 stackOverFlow 에서는 아래와 같이 솔루션을 줍니다.
fun <T> MutableLiveData<T>.notifyObserver() {
    this.value = this.value
}
굳이..저걸 모든 List 형식의 LiveData 에다가 처리를 해야 할까??? 이런 딜레마에 빠지게 됩니다.
그럼 AddAll 은 그렇다 쳐도 AddIndex, remove, removeInstance 이런 기능들에 대한 처리가 엄청 애매하게 됩니다.

 

  • 다른 개발자들은 모르겠지만, 저 같은 경우에는 쓸데 없이 반복적인 함수를 싫어 합니다.
    예를 들어 아래와 같이 LiveData.value.forEach.... 이렇게 뎁스가 많아지는 케이스 와 List 를 가지고 여러 처리를 외부에서 다 드러내서 처리하는걸 지양 합니다.
        _listTest.value?.forEach { 
            if(it == "ListLiveData?") {
                // do something...
            }
        }
  • 저같은 경우에는 ListLiveData 를 따로 만들어서 처리합니다.
class ListLiveData<T> : MutableLiveData<MutableList<T>>() {
    private val temp: MutableList<T> by lazy { mutableListOf() }

    init {
        value = temp
    }

    override fun getValue() = super.getValue()!!
    val size: Int get() = value.size
    operator fun get(idx: Int) =
        if (size > idx) value[idx] else throw ArrayIndexOutOfBoundsException("Index $idx Size $size")

    fun data(): List<T> {
        return temp
    }

    fun add(item: T) {
        temp.add(item)
        value = temp
    }

    fun addAll(list: List<T>) {
        temp.addAll(list)
        value = temp
    }

    fun clear() {
        temp.clear()
        value = temp
    }
}
  • 위 클래스는 아주 간단한 함수들만 추가 했지만, List 에서 여러 "기능" 들에 대해서는 사용자 입맛에 맞게 만들면 되겠습니다. 

 

5. Fragment 에서 LiveData 사용시 유의 사항

  • 여기가 가장 중요한 부분입니다. 위에 있는 글을 읽고 LiveData 가 사용자에게 보여지는 시점에 "observer" 함수가 선언된 위치에 "최신값" 을 콜백처리합니다.
    위 내용에는 빠뜨렸지만,  Bidning -> Activity or Fragment 에서도 setLifecycleOwner 를 하면서 Layout 안에 LiveData 를 받아서 처리하는 부분이 제대로 업데이트 되도록 합니다.
binding = DataBindingUtil.setContentView<ATestRecyclerViewBinding>(
            this,
            R.layout.a_test_recycler_view
        ).apply {
        // DataBinding 에서의 "setLifecycleOwner" 처리입니다.
       lifecycleOwner = this@TestRecyclerViewActivity
       setVariable(BR.vm,viewModel)
}
  • 하지만,Lifecycle 이 복잡한 Fragment 에서는 몇가지 유의해야 하는 상황이 있습니다.
    • 하나의 액티비티 안에 여러개의 Fragment 를 onCreateView, onDestoryView 를 처리하면서 LiveData 의 고유 특성 때문에 조심해야 하는 부분이 있습니다. 흔히 자주 사용하는(?) Fragment Navigation 을 예시로 보여드리겠습니다.
     

  • 위와 같은 구조에서 ViewModel 를 통해 버튼 클릭 이벤트를 처리한다면..? 어떻게 될까요??
  •  
ViewModel.kt
class SieunViewModel : ViewModel() {

    val startQtzzFragmentEvent: MutableLiveData<Unit> by lazy { MutableLiveData() }

    fun onMoveFragment() {
        startQtzzFragmentEvent.value = null
    }
}

Fragment.kt -> onViewCreated
with(viewModel) {
	startQtzzFragmentEvent.observe(viewLifecycleOwner) {
    	findNavController().navigate(R.id.qtzzFragment)
    }
}
  • 위와 같이 처리하고 나서 SieunFragment -> QtzzFragment 로 전환후 "뒤로가기" 를 실행하는 경우 SieunFragment 로 절대 가지 못합니다..그 이유는 이전에 있던 Fragment 에 선언된 ViewModel 에 LiveData 에 데이터가 남아있기 때문입니다.
  • Fragment Navigation 특성상 이전에 있던 Fragment를 제대로 Destory 하지 않기 때문에 ViewModel 객체들은 어딘가에 남아 있습니다. 그러다 보니 뒤로가기를 선택하여 이전 Fragment 로 넘어가는 경우 onDestoryView -> onCreateView -> onViewCreated 상태로 넘어가면서 "observe" 로 함수를 다시 받아서 해당 LiveData 의 최신값을 다시 받게 됩니다. 
  •  

LiveData 는 onViewCreated 에 리스너를 달아서 처리하기때문에 Fragment A, B가 무한 반복 됩니다.

  • 아주 간단한 해결책으로는 xml 에서 onClick 할때 처리를 Fragment 자체적으로 처리하는 방법이 있습니다.
    fun onMoveAppleFragment(){
        findNavController().navigate(R.id.appleFragment)
    }
  • 또는 SingleLiveEvent 라고 java atomicBoolean 을 이용한 LiveData 를 이용하는 방법이 있습니다.
class SingleLiveEvent<T> : MutableLiveData<T>() {

    private val isPending = AtomicBoolean(false)

    /**
     * 값이 변경되면 false였던 isPending이 true로 바뀌고,
     * Observer가 호출됩니다.
     */
    @MainThread
    override fun setValue(value: T?) {
        isPending.set(true)
        super.setValue(value)
    }

    /**
     * 2. 내부에 등록된 Observer는 isPending이 true인지 확인하고,
     *    true일 경우 다시 false로 돌려 놓은 후에 이벤트가 호출되었다고 알립니다.
     */
    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner) {
            if (isPending.compareAndSet(true, false)) {
                observer.onChanged(it)
            }
        }
    }

    /**
     * T가 Void일 경우 호출을 편하게 하기 위해 있는 함수입니다.
     */
    @MainThread
    fun call() {
        value = null
    }
}
  • 이럴듯 ViewModel 은 오랫동안 데이터를 가지고 있어서 장점이 있지만, 또한 단점아닌 단점이 될수도 있습니다. LiveData 는 최신의 데이터를 onResumed 상태일때 콜백을 무조건 전달하기 때문에 위와 같은 경우에는 단점이 될수 있습니다.
  • 사실 위와 같은 이슈는 Fragment Navigation 안쓰고 자체 FragmentManager 를 사용하면 AKA. replace, addBackStack을 적절히 사용하면 해결할수 있겠습니다.

6. 끝으로

  • LiveData 는 Activity Lifecycle 에 의존하고 화면이 보여지는 상태(onResume) 일때만 "observer" 함수가 선언된 곳에 콜백한다.
  • MutableLiveData 를 잘 활용하여 ListLiveData, SingleLiveEvent 이런 여러 커스텀한 클래스를 만들수 있다.
  • LiveData 는 항상 최신값을 계속 저장하고 있습니다. "observer" 함수에 호출했다고 해서 그 뒤로는 같은값이 호출 안되는건 아닙니다.
  • LiveData 에서 콜백을 받고 싶다면 setLIfecycleOwner 를 무조건 해야 한다.

 

7. 참고 문헌

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

 

Pluu Dev - Fragment Lifecycle과 LiveData

[요약] Performance best practices for Jetpack Compose (Google I/O '22) Posted on 24 Jun 2022 [요약] Lazy layouts in Compose (Google I/O '22) Posted on 19 Jun 2022 [발표 자료] Modern Android Developer Posted on 14 Jun 2022 [발표 자료] Whats new

pluu.github.io

관련 코드는 아래 링크 클릭하면 되겠습니다.

https://github.com/sieunju/liveDataSample

 

GitHub - sieunju/liveDataSample: LiveData 샘플 예제입니다.

LiveData 샘플 예제입니다. Contribute to sieunju/liveDataSample development by creating an account on GitHub.

github.com

 

제가 만든 안드로이드 개발자라면 꼭 겪는 딥링크 테스트에 필요한 앱소개입니다. 많은 애용 바랍니다 :)

https://jsieun73.tistory.com/185

 

[안드로이드] 딥링크 테스트 어플 소개

안녕하세요. 이번에는 제가 딥링크 테스트를 하고 있다가 너무 귀차니즘이 발동하여 앱을 하나 만들었는데 한번 소개해볼까 합니다. https://play.google.com/store/apps/details?id=com.hmju.deeplink 퀵딥링크(Q.

jsieun73.tistory.com

 

반응형