1. 들어가기에 앞서
2. 예제
2-1. 가장 기본적인 코드
2-2. View Binding
2-3. View Binding + ViewModel
2-4. View Binding + ViewModel + LiveData
2-5. Data Binding
2-6. Data Binding + ViewModel
2-7. Data Binding + ViewModel + LiveData
2-8. 깃허브 링크
1. 들어가기에 앞서
이 게시글은 위 4개의 개념이 모두 잡혀있다는 가정하에 따로 자세한 설명을 하지 않을 예정이다.
조합별로 어떻게 사용하는지 어떤 게 좋은지 비교 및 연습하고 싶어서 작성하는 글이니
설명이 필요하다면 각 링크된 게시글을 참고하자.
2. 예제
2-1. 가장 기본적인 코드
class MainActivity : AppCompatActivity() {
private var number = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var txtNumber = findViewById<TextView>(R.id.txt_number)
var btnIncrease = findViewById<Button>(R.id.btn_increase)
btnIncrease.setOnClickListener {
txtNumber.text = number++.toString()
}
}
}
안드로이드를 처음 배우는 단계에서는 위와 같이 findViewById를 주로 사용한다.
과거에는 이 방법이나 버터나이프 같은 라이브러리를 사용하는 방법밖에 없긴 했다
뷰 바인딩 포스팅의 [목차 1-2]에서 이에 대해 잠시 언급한 적이 있다.
<?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/txt_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
android:text="0"
android:textSize="24sp"
app:layout_constraintBottom_toTopOf="@+id/btn_increase"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/btn_increase"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
레이아웃 파일이다. 딱히 설명할 것이 없다.
2-2. View Binding
class MainActivity : AppCompatActivity() {
private var number = 0
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnIncrease.setOnClickListener {
binding.txtNumber.text = number++.toString()
}
}
}
뷰 바인딩을 사용하면 findViewById를 사용하지 않아도 되고 코드도 간결해진다.
레이아웃 파일은 [목차 1-1]과 달라지는 점은 없다.
2-3. View Binding + ViewModel
class MainViewModel : ViewModel() {
private var number = 0
fun increase() {
number++
}
fun getNumber(): String {
return number.toString()
}
}
[목차 1-2]의 코드의 문제점은 스마트폰을 회전했을 때 number의 값이 초기화된다는 점이다.
이 문제를 해결하기 위해선 ViewModel을 사용해야 한다. 위와 같이 뷰 모델 파일을 생성해주자.
ViewModel 포스팅의 움짤을 보고 오면 무슨 문제를 말하는 지 이해하기 쉬울 것이다.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val model: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.txtNumber.text = model.getNumber()
binding.btnIncrease.setOnClickListener {
model.increase()
binding.txtNumber.text = model.getNumber()
}
}
}
뷰 모델을 사용하니 클래스 파일도 하나 늘어나고 코드가 더 많아졌지만 각자 맡은 역할만 하면 되도록 바뀌었다.
지금은 number의 값을 증가시키는 로직이 굉장히 간단해서 티가 나지 않지만
굉장히 길고 복잡한 로직이 사용된다고 상상해보자.
[목차 1-1]과 [목차 1-2]의 방법을 사용한다면 UI 컨트롤러(= 액티비티)의 몸집이 굉장히 커질 것이다.
혼자서 계산도 해야하고 화면 갱신도 해야 하고 아주 바빠진다.
하지만 ViewModel을 사용함으로써
View는 ViewModel에게 받은 데이터로 화면을 갱신하는 역할만을 하며
ViewModel은 View가 화면을 갱신하기 위한 데이터를 가공한다.
<?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/txt_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
android:text = "0" // 이 부분을 빼주자.
android:textSize="24sp"
app:layout_constraintBottom_toTopOf="@+id/btn_increase"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/btn_increase"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
레이아웃에서는 text를 초기화 해주는 부분을 빼주자.
이걸 안 빼주면 화면을 회전할때마다 처음에 무조건 0이 표시된다.
2-4. View Binding + ViewModel + LiveData
class MainViewModel : ViewModel() {
private var number = MutableLiveData<Int>()
init {
number.value = 0
}
fun increase() {
number.value = number.value?.plus(1)
}
fun getNumber(): MutableLiveData<Int> {
return number
}
}
LiveData를 사용하면 데이터 값이 바뀌었을 때 알아서 감지해서 화면을 갱신한다.
뷰 모델은 코드가 위와 같고
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val model: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnIncrease.setOnClickListener {
model.increase()
}
model.getNumber().observe(this, Observer { number ->
binding.txtNumber.text = number.toString()
})
}
}
액티비티에서는 버튼을 누를때마다
값 증가 -> 화면 갱신을 했었지만 LiveData를 사용함으로써 값 증가만 하면 되고
값의 변화하면 observer가 이를 감지하여 화면 갱신하는 코드를 실행한다.
2-5. Data Binding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var number = 0;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.counter = this
binding.txtNumber.text = number.toString()
}
fun increase(){
number++
binding.txtNumber.text = number.toString()
}
}
데이터 바인딩은 뷰 바인딩과 똑같이 findViewById를 대신할 수 있다.
<?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">
<data>
<variable
name="counter"
type="com.example.selfstudy_kotlin.MainActivity" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/txt_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
android:textSize="24sp"
app:layout_constraintBottom_toTopOf="@+id/btn_increase"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/btn_increase"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
android:onClick="@{() -> counter.increase()}" // 괜히 한 번 써봄
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
대신 레이아웃 구조가 조금 바뀐다.
그러면서 이제 결합 표현식(binding expressions)을 사용할 수 있는데
위 예제에서는 버튼을 눌렀을 때 액티비티에 있는 increase 함수를 호출하도록 표현식을 사용해 보았다.
개인적인 의견으로.. 단독으로 쓸 거면 뷰 바인딩을 쓰는 게 낫다고 생각한다.
2-6. Data Binding + ViewModel
class MainViewModel : ViewModel() {
private var number = 0;
fun increase() {
number++
}
fun getNumber(): String {
return number.toString()
}
}
데이터 바인딩도 뷰 모델 없이는 회전할 때 데이터가 손실되므로 뷰 모델과 함께 사용해보자.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
val model: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.counter = this
binding.txtNumber.text = model.getNumber()
}
fun increase(){
model.increase()
binding.txtNumber.text = model.getNumber()
}
}
메인 액티비티 코드 (설명 생략)
<?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">
<data>
<variable
name="counter"
type="com.example.selfstudy_kotlin.MainActivity" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/txt_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
android:textSize="24sp"
app:layout_constraintBottom_toTopOf="@+id/btn_increase"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/btn_increase"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
android:onClick="@{() -> counter.increase()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
레이아웃 코드 (설명 생략)
2-7. Data Binding + ViewModel + LiveData
class MainViewModel : ViewModel() {
private var number = MutableLiveData<Int>()
init {
number.value = 0
}
fun increase() {
number.value = number.value?.plus(1)
}
fun getNumber(): MutableLiveData<Int> {
return number
}
}
드디어 데이터 바인딩 + 뷰 모델 + 라이브 데이터 삼위일체!
뷰 모델은 크게 달라진 게 없어 보이지만
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val model: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.lifecycleOwner = this
binding.viewModel = model
}
}
확실히 액티비티의 코드가 간결해진 느낌이다.
바인딩을 해주기 때문에 관찰자를 생성할 필요도 없다.
<?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">
<data>
<variable
name="viewModel"
type="com.example.selfstudy_kotlin.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/txt_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
android:textSize="24sp"
android:text="@{Integer.toString(viewModel.getNumber())}"
app:layout_constraintBottom_toTopOf="@+id/btn_increase"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/btn_increase"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
android:onClick="@{() -> viewModel.increase()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
레이아웃 코드 내에서 라이브 데이터 값을 가져와서 업데이트하고
버튼이 눌리면 뷰 모델의 함수를 호출하고 하니
약간 뭐랄까...
기존에는 생선을 살 때 어부 -> 중간 판매자 -> 나 이렇게 거쳐서 거래를 했었는데
지금은 직거래 플랫폼을 이용해서 어부 -> 나 이렇게 바로 직거래하는 느낌...? 😂
데이터 바인딩이 어부와 나를 매칭 시켜 주는 역할을 하는 느낌이다.
2-8. 깃허브 링크
오늘 설명한 코드들은 전부 위 링크에서 확인할 수 있다.
💡 느낀 점
- View Binding, Data Binding, ViewModel, LiveData 전부 밀접한 관계에 있어서 따로따로 공부했을 때는 각자의 개념은 이해가 가지만 큰 그림이 그려지지 않는 느낌이었는데, 이렇게 전체적으로 다시 정리를 하니 이해가 되는 것 같다.
- 데이터 바인딩(or 뷰 바인딩)이 단독으로 쓰이는 경우는 학습단계 정도에서만이 아닐까 싶다. 앱 개발을 하다 보면 뷰 모델과 라이브 데이터는 거의 필수적으로 쓰이게 되는 듯.
- 데이터 바인딩이 나에겐 낯설고 편하지 않아서 뷰 바인딩을 대신 사용하곤 했는데 이제는 데이터 바인딩을 써야 할 때가 온 것 같다. 막상 써보니 나쁘지 않은 것 같기도!
📘 참고한 자료
'오늘은 뭘 배울까? > Android' 카테고리의 다른 글
안드로이드 옵션 메뉴(Option Menu)란? (0) | 2021.09.26 |
---|---|
Binding Adapter(바인딩 어댑터)를 배워보자! (8) | 2021.05.29 |
LiveData(라이브 데이터)란? (0) | 2021.05.14 |
레트로핏을 이용하여 서버와 통신하자! (2) | 2021.04.12 |
안드로이드 Jetpack이란? (3) | 2021.04.05 |
댓글