사실 데이터바인딩은 안드로이드에서만 사용하는 개념은 아니다. 포괄적인 의미에서 데이터바인딩은 UI 요소와 데이터를 결합시켜 사용하는 것을 의미하고, 당연히 다른 언어와 프레임워크에서도 사용되는 개념이다. 아래에서는 Android Jetpack - AAC의 일부인 databinding library에 대해서만 설명할 것!
선언적 형식으로 레이아웃의 UI 구성요소를 앱의 데이터 소스와 결합할 수 있는 지원 라이브러리
이 말이 바로 와닿지는 않을 테니(저는 그랬습니다), 우선은 레이아웃 xml 파일에 data를 연결해서 사용하는 것 정도로 이해하고 넘어가 보자.
🤔 선언적 형식이 궁금하다면? (접은글로 만들고 싶은데..velog에서 아직 지원하지 않는듯 ㅠ)
선언적 형식이라는 말이 무슨 말일까? 를 이해하기 위해서는, findviewById를 이용했던 기존 방식(명령적)과 차이를 보면서 이해하면 좋을 것 같다.
- 기존 명령형 방식
<Button android:id="@+id/someButton" />
override fun onCreate(savedInstanceState: Bundle?) { //... val someButton = findViewById<Button>(R.id.someButton) someButton.text = "someText" someButton.setOnClickListener { viewModel.someFunction() } }
- 선언형 방식
<Button android:id="@+id/someButton" android:text="someText" android:onClick="@{viewModel.someFunction()}" />
보시다시피 명령형 방식에선
어떻게
UI가 구성되는지를 순차적인 명령을 통해서 지정한다. 반면 선언형 방식에서는 순서는 중요하지 않다. 속성에 해당하는 값이무엇
인지 선언만 하면 된다. 이 방식은 보일러플레이트 코드를 줄이고 코드 재사용이 쉽다는 장점이 있다(가독성은 덤).
선언형
vs명령형
에 대한 내용은 안드로이드 뿐만 아니라 모든 언어와 플랫폼에서 사용되는 개념이므로 이참에 자세히 알아두는 것도 좋을 것 같다.
👇 그래서 좀 더 잘 설명된 아래 글을 읽어보면 좋을 듯! 👇
명령형 프로그래밍 VS 선언형 프로그래밍
databinding을 사용하기 전에는 xml에 고정된 값만 넣을 수 있었다. 만약 특정 데이터에 따라 뷰를 변경하려면 액티비티의 코드를 통해 뷰를 갱신해주어야 한다.
이 방식에는 단점이 몇 가지 있다. 먼저, findViewById
함수를 통해 뷰에 접근을 하게 되면, 내부적으로 레이아웃 파일 트리를 순회하며 해당하는 요소를 찾게 된다. 따라서 레이아웃 파일이 많아질 수록 성능적인 이슈가 발생하게 된다.
또한 불필요하게 작성해야 될 코드의 양이 많으며, 실수로 뷰에 잘못된 접근을 했을 때 NPE(Null Pointer Exception)
이 발생할 수 있다. 즉, 이를 항상 신경쓰고 있어야 한다는 것이다.
이렇듯 기존 방식에는 여러 단점이 있었고, 이러한 문제들을 해결하기 위해 데이터바인딩이라는 개념이 등장하게 되었다.
(참고로,
findViewById
없이도 뷰에 접근할 수 있던 kotlin android extension (kotlin synthetic)은deprecated
되었다!)
가독성에 대해서는 사람마다 다르게 느낄 수도 있다고 생각하지만, 기본적으로 선언형 방식이 훨씬 깔끔하게 읽혀지는 것 같다.
액티비티에서 뷰에 접근하는 보일러 플레이트 코드를 작성하지 않아도 된다.
뷰모델에 데이터나 함수 등을 정의해 두고 속성을 지정만 하면 되기 때문에 재사용이 쉽다.
양방향 바인딩을 활용하면 뷰 변경에 따라 데이터도 자동으로 갱신되고, 라이브데이터와 함께 사용하면 data가 변할 때마다 view가 자동으로 갱신되기 때문에 작성할 코드가 훨씬 줄어들고 뷰/데이터 갱신에 신경을 안써도 된다. 즉, 뷰와 데이터를 서로의 변경에 상관없이 완벽하게 일치시킬 수 있다.
디버깅이 어렵다. xml은 기본적으로 디버깅이 안되기 때문에, 데이터가 제대로 넘어가지 않는 경우 이유를 확인하기 어렵다.
클래스 파일이 많이 생기고, 이에 따라 빌드 속도가 느려지며 앱 용량도 증가한다.
viewBinding은 databinding을 단순히 view에 대한 참조를 얻기 위한 목적으로 사용하는 사람들을 위해 탄생했다.
databinding과 비슷한 역할과 특징을 갖지만, 차이점으로 xml 파일에 태그가 필요하지 않고 컴파일 속도가 빠르며 앱 용량이 좀 더 작다는 장점과 양방향 바인딩, binding adapter 등을 통한 동적 변경이 불가능하다는 단점을 갖고 있다.
기본적인 사용법은 developer 사이트에서 충분히 확인할 수 있다. 여기서는 viewModel, room, liveData와 함께 사용하는 법을 살펴보자.
공식 문서
android{
buildFeatures {
dataBinding true
}
}
dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
}
<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="exam.yeonj.jetpackexample.MainViewModel" />
</data>
</layout>
// activity class
//setContentView(R.layout.activity_main) 대신 bindingUtil 사용
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
// lifecyclerOwner를 지정해 줘야 livedata를 관찰해서 화면 refresh 가능
binding.lifecycleOwner = this
binding.viewModel = mainViewModel
// viewModel class
var todos: LiveData<List<Todo>>
var newTodo: String? = null
init {
todos = getAll()
}
fun getAll(): LiveData<List<Todo>> {
return db.todoDao().getAll()
}
fun insert(todo:String){
viewModelScope.launch(Dispatchers.IO) {
db.todoDao().insert(Todo(todo))
}
}
<TextView
android:text="@{viewModel.todos.toString()}"
</TextView>
<EditText
android:text="@={viewModel.newTodo}"
</EditText>
<Button
android:onClick="@{() -> viewModel.insert(viewModel.newTodo)}"
</Button>
databinding은 장단점이 있는 패러다임이지만 장점이 워낙 매력적이고, 무엇보다 view와 데이터 혹은 로직 사이의 의존성을 최소화할 수 있어 MVVM 패턴에 필수적이다.
아직 소개하지 않은 기능이 있는데, bindingAdapter를 사용하게 되면 recyclerview의 리스트 갱신 같은 복잡한 결합도 선언적으로 가능하게 할 수 있다. 다음에는 bindingAdapter를 사용하는 이유, 방법 등에 대해 글을 써 보겠습니다!
p.s 최근 베타 버전으로 업그레이드 된 컴포즈 라이브러리 또한 선언형 UI 작성 toolkit이다. 지금은 xml과 데이터바인딩을 주로 사용한다고 하더라도, 언젠가 자연스럽게 컴포즈로 대체되지 않을까 싶다.
와 정리가 너무 훌륭하네요.... 데이터바인딩을 왜 MVVM에서 사용하는지 헷갈렸었는데 개념 정리가 한번에 싹 됐어요!! 리액티브 프로그래밍에 대해서도 다뤄주세요!!!