Android

[안드로이드] LiveData에 관하여

나맘임 2023. 8. 24. 21:47

안드로이드 개발을 진행할 때 꼭 보게 되는 친구가 있습니다.

 

바로 LiveData 인데

 

보통 ViewModel에 LiveData를 저장한 뒤 데이터 바인딩을 사용해 UI와 결합시키고

 

그 값을 계속해서 Observe 하여 UI를 갱신합니다.

 

처음 LiveData를 접할 땐 그냥 좋으니까 쓰면 된다 이런 식으로 배워서 아무 생각없이 쓰고 있었습니다.

 

그러다가 문득 이 LiveData에 관해 궁금해져서 공부 겸 이 글을 쓰게 되었습니다.

 


LiveData란?

Android Jetpack 라이브러리의 일부로, 안드로이드 수명 주기를 관찰 가능한 데이터 홀더 클래스

 

라고 안드로이드 공식 문서에서 정의하고 있습니다.

 

클래스이긴 한데 관찰(Observe)을 한다는데 이게 뭔 말일까요??

 

이걸 이해하기 위해선 옵저버(Observer) 패턴을 이해 해야합니다.

 


옵저버(Observer) 패턴?

디자인 패턴의 일종으로 객체의 상태 변화를 관찰하는 관찰자인 옵저버가 존재하고 이 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메소드 등을 통해 객체가 직접 목록의 각 옵저버에 통지하도록 하는 패턴

위키피디아의 정의인데 말이 너무 어렵습니다.

 

예를 들어 쉽게 풀어보겠습니다.

 

여러분이 어느 언론사 A의 앱을 설치하여 구독하고 있다고 생각해봅시다.

 

이 언론사 앱은 언론사 A가 새로운 뉴스나 칼럼을 개시할 때마다 여러분에게 알림을 보내줍니다.

 

여기서 여러분을 옵저버, 언론사 A를 이벤트를 발생시키는 객체, 앱을 객체의 변화를 탐지하는 주체라고 할 수 있습니다.

 

옵저버가 관찰하고 있는 객체인 언론사 A가 상태 변화(새로운 뉴스 개시)가 발생할 때마다

 

옵저버의 목록을 가진 주체(언론사 앱)는 여러분(옵저버)에게 알림을 보냅니다.

 

그러면 여러분은 이 뉴스를 보게 되겠죠(메소드 등을 실행)

즉,

여러분은 (옵저버)

언론사 A를 구독하고 있고 (관찰)

새로운 뉴스가 나올 때 (상태 변화 발생)

그 뉴스를 보게 됩니다 (특정 코드 실행)

 

더 상세히 들어가면

 

옵저버를 등록하는 있는 객체와 주체가 같은 경우, 주체와 객체를 따로 두지 않는 경우 등등 

 

너무 머리가 아픕니다.

 

그냥 간단하게 생각하시면 됩니다.

'어느 객체를 관찰하고 있다가 그 객체의 변화가 생기면 어느 행동을 취한다'

즉, 옵저버 패턴은 특정 이벤트가 발생하고 그에 따른 처리가 필요할 때 많이 사용합니다.

 


이제 다시 LiveData로 돌아가보겠습니다.

Android Jetpack 라이브러리의 일부로, 안드로이드 수명 주기를 관찰 가능한 데이터 홀더 클래스

아까 LiveData의 정의가 위와 같다고 했습니다.

 

LiveData는 옵저버를 통해 관찰이 가능함과 동시에 안드로이드의 수명 주기를 고려합니다.

 

즉, 옵저버도 등록이 가능하고 안드로이드 수명 주기를 나타내는 LifecycleOwner 객체를 동시에 등록할 수 있습니다.

 

LifecycleOwner는 수명 주기를 나타내는 Lifecycle 객체를 반환하는 인터페이스로 액티비티, 프래그먼트 등의 수명 주기를 가지고 있습니다.

LifecycleOwner는 다음을 참고해주세요.

수명 주기 인식 구성요소로 수명 주기 처리  |  Android 개발자  |  Android Developers

 

수명 주기 인식 구성요소로 수명 주기 처리  |  Android 개발자  |  Android Developers

새 Lifecycle 클래스를 사용하여 활동 및 프래그먼트 수명 주기를 관리합니다.

developer.android.com

 


그러면 왜 LiveData를 ViewModel에 많이 사용 할까요??

1. LiveData는 수명 주기를 자동으로 처리합니다.

ViewModel은 UI 관련 데이터를 관리하고 있기 때문에 프래그먼트, 액티비티 수명 주기와 연결되어 있고 이 생명 주기에 따라 관찰자에게 데이터 변경을 알릴 수 있습니다.

즉, 수명 주기를 수동으로 처리할 필요성이 사라집니다.

 

2. 메모리 누수와 비정상적인 종료가 존재하지 않습니다.

연결된 프래그먼트, 액티비티가 중지가 되면 자동으로 관찰자에게 데이터 변경을 알리지 않기 때문에 메모리 누수와 비정상적인 종료가 존재하지 않습니다.

3. 최신 데이터를 유지할 수 있습니다.

화면 구성의 변경(화면 회전 등)에도 데이터를 유지할 수 있습니다. 1번과 비슷한 맥락으로 화면 구성의 변경이 발생하면 수명 주기가 변경이 되고 그에 맞춰 관찰자에게 변경 사항을 알리기 때문에 계속해서 데이터를 갱신해줍니다.

따라서 화면이 재구성되더라도 최신 데이터를 유지할 수 있습니다.

 

 


LiveData를 ViewModel 말고 다른 곳에 사용할 순 없나요?

1. Room DB 예시

class UserDao: Dao {
    @Query("SELECT * FROM User WHERE id = :id")
    fun getUser(id: String): LiveData<User>
}

class MyRepository {
    fun getUser(id: String) = liveData<User> {
        val disposable = emitSource(
            userDao.getUser(id).map {
                Result.loading(it)
            }
        )
        try {
            val user = webservice.fetchUser(id)
            disposable.dispose()
            userDao.insert(user)
            emitSource(
                userDao.getUser(id).map {
                    Result.success(it)
                }
            )
        } catch(exception: IOException) {
            emitSource(
                userDao.getUser(id).map {
                    Result.error(exception, it)
                }
            )
        }
    }
}

2. 코루틴을 이용하여 비동기 처리 예시

val user: LiveData<User> = liveData {
    val data = database.loadUser() 
    emit(data)
}

출처 : 안드로이드 공식 문서

 

Room DB와 코루틴을 사용하여 LiveData를 비동기 처리할 수 있습니다.

 

다만, 공식 문서에선 LiveData를 데이터 계층에서 사용하는 것을 권장하진 않고 있습니다.

이는 Flow라는 명백한 대체제가 존재하기 때문인데,

Flow는 코루틴 라이브러리의 한 유형으로 비동기 스트림, 데이터 스트림 결합 등 다양한 기능을 지원합니다.

또한 데이터 계층과 아키텍처 스타일 코드를 준수할 수 있다고 설명하고 있습니다.

UI계층에서 LiveData를 사용할 수 있도록 Flow.asLiveData() 메소드가 있어 Flow에서 LiveData로 변환까지 가능합니다.

예시

즉, ViewModel ~ UI Controller에서만 LiveData를 쓰고 데이터 계층에선 Flow를 사용하라고 권고하고 있습니다.


LiveData를 ViewModel에 어떻게 사용하나요?? (뷰 바인딩, 데이터 바인딩 사용)

결과물

먼저 데이터를 관리할 ViewModel부터 구성해보겠습니다.

MainViewModel.kt

class MainViewModel : ViewModel(){

    private val _count = MutableLiveData<Int>(0)
    val count : LiveData<Int> get() = _count


    fun upCount(){
        _count.value = count.value?.plus(1)
    }

    fun downCount(){
        _count.value = count.value?.minus(1)
    }


}

기본적으로 LiveData는 읽기만 가능합니다. 

수정을 하기 위해 사용하는 것이 MutableLiveData입니다.

LiveData와 MutableLiveData를 분리하여 사용하는 이유는 데이터 처리 로직과 UI를 분리하기 위함입니다.

 

데이터를 처리할 때는 _count 변수를 사용하고 UI에 데이터를 반영할 땐 count를 사용함으로써

데이터의 불변성을 지킬 수 있습니다.

메인 화면을 구성해보겠습니다.

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    private val mainViewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)


        binding.apply {
            viewModel = mainViewModel
            lifecycleOwner = this@MainActivity
        }


    }

}

뷰 바인딩, by viewModels()를 사용하였습니다.

by viewModels() 란?

ViewModel를 지연 생성하기 위한 방식 중 하나입니다.

ViewModelProvider를 사용하지 않을 수 있으나 gradle에 종속성을 추가해줘야 합니다.

ViewModel를 선언한 View(Activity, Fragemnt)의 수명 주기에 종속되는 것이 특징입니다.

여기서 중요한 것은 lifecycleOwner에 this로 해주는 겁니다.

 

이는 observer를 MainActivity로 설정한다는 의미로 MainActivity의 생명 주기를 탐지한다는 의미입니다.

 

이 때문에 데이터 바인딩을 사용할 수 있게 됩니다.

 

actitvity_main.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"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".MainActivity">

    <data>
        <variable
            name="viewModel"
            type="com.github.myapplication.MainViewModel" />
    </data>

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

        <TextView
            android:id="@+id/countTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.count.toString()}"
            android:textSize="100dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            />

        <Button
            android:id="@+id/upButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{()-> viewModel.upCount()}"
            android:text="up"
            android:layout_marginTop="10dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@id/downButton"
            app:layout_constraintTop_toBottomOf="@id/countTextView" />

        <Button
            android:id="@+id/downButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{()-> viewModel.downCount()}"
            android:text="down"
            android:layout_marginTop="10dp"
            app:layout_constraintStart_toEndOf="@+id/upButton"
            app:layout_constraintTop_toBottomOf="@id/countTextView"
            app:layout_constraintEnd_toEndOf="parent"/>


    </androidx.constraintlayout.widget.ConstraintLayout>


</layout>

데이터 바인딩을 사용하여 viewmodel를 가져왔습니다.

 

각각의 버튼엔 viewmodel의 메소드와 연결해두었고 텍스트뷰에 count 변수 값을 가져오게 하였습니다.