Study Record

[안드로이드] ViewModel 과 LiveData & Data Binding 본문

안드로이드

[안드로이드] ViewModel 과 LiveData & Data Binding

초코초코초코 2023. 7. 28. 12:06
728x90

😶 Android Jetpack Libraries & Android Architecture Components 

Android Jetpack 라이브러리는 안드로이드 앱을 더 쉽게 만들 수 있게 도와주는 라이브러리 모음이다. 사용 모범 사례와 플레이트 코드 작성을 자유롭게 하여 복잡한 작업을 단순화할 수 있다.

 

Android Architecture Components 는 Android Jetpack Libraries 의 일부로 좋은 아키텍처로 앱을 설계하는데 도움을 준다. 앱 아키텍처란 설계 규칙의 집합으로 앱의 개발 구조를 제공한다. 잘 사용하면 유연하고 확장 가능한 유지보수를 가능하게 만들 수 있다.

 

 

😶 ViewModel 개요

ViewModel 은 Architecture Components 중 하나로 앱에서 사용하는 데이터 상태를 관리한다. 구성 요소가 변경되거나 다른 이벤트 중 프레임워크가 Activity 나 Fragment 를 파괴하여 다시 생성돼도 저장된 데이터가 손실하지 않도록 할 수 있다.

 

+ ViewModel 을 사용하지 않고 손실된 데이터를 해결하는 방법은 onSaveInstanceState() 콜백을 사용할 수 있다. 하지만 이 메서드는 bundle 객체로 상태를 저장하고 복구하기 때문에 저장할 수 있는 데이터 양이 적다.

 

 

😶 Architecture guide 

Architecture(아키텍처)는 앱에서 Class 간 책임을 할당하는데 도움이 되는 지침을 제공한다.

 

 

관심사 분리(Separation of concerns)

앱을 각각 분리된 역할(책임)을 가지고 있는 Class 로 나눠야 한다.

 

 

Drive Ui from a model

지속가능한 model로부터 UI를 구성해야 한다. 여기서 model은 앱의 데이터를 관리하는 책임을 갖는 컴포넌트이다. 앱에서 View와 앱 컴포넌트들은 독립적이어야 하며 앱의 라이프사이클과 그와 관련된 것들에 영향을 받지 않는다.

 

안드로이드 아키텍쳐의 메인 클래스와 컴포넌트는 UI Controller(Fragment, Activity), ViewModel, Liveate and Room 등이 있다. 이 컴포넌트들은 라이브사이클 이슈를 피할 수 있게 해 준다.

 

UI Controller(Fragment, Activity)

Activity와 Fragment는 UI Controller로 화면에 View를 그리거나 이벤트를 캡처하거나 기타 UI와 관련된 모든 것을 제어한다. Android 시스템은 UI Controller 를 메모리 부족의 문제 혹은 사용자에 의해 언제든지 강제로 종료될 수 있다. 따라서 UI Controller 에는 앱의 데이터에 대한 로직이 있으면 안 된다. 데이터에 관한 것은 ViewModel에서 다룬다.

 

 

ViewModel

ViewModel은 View와 관련된 앱 데이터의 model이다. 여기서 model은 앱 데이터를 처리하는 역할을 하는 컴포넌트이다. ViewModel은 UI Controller 와 관련된 앱 데이터의 상태를 관리한다. Android 시스템이 Activity/Fragment를 종료해도 데이터가 자동으로 유지된다. 따라서 강제로  종료되고 생성된 Activity/Fragment가 즉시 데이터를 사용할 수 있다.

 

 

 

😶 ViewModel Lifecycle

ViewModel은 Activity 와 Fragment 의 모든 라이프사이클 범위에 존재한다. 화면 전환과 같은 앱 구성요소가 바뀔 때마다 없어지지 않는다. 단, 앱이 완전히 종료될 때는 같이 종료된다.

 

 

 

 

😶 ViewModel 사용하기

 

의존성 추가 (dependencies)

dependencies {
    // LiveData
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
    // ViewModel
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
}

 

 

ViewModel 상속받기 & 변수

ViewModel 은 ViewModel 클래스를 상속받아 정의할 수 있다. ViewModel에는 앱 데이터와 데이터에 관련된 로직을 책임진다고 했다. 다른 UI Controller(Activity/Fragment)나 외부 클래스에서도 ViewModel의 데이터에 직접적으로 접근하여 변경할 수 없다.

import androidx.lifecycle.ViewModel

class TestViewModel: ViewModel() {
    private var count = 0
}

위와 같이 private var 변수로 선언하면 외부 클래스에서 count 변수를 변경할 수 없지만 접근할수도 없게 된다. 그렇다고 public 키워드를 사용하기에는 외부에서 데이터를 변경할 가능성이 있다.

 

 

이런 문제를 해결하기 위해 Backing Property 라고 불리는 기술을 사용할 수 있다.

class TestViewModel: ViewModel() {
    private var _count = 0
    
    // Custom Getter
    val count: Int
        get() = _count
}

모든 프로퍼티에는 Setter와 Getter 가 자동으로 생성되는데 변수를 선언할 때 Getter와 Setter를 커스텀할 수 있다. _count 변수는 private var 로 선언하여 ViewModel 내에서만 변경할 수 있게 하고 count 변수는 val 로 선언하고 Getter 메서드를 _count 변수를 리턴하게 커스텀하면 외부에서 count 변수를 참조하면 _count 를 반환하게끔 할 수 있다. count 는 val 이기 때문에 데이터 값을 변경할 수 없다.

 

 

😶 ViewModel Example

간단한 플러스 버튼을 누르면 중앙에 있는 값이 증가하고 마이너스 버튼을 누르면 감소하는 모습을 보여주는 앱이 있다.

 

 

ViewModel 을 사용하지 않고 앱을 만든다면 다음과 같을 것이다.

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"
    tools:context=".MainActivity">


    <TextView
        android:id="@+id/count_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="@string/count_text" />

    <Button
        android:id="@+id/plus_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:paddingHorizontal="50dp"
        android:text="@string/plus_text"
        android:textSize="20sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/minus_button"
        app:layout_constraintTop_toBottomOf="@+id/count_text_view" />

    <Button
        android:id="@+id/minus_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingHorizontal="50dp"
        android:text="@string/minus_text"
        android:textSize="20sp"
        app:layout_constraintEnd_toStartOf="@+id/plus_button"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/plus_button" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

MainActivity

class MainActivity : AppCompatActivity() {
    private lateinit var _binding: ActivityMainBinding
    private var count = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(_binding.root)
        
        _binding.plusButton.setOnClickListener {
            plusCount()
            updateCount()
        }

        _binding.minusButton.setOnClickListener {
            minusCount()
            updateCount()
        }

        updateCount()
    }

    private fun plusCount() {
        count++
    }

    private fun minusCount() {
        count--
    }

    private fun updateCount() {
        // R.string.count_text = "\" %d \""
        _binding.countTextView.text = getString(R.string.count_text, count)
    }
}

플러스 버튼을 누르면 plusCount() 로 count 값을 증가시키고 UI를 업데이트하고, 마이너스 버튼을 누르면 minusCount() 로 count 값을 감소시키고 UI를 업데이트한다.

 

이 코드에 문제점은 화면을 회전했을 때의 사후처리가 안되어있다는 점이다. 가로 모드 혹은 세로모드로 회전하면 count 값이 다시 초기화되는 것을 볼 수 있다. 이 문제는 onSaveInstanceState() 콜백을 사용하여 해결할 수 있지만 저장할 수 있는 양이 한정되어 있다는 점과 Activity 나 Fragment 파일에 한 번에 모든 것들이 들어있으면 관리하기 힘들어진다.

 

여기서 ViewModel 을 사용하면 데이터를 관리하는 ViewModel 컴포넌트와 UI 업데이트를 담당하는 Activity/Fragment 로 나눠져 화면 회전 등 구성 요소가 변경되었을 때 데이터를 관리하는 문제도 유지 관리하는 작업도 쉬워질 것이다.

 

MainViewModel

class MainViewModel: ViewModel() {
    private var _count = 0
    val count: Int
        get() = _count

    fun plusCount() {
        _count++
    }

    fun minusCount() {
        _count--
    }
}

ViewModel 는 앱 데이터를 관리하기 때문에 count 변수와  plusCount() 와 minusCount() 함수를 옮긴다.

 

MainActivity

class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
    private lateinit var _binding: ActivityMainBinding

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

        _binding.minusButton.setOnClickListener {
            viewModel.minusCount()
            updateCount()
        }

        _binding.plusButton.setOnClickListener {
            viewModel.plusCount()
            updateCount()
        }

        updateCount()
    }

    private fun updateCount() {
        _binding.countTextView.text = getString(R.string.count_text, viewModel.count)
    }
}

UI Controller(Activity/Fragment)에서 데이터를 관리하지 않고 이벤트를 호출하거나 UI 업데이트를 실행한다. ViewModel 을 적용한 이 코드를 실행했을 때는 화면을 회전해도 count 값이 초기화되지 않는다는 것을 볼 수 있다.

 

보통 앱의 데이터가 변경되면 정해진 UI을 업데이트한다. 현재까지의 코드로는 데이터를 업데이트(minusCount(), plusCount())하는 것과 UI 업데이트(updateCount())하는 것은 Activity 에서 따로 선언해줘야 한다. 만약 ViewModel 의 데이터가 변경되면 자동으로 UI를 업데이트할 수 있다면 더 편할 것이다. 이것을 가능하게 해주는 기술이 LiveData 이다.

 

LiveData 에 observer 를 연결하면 Live Data 의 데이터 값이 변경될 때마다 observer 에게 통지한다. 따라서 각 observer 를 연결할 때 데이터가 변할 때마다 실행할 작업을 선언해 두면  UI를 업데이트(updateCount())하는 코드를 적지 않아도 된다.

 

 

😶 LiveData 개요

LiveData 는 Android Architecture Components 중 하나로 관찰할 수 있는 데이터 홀더 클래스로 여러 가지 특징이 있다.

 

1. 모든 유형의 데이터와 함께 사용할 수 있는 Wrapper 이다.

2. 관찰 가능하다. 즉, LiveData 객체가 보유한 데이터가 변경될 때마다 observer 에게 통지된다.

3. LiveData 는 수명주기를 인식한다. observer 를 LiveData 에 연결하면 수명주기 소유자(Activity, Fragment)와 연결된다.

4. observer 는 LifecyclerOwner(Activity, Fragment) 와 연관되어 있는데 STARTED 혹은 RESUME 때만 업데이트 된다.

5. observer 는 연결된 수명주기 소유자가 파괴(destory)된 후 스스로 객체를 정리한다. 

 

즉, LiveData 는 모든 유형의 데이터와 함께 사용할 수 있는 Wrapper 로 observer 를 선언하면 데이터 값이 변경될 때마다 oberver 에게 통지되어 UI 를 변경하는 등의 작업을 자동으로 할 수 있다. 수명주기를 인식하기 때문에 연결된 수명주기 소유자(Activity/Fragment)가 사용자 화면에 보이고 포커스를 갖는 STARTED 혹은 RESUME 때만 선언한 observer 가 데이터를 업데이트한다.

 

 

LiveData 선언하기

private val _count: MutableLiveData<Int> = MutableLiveData(0)
val count: LiveData<Int>
    get() = _count
    
private val _name: MutableLiveData<String> = MutableLiveData("name")
val name: LiveData<String>
    get() = _name
    
// 데이터 값 가져오기
_count.value
_name.value

// 데이터 값 설정하기
_count.value = 3
_name.value = "John"

 

LiveData Observer 선언하기

var count: MutableLiveData<Int> = MutableLiveData(0)

// 첫번째 인자는 lifecyclerOwner로 Activity 에서는 this, Fragment 에서는 viewLifecycleOwner 로 가능
count.observe(this, { newCount ->
    // newCount is updated data
})

 

 

 

😶 ViewModel + LiveData Example

ViewModel 예시의 코드에 LiveData 를 적용해보면 다음과 같다.

 

MainViewModel

class MainViewModel: ViewModel() {
    private val _count: MutableLiveData<Int> = MutableLiveData(0)
    val count: LiveData<Int>
        get() = _count

    fun plusCount() {
        _count.value = (_count.value)?.inc()
    }

    fun minusCount() {
        _count.value = (_count.value)?.dec()
    }
}

 

MainActivity

class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
    private lateinit var _binding: ActivityMainBinding

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

        _binding.minusButton.setOnClickListener {
            viewModel.minusCount()
            // updateCount()
        }

        _binding.plusButton.setOnClickListener {
            viewModel.plusCount()
            // updateCount()
        }

        viewModel.count.observe(this, { newCount ->
            _binding.countTextView.text = getString(R.string.count_text, newCount)
        })
    }
    
    /*
    private fun updateCount() {
        _binding.countTextView.text = getString(R.string.count_text, viewModel.count)
    }
    */
}

Activity 에서 UI 업데이트하는 updateCount() 함수를 호출하는 것 대신 ViewModel 의 count 변수에 observe 를 설정하여 count 값이 변경될 때마다 newCount 로 값을 받아 UI 를 업데이트한다.

 

 

😶 ViewModel + LiveData + Data Binding

여태까지의 ViewModel + LiveData 에서 Data Binding 기술까지 더하면 observer 의 이벤트 등록을 하지 않아도 값이 변하는 걸 인지하면 layout 안에서 자동으로 업그레이드한다. 즉, UI 를 업데이트하는 코드를 추가하지 않아도 된다.

 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>
        <variable
            name="mainViewModel"
            type="com.example.viewmodeltest.MainViewModel" />
    </data>

    <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"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/count_text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/black"
            android:textSize="25sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="@{@string/count_text(mainViewModel.count)}" />

        <Button
            android:id="@+id/plus_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:paddingHorizontal="50dp"
            android:text="@string/plus_text"
            android:textSize="20sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/minus_button"
            app:layout_constraintTop_toBottomOf="@+id/count_text_view" />

        <Button
            android:id="@+id/minus_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingHorizontal="50dp"
            android:text="@string/minus_text"
            android:textSize="20sp"
            app:layout_constraintEnd_toStartOf="@+id/plus_button"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@+id/plus_button" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

data binding 은 xml 파일의 최상위 루트를 <layout> 태그로 감싸주고 <data> 태그에 layout 에서 사용할 데이터를 명시한다. 데이터를 넣는 자리에 @{} 기호 사이 데이터를 넣어주면 되는데 만약 string.xm 파일에 있는 데이터의 정보를 넣고 싶다면 @{@string/count_text(mainViewModel.count)} 처럼 string 경로를 넣고 받을 데이터는 () 사이에 넣는다.

// string.xml
<string name="count_text">\" %d \"</string>

// xml 예시
android:text="@{@string/count_text(mainViewModel.count)}"

 

MainActivity

class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
    private lateinit var _binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        _binding.minusButton.setOnClickListener {
            viewModel.minusCount()
        }

        _binding.plusButton.setOnClickListener {
            viewModel.plusCount()
        }
        
        /*
        viewModel.count.observe(this, { newCount ->
            _binding.countTextView.text = getString(R.string.count_text, newCount)
        })
        */

        _binding.lifecycleOwner = this
        _binding.mainViewModel = viewModel
    }

}

observer 코드를 제외하고 _binding 객체에 lifecyclerOwner 를 정해주고 activity_main 파일에서 <data> 태그에서 사용할 mainViewModel 에 데이터를 정해준다. 이렇게 하면 자동으로 LiveData 의 count 변수의 데이터값이 변경될 때마다 layout 파일의 android:text="@{@string/count_text(mainViewModel.count)}" 에서 count 값이 변경될 것이다.

 

 

 

 

 

앱 아키텍처 가이드  |  Android 개발자  |  Android Developers

앱 아키텍처 가이드 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 가이드에는 고품질의 강력한 앱을 빌드하기 위한 권장사항 및 권장 아키텍처가 포함

developer.android.com

 

ViewModel 개요  |  Android 개발자  |  Android Developers

ViewModel을 사용하면 수명 주기를 인식하는 방식으로 UI 데이터를 관리할 수 있습니다.

developer.android.com

 

 

LiveData 개요  |  Android 개발자  |  Android Developers

LiveData를 사용하여 수명 주기를 인식하는 방식으로 데이터를 처리합니다.

developer.android.com

 

728x90