728x90


📌  LiveData란?

class MainActivity : AppCompatActivity() {
 
    override fun onCreate(savedInstanceState: Bundle?) {
    	...
        
        DB에서 초기 아이템 목록 호출
        UI 업데이트
        
        추가 버튼 클릭 리스너 {
            아이템 추가
            UI 업데이트
        }
        
        삭제 버튼 클릭 리스너 {
            아이템 삭제
            UI 업데이트
        }
    }
}

위 코드의 문제가 무엇일까?

추가 버튼을 누르면 아이템이 추가되고 UI 가 업데이트된다. 마찬가지로 삭제 버튼을 누르면 아이템이 삭제되고 UI 가 업데이트된다.

 

만약 아이템이 추가되는 작업이 엄청나게 가볍고 빠른 작업이라면 상관없겠지만, DB 단, 혹은 네트워크 단에서 작업이 이루어진다면 분명 UI 업데이트보다 작업이 오래 걸리게 될 것이다.

그렇게 된다면 실제 데이터와 UI가 일치하지 않는 상황이 올 수도 있다.

 

 

아이템 추가나 삭제 같은 데이터 변경을 요청했을 때 누군가가 데이터의 변경이 완료되기를 기다리고 있다가 나에게 알려주는 것이 Observer이다.

안드로이드에서의 Observer데이터가 변경되는지 감시하고 있다가 UI 컨트롤러 (Activity or Fragment)에게 알려준다.

private val observableString = ObservableField<String>("Default Value")

observableString.addOnPropertyChangeCallback(object : Observable.OnPropertyChangedCallBack()) { // 콜백함수를 따로 구현
    override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
        // Do SomeThing
     }
}

 

하지만 위의 코드 즉 Observable은 LifeCycle을 알 수 없으므로 등록한 콜백이 상시 작동되어야 한다. 또한 작동이 필요 없어지면 removeOnPropertyChangedCallback을 호출하여 콜백을 수동으로 직접 제거해야 한다는 번거로움이 있다. 

 

이 같은 문제를 해결하기 위해서 안드로이드에서 사용하는 것이 LiveData이다.

 

📌 LiveData

  • Android Jetpack 라이브러리의 하나의 기능
  • LiveData는 Data의 변경을 관찰할 수 있는 Data Holder 클래스
  • 일반 Obserable한 클래스와 달리 LiveData는 안드로이드 생명주기를 알고 있다.
  • LiveData는 Observer 객체와 함께 사용되는데, LiveData가 가지고 있는 데이터에 어떠한 변화가 일어날 경우, LiveData는 등록된 Observer 객체에 변화를 알려주고, Observer의 onChanged() 메서드가 실행되게 된다.

 

🤷‍♂️ LiveData가 어떻게 생명주기를 아는 것일까?

  • LifeCycleOwner를 이용한다.
  • LifeCycleOwner안드로이드 생명주기를 알고 있는 클래스이며 메서드가 오직 getLifeCycle() 밖에 없는 단일 메서드 인터페이스 클래스이다.
  • Activity나 Fragment가 LifeCycleOwner를 상속받고 있다.
  • LiveData의 Observer 메서드의 LifeCycleOwner를 Activity나 Fragment를 변수로써 사용한다면 각 화면 별 생명주기에 따라 LiveData는 자신의 임무를 수행한다.

 

 

 

간단한 코드로 예시를 들자.

var money = 10000

 

만약 지갑에 있는 돈을 실시간으로 감시하고 싶다면

var money = MutableLiveData<Int>() // 타입을 명시해 주어야 한다.

money.value = 10000

 

MutableLiveData 객체를 생성하고, value를 통해 값을 넣으면 이제 이 돈 즉 money라는 데이터는 Observer 가 감시할 수 있는 데이터가 되는 것이다.

 

 

 

📌  LiveData의 장점

  • Data와 UI간의 동기화
    • 데이터의 변화가 일어나는 곳마다 매번 UI를 업데이트하는 코드를 작성할 필요 없이 통합적이고 확실하게 데이터의 상태와 UI를 일치시킬 수 있다.
  • 메모리 누수(Memory Leak)가 없다.
    • Observer 객체는 안드로이드 생명주기 객체와 결합되어 있기 때문에 컴포넌트가 Destroy 될 경우 메모리상에서 스스로 해제한다.
  • Stop 상태의 Activity와 Crash가 발생하지 않는다.
    • 액티비티가 Back Stack에 있는 것처럼 Observer의 생명주기가 inactive(비활성화) 일 경우, Observer는 LiveData의 어떤 이벤트도 수신하지 않는다.

 

 

 


 

 

📌  LiveData로 계산기 만들기

EditText에 숫자를 넣고 더하기 빼기 버튼을 누르면 실시간으로 계산된 값을 보여주는 계산기를 LiveData를 이용해 구현해보자.

 

activity_main.xml 작성

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="10dp"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/main_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingBottom="40dp"
        android:text="@string/zero"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/edit_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toStartOf="@+id/plus_button"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/main_text"
        android:hint="@string/write_number"/>

    <Button
        android:id="@+id/plus_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="10dp"
        android:text="@string/plus"
        app:layout_constraintEnd_toStartOf="@+id/minus_button"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/edit_text"
        app:layout_constraintTop_toBottomOf="@+id/main_text" />

    <Button
        android:id="@+id/minus_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/minus"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/plus_button"
        app:layout_constraintTop_toBottomOf="@+id/main_text" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

위와 같이 EditText 에 숫자를 넣고 더하기 버튼 혹은 빼기 버튼을 누르면 계산된 값을 TextView로 보여주는 계산기 UI를 간단히 만들어 보았다.

 

 

ViewModel & LiveData 의존성 주입

기본적으로 안드로이드 스튜디오에서 프로젝트를 만들고 ViewModel 및 LiveData를 사용하려고 하면 import 가 안될 것이다. 그렇기 때문에 의존성 주입이 필요하다. 

 

https://developer.android.com/jetpack/androidx/releases/lifecycle

 

Lifecycle  |  Android 개발자  |  Android Developers

Lifecycle 수명 주기 인식 구성요소는 활동 및 프래그먼트와 같은 다른 구성요소의 수명 주기 상태 변경에 따라 작업을 실행합니다. 이러한 구성요소를 사용하면 잘 구성된 경량의 코드를 만들어

developer.android.com

 

공식문서에 들어가 보면 의존성을 주입하기 위해 build.gradle 에 어떤 코드를 추가해야 하는지 주석과 함께 자세히 나와있다.

 

 

공식문서에 나와 있는 대로 의존성을 추가해준다.

dependencies {
    def lifecycle_version = "2.5.0-alpha05"

    // com.example.livedatatest.ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    // LiveData
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}

 

 

 

ViewModel 클래스 작성

package com.example.livedatatest

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class ViewModel : ViewModel() {
    private val _numberList = MutableLiveData<Int>()

    val numberList: LiveData<Int>
        get() = _numberList

    init {
        _numberList.value = 0
    }

    fun calculate(state: ButtonState, number: Int) {
        when (state) {
            ButtonState.PLUS ->
                _numberList.value = _numberList.value?.plus(number)
            ButtonState.MINUS ->
                _numberList.value = _numberList.value?.minus(number)
        }
    }
}

 

 

MutableLiveData와 LiveData를 구분해서 사용하는 이유를 알아보자.

  • MutableLiveData : 값의 get/set 모두를 할 수 있다.
  • LiveData : 값의 get()만을 할 수 있다.

Mutable은 코틀린 언어에서 흔하게 볼 수 있는 내용인데, 값의 get/set 모두를 허용하기에, 값의 변경과 읽기 모두 가능하다.

반대로 LiveData는 읽기 전용의 데이터를 만들기에 값의 변경은 불가능하다.

 

 

이런 기법은 ViewModel과 View의 역할을 분리하기 위함이라 이렇게 사용한다. ViewModel은 언제나 새로운 값의 변경이 일어나고, 다시 읽을 수 있는 형태로 사용하는 것이고, View는 값의 입력이 아닌 읽기만을 허용하는 것이다.

즉, UI 컨트롤러(액티비티, 프래그먼트)가 값을 직접 수정하지 못한다.

 

 

또한 MutableLiveData 앞에는 _ 가 붙은 네이밍 컨벤션을 확인할 수 있고 LiveData는 커스텀 게터를 통해 MutableLiveData에 접근하고 있는데, 이것은 

https://kotlinlang.org/docs/coding-conventions.html#names-for-backing-properties

 

Coding conventions | Kotlin

 

kotlinlang.org

 

코틀린 공식 홈페이지의 backing properties 관련 코틀린 컨벤션에 따른 표현이다.

 

 

 

ButtonState 클래스 & calculate 메서드

package com.example.livedatatest

enum class ButtonState {
    PLUS, MINUS
}

 

fun calculate(state: ButtonState, number: Int) {
        when (state) {
            ButtonState.PLUS ->
                _numberList.value = _numberList.value?.plus(number)
            ButtonState.MINUS ->
                _numberList.value = _numberList.value?.minus(number)
        }
    }

ViewModel 클래스의 calculate 메서드를 보면 버튼의 상태에 따라 MutableLiveData의 값을 새롭게 초기화해주고 있다.

 

 

 

MainActivity

package com.example.livedatatest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.ViewModelProvider
import com.example.livedatatest.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity(){
    private val binding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    private lateinit var viewModel: ViewModel

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

        viewModel = ViewModelProvider(this)[ViewModel::class.java]
        viewModel.numberList.observe(this) {
            binding.mainText.text = it.toString()
        }

        binding.plusButton.setOnClickListener {
            val input = binding.editText.text.toString().toInt()
            viewModel.calculate(state = ButtonState.PLUS, input)
        }

        binding.minusButton.setOnClickListener {
            val input = binding.editText.text.toString().toInt()
            viewModel.calculate(state = ButtonState.MINUS, input)
        }
    }
}

우선 뷰 바인딩 처리를 해주고 ViewModel을 lateinit으로 지연 초기화로 선언해준다.

 

 

ViewModel을 사용하려면 ViewModelProvider 가 필요하다. 따라서 ViewModelProvider에 필요한 인자 값 즉 context와 ViewModel::class.java를 넣어주고 ViewModel 클래스의 LiveData 인 numberList를 observe 즉 관찰해준다.

 

 

여기서 MainActivity 즉 View는 데이터를 변경할 수 없이 읽기만 가능한 LiveData 타입을 관찰한다.

이것이 ViewModel에서 MutableLiveData 인 _numberList는 private 처리를 해주고 LiveData 인 numberList는 public으로 지정된 이유이다.

 

 

그다음 버튼의 setOnClickListener를 통해 ViewModel에서 구현했던 데이터를 업데이트해주는 calculate 함수를 상황에 맞게 호출해 주면 완성이다.

 

 

 

 

📱 결과물

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Reference:

https://www.youtube.com/watch?v=-b0VNKw_niY 

https://thdev.tech/android/2021/02/01/LiveData-Intro/

https://todaycode.tistory.com/49

 

복사했습니다!