일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- viewmodel
- textfield
- DART
- Compose
- activity
- livedata
- 테스트
- 안드로이드
- Button
- LifeCycle
- scroll
- textview
- TEST
- ScrollView
- Navigation
- data
- intent
- 앱
- Dialog
- 앱바
- Flutter
- Coroutines
- android
- tabbar
- appbar
- drift
- CustomScrollView
- 계측
- Kotlin
- binding
- Today
- Total
Study Record
[안드로이드] ViewModel 과 LiveData & Data Binding 본문
😶 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 값이 변경될 것이다.
'안드로이드' 카테고리의 다른 글
[안드로이드] ViewModel 참고사항 (0) | 2023.08.02 |
---|---|
[안드로이드] navigation 참고사항 (0) | 2023.08.01 |
[안드로이드] Dialog 정리 (0) | 2023.07.26 |
[안드로이드] 메뉴 만들기 (0) | 2023.07.20 |
[안드로이드] Activity 시작하기 (0) | 2023.07.20 |