안드로이드) LiveData

밍나·2022년 5월 24일
0

Android

목록 보기
33/36

✏️ LiveData

1. LiveData

  • LiveData는 관찰 가능한 데이터 클래스다.
  • 데이터 바인딩의 Observable 클래스나 Rxjava의 Observable과는 달리 LiveData는 Lifecycle을 통해 생명 주기를 인식한다.
    • 즉 액티비티, 프래그먼트, 서비스와 컴포넌트들의 생명 주기를 따른다.

2. LiveData 동작 과정

  • LiveData는 데이터의 변경을 활성화된 관찰자(Observer)를 통해 알린다.
  • 주어진 LifecycleOwner의 생명 주기가 STARTED 또는 RESUME 상태인 경우만 Observer를 활성(active) 상태로 간주한다.
  • LifecycleOwner 인터페이스를 구현하는 객체를 매개 변수로 하는 observe() 메서드를 통해 옵서버를 등록할 수 있다.

3. LiveData의 장점

(1) UI와 데이터 상태의 동기화

  • LiveData는 옵서버 패턴을 따른다.
  • LiveData는 생명 주기 상태 변화를 Observer에게 알린다. Observer 객체에서 데이터 변경에 따른 UI를 갱신하려면 코드를 작성해야 한다.

(2) 메모리 누수 방지

  • Observer는 Lifecycle에 바인딩 되며, 생명 주기 상태가 DESTROYED 되면 자동으로 옵서버가 제거되어, 컴포넌트의 생명 주기에 따른 리소스 해제 코드를 작성할 필요가 없다.

(3) 액티비티가 갑작스럽게 종료되어도 안전하다

  • 액티비티가 백스택으로 들어가는 경우와 같이 관찰자가 비활성화된 상태일때라도, LiveData로부터 어떠한 이벤트도 받지 않아 안전하다.

(4) 생명 주기에 대한 고민 X

  • UI 컴포넌트들은 UI에 표현할 데이터를 관찰할 뿐 생명 주기 상태가 중지(Stopped)인지 재개(Resumed)인지 걱정할 필요가 없다.
  • LiveData에 LifecycleOwner를 위임해 자동으로 모든 것을 관리한다.

(5) 최신의 데이터를 유지한다

  • 생명 주기가 비활성화되면 데이터 변경을 감지하지 않지만, 생명 주기가 활성화 되는 시점에 최신 데이터를 다시 가져온다.
  • 따라서 액티비티가 백그라운드인 상태에서 데이터가 변경되도, 액티비티가 포그라운드 상태가 될 때 최신의 데이터를 바로 받는다.

(6) 구성 변경에 대응한다

  • 액티비티 또는 프래그먼트가 화면 회전 등과 같은 구성 변경으로 인해 재생성되어도 즉시 최신 데이터를 받을 수 있다.

(7) 자원 공유

  • LiveData를 상속하여 싱글턴 패턴으로 사용할 수도 있다.
  • 안드로이드 시스템 서비스와 같은 곳에 한 번만 연결하고, 애플리케이션 내 어디에서나 다중으로 접근해 이 서비스를 관찰할 수 있다.

✏️ MutableLiveData를 이용한 데이터 쓰기

1. MutableLiveData 예제

  • LiveData는 데이터 읽기만 가능하므로 데이터를 쓰려면 MutableLiveData를 사용한다.
classs MainActivity: AppCompatActivity() {
    var liveString = MutableLiveData<String>()
    
    override fun onCreate(saveInstanceState: Bundle?) {
        super.onCreate(saveInstanceState)
        
        liveString.postValue("Hello Minha") //데이터 쓰기
        liveString.value = "Hello World" //데이터 쓰기
        
        liveString.observe(this) {
            // 어떤 값이 먼저 들어올까?
        }
    }
}
  • MutableLiveData가 가진 데이터를 쓰는 메서드는 setValue()와 postValue() 메서드가 있다.
  • 데이터를 직접 읽으려면 getValue() 메서드를 사용하면 된다.
  • setValue
    • setValue()는 반드시 메인 스레드에서만 호출해야 한다. 백그라운드에서 사용하는 경우 IllegalStateException이 발생한다.
    • 이미 활성화된 Observer를 가진다면, 변경된 데이터를 onChanged() 콜백 메서드로부터 얻을 수 있다.
  • postValue
    • postValue()는 주로 백그라운드 스레드에서 호출하는 용도로 사용된다.
    • 주어진 값을 설정하는 태스크를 내부에서 핸들러를 통해 메인 스레드에 전달하기 때문이다.
    • 그러므로 메인 스레드가 실행되기 전 postValue()를 여러 번 호출해도 가장 마지막에 설정된 값만 가져온다.
  • 예제로 돌아와서 코드의 순서를 살펴보면 "Hello Minha"를 먼저 설정하고 "Hello World"를 그 뒤에 설정했다.
  • onCreate() 내부의 예제 코드는 메인 스레드에서 실행되는데, postValue()의 경우 값을 즉시 설정하지 않고 작업을 뒤로 미룬다.
  • 그다음 setValue()가 호출되고 메인 스레드에서 즉시 값을 설정하므로, onChanged(String)에 값이 들어오는 순서를 확인하면 Hello World가 먼저 들어오는 것을 확인할 수 있다.

2. MutableLiveData 초기값 설정

  • MutableLiveData를 생성한 직후는 초깃값이 null이다.
  • 이를 방지하고자 아래와 같이 생성자 매개 변수에서 초깃값을 적용하면 null로부터 안전하게 생성할 수 있다.
class InitMutableLiveData<T>(initValue: T) : MutableLiveData<T>() {
    init {
        value = initValue
    }
}

✏️ 상속을 통한 LiveData 사용하기

  • LiveData는 Observer의 생명 주기가 STARTED 또는 RESUMED인 경우 활성화 상태로 간주한다.
  • 아래의 코드는 주식 가격의 변동을 LiveData 클래스로 확장하는 방법을 보여준다.
class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
    private val stockManager = StockManager(symbol)

    private val listener = { price: BigDecimal ->
        value = price
    }

    override fun onActive() {
        stockManager.requestPriceUpdates(listener)
    }

    override fun onInactive() {
        stockManager.removeUpdates(listener)
    }
}
  • 이 예제에서 가격을 나타내는 리스너의 구현은 메서드의 호출에 따라 이루어진다.
    • onActive() : LiveData에 활성화된 관찰자가 있는 경우 호출된다. 즉 주식 가격의 변동을 이때부터 관찰하고 LiveData에 값을 설정한다.
    • onInactive() : LiveData에 활성화된 관찰자가 전혀 없는 경우 호출된다. 관찰자가 없으므로 StockManager에 연결을 유지할 필요가 없다.
    • value = price : LiveData의 값을 갱신할 때 호출되며, 활성화된 관찰자들에게 변경에 대해 알린다.

  • StockLiveData는 다음과 같이 사용할 수 있다.
public class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        val myPriceListener: LiveData<BigDecimal> = ...
        myPriceListener.observe(viewLifecycleOwner, Observer<BigDecimal> { price: BigDecimal? ->
            // UI를 갱신
        })
    }
}

  • LiveData 객체가 생명 주기를 안다는 것은, 여러 액티비티, 프래그먼트, 서비스들 사이에서 이 객체를 공유할 수 있다는 것을 의미한다.
  • 아래는 LiveData를 싱글턴으로 구현한 예제이다.
class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
    private val stockManager: StockManager = StockManager(symbol)

    private val listener = { price: BigDecimal ->
        value = price
    }

    override fun onActive() {
        stockManager.requestPriceUpdates(listener)
    }

    override fun onInactive() {
        stockManager.removeUpdates(listener)
    }

    companion object {
        private lateinit var sInstance: StockLiveData

        @MainThread
        fun get(symbol: String): StockLiveData {
            sInstance = if (::sInstance.isInitialized) sInstance else StockLiveData(symbol)
            return sInstance
        }
    }
}

  • 또한 아래와 같이 프래그먼트에서 사용할 수 있다.
  • 여러 프래그먼트에 액티비티가 MyPriceListener 인스턴스를 관찰할 수 있다.
class MyFragment : Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        StockLiveData.get(symbol).observe(viewLifecycleOwner, Observer<BigDecimal> { price: BigDecimal? ->
            // UI 갱신
        })

    }

✏️ MediatorLiveData 사용하기

  • MediatorLiveData는 MutableLiveData의 하위 클래스로 다른 여러 LiveData를 관찰하고 데이터의 변경에 반응한다.
  • 이 클래스는 자신의 활성화/비활성화 상태를 연결된 소스들에 전파한다.
    • 예를 들어 두 개의 LiveData 인스턴스가 있을 때, 이 둘의 데이터 변경 내용을 하나의 LiveData로 관리하고 싶다면 MediatorLiveData에 두 개의 LiveData를 소스로 추가하면 된다.
var liveData1: LiveData = ...
var liveData2: LiveData = ...

val liveDataMerger: MediatorLiveData<*> = MediatorLiveData<Any>()
liveDataMerger.addSource(liveData1) { value: Any? -> liveDataMerger.setValue(value)}
liveDataMerger.addSource(liveData2) { value: Any? -> liveDataMerger.setValue(value)}
  • liveData1에 의한 데이터 변경이 10번 발생했을 때 리스닝을 멈추고자 MediatorLiveData로부터 liveData1 소스를 제거할 수도 있다.
liveDataMerger.addSource(liveData1, object : Observer() {
    private var count = 1
    fun onChanged(@Nullable s: Int?) {
        count++
        liveDataMerger.setValue(s)
        if (count > 10) {
            liveDataMerger.removeSource(liveData1)
        }
    }
})

✏️ LiveData 변형하기

1. LiveData 변형하기

  • LiveData에 저장된 값을 관찰자로 전달하기 전에 이 값을 변경하거나 또는 다른 타입의 LiveData 인스턴스로 전달하고 싶을 수 있다.
  • 이를 위해 lifecycle 패키지에서는 Transformations라는 클래스를 제공한다.

2. Transformations.map() 메서드

  • 첫번째 매개변수에서 새로운 타입의 LiveData를 만들 수 있고, 두번째 매개변수에서 소스값을 원하는 새로운 값으로 반환한다.
  • 아래의 예제에서 userLiveData가 변경될 때마다 userName 또한 데이터가 변경된다.
val userLiveData: LiveData<User> = UserLiveData()
val userName: LiveData<String> = Transformations.map(userLiveData) {
    user -> "${user.name} ${user.lastName}"
}

3. Transformations.switchMap() 메서드

  • map()과 비슷하지만 mapFunction에서 변형시킨 데이터를 LiveData로 반환하는 점이 다르다.
private fun getUser(id: String): LiveData<User> {
  ...
}

val userId: LiveData<String> = ...
val user = Transformations.switchMap(userId) { id -> getUser(id) }

✏️ 데이터 바인딩과 LiveData의 사용

1. 데이터 바인딩과 LiveData의 사용

  • 데이터 바인딩의 가장 중요한 기능 중 하나가 관찰성이다.
  • 데이터 바인딩과 LiveData를 사용하면 생명 주기에 대한 걱정 없이 데이터의 변경에 따른 UI 변경을 자동으로 처리하도록 설정할 수 있다.

2. Observable 필드를 LiveData로 마이그레이션하기

  • Observable 필드와 달리 LiveData는 생명 주기를 알고 관찰자들에게 데이터를 올바르게 전파한다.
  • Android Studio 3.1 이상부터는 Observable 필드 대신 LiveData를 데이터 바인딩 코드에서 사용하는 것을 권장한다.
  • LiveData와 바인딩 클래스를 같이 사용하려면 바인딩 클래스에 LifecycleOwner를 명시하여, 생명 주기를 인식하고 이에 따라 LiveData가 반응할 수 있도록 해야 한다. 방법은 아래와 같다.
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: ActivityMainBinding = ...
        binding.setLifecyclerOwner(this)
    }
}
  • 바인딩 클래스에 LifecyclerOwner를 명시했다면, 레이아웃에 선언된 변수를 LiveData로 교체해야 한다. 기존에 ObservableField를 사용하는 레이아웃으로부터 LiveData를 사용하는 레이아웃으로 마이그레이션 해본다.
<!-- LiveData 사용 전 -->
<data>
    <variable
        name="name"
        type="androidx.databinding.ObservableField&lt;String>" />
</data>
...
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@{name}" />
<!-- LiveData 사용 후 -->
<data>
    <variable
        name="name"
        type="androidx.lifecycle.LiveData&lt;String>" />
</data>
...
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@{name}" />
  • 일반적으로는 레이아웃에서 뷰-모델 변수를 선언하고, 뷰-모델이 가진 멤버로 LiveData를 참조하는 것을 권장한다.
  • LiveData에서는 원시 타입을 다룰 수 없어 박스 클래스를 제네릭으로 참조하는데, 이를 바인딩 표현식에서 사용하는 경우 언박싱 과정에서 NPE를 방지하도록 safeUnbox()를 사용하라는 메세지가 뜰 수 있다. 그 경우 아래와 같이 작성한다.
app:userAge="@{safeUnbox(viewModel.age)}"
  • 바인딩 표현식에서 LiveData를 사용하는 경우 바인딩 클래스 내부에서 getValue()를 호출해 데이터를 참조한다.

3. LiveData를 사용하면서 양방향 바인딩 사용하기

  • setter 메서드의 추가로 양방향 바인딩의 구현과 LiveData의 사용을 동시에 할 수 있다.
  • 주의할 점은 메서드 시그니처다.
    • 메서드의 이름과 바인딩 표현식에서 참조하는 멤버 이름이 일치해야 한다.
    • 매개 변수의 타입이 LiveData의 제네릭 타입과 일치해야 한다.
class UserViewModel : BaseObservable() {
    val name = MutableLiveData<String>()
    fun setName(name: String) {
        this.name.value = name
    }
}
<data>
    <variable
        name="viewModel"
        type="com.example.UserViewModel" />
</data>
...
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@{viewModel.name}" />
profile
🤗🤗🤗

0개의 댓글