[Android] AAC - Databinding

ENAN·2021년 6월 25일
0

사실 데이터바인딩은 안드로이드에서만 사용하는 개념은 아니다. 포괄적인 의미에서 데이터바인딩은 UI 요소와 데이터를 결합시켜 사용하는 것을 의미하고, 당연히 다른 언어와 프레임워크에서도 사용되는 개념이다. 아래에서는 Android Jetpack - AAC의 일부인 databinding library에 대해서만 설명할 것!

DataBinding이란?

선언적 형식으로 레이아웃의 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은 기본적으로 디버깅이 안되기 때문에, 데이터가 제대로 넘어가지 않는 경우 이유를 확인하기 어렵다.

클래스 파일이 많이 생기고, 이에 따라 빌드 속도가 느려지며 앱 용량도 증가한다.

vs ViewBinding

viewBinding은 databinding을 단순히 view에 대한 참조를 얻기 위한 목적으로 사용하는 사람들을 위해 탄생했다.

databinding과 비슷한 역할과 특징을 갖지만, 차이점으로 xml 파일에 태그가 필요하지 않고 컴파일 속도가 빠르며 앱 용량이 좀 더 작다는 장점과 양방향 바인딩, binding adapter 등을 통한 동적 변경이 불가능하다는 단점을 갖고 있다.

사용법

기본적인 사용법은 developer 사이트에서 충분히 확인할 수 있다. 여기서는 viewModel, room, liveData와 함께 사용하는 법을 살펴보자.
공식 문서

  • 사용법
  1. 의존성 추가
        android{
        	buildFeatures {
                dataBinding true
          }
        }

        dependencies {
            implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
        }
  1. xml 파일 최상단에 layout, data 태그 추가
    : 레이아웃 태그를 추가하면 binding 클래스가 자동으로 생성된다. 클래스명은 xml파일명 기준으로 파스칼 케이스로 변환되고 뒤에 Binding이 붙은 이름으로 정해진다. (activity_main.xml → ActivityMainBinding)
        <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>
  1. MainActivity의 onCreate 내부
        // 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
  1. xml에서 viewModel의 LiveData 변수에 접근할 수 있도록 public 변수 할당
    : getAll() 함수는 room을 통해 내장 db에 저장된 Todo 리스트를 가져오는 함수이다. 이 때, todoDao().getAll() 함수 역시 LiveData를 반환해야 데이터를 제대로 Observing해서 갱신할 수 있다.
        // 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))
        		}
        }
  1. xml의 view에 databinding
  • 단방향 databinding
    : viewModel.todos와 같이 LiveData 변수와 바인딩하면 데이터가 변경된 경우 자동으로 뷰가 갱신된다. 게다가 여기서는 room까지 연결했으므로 내장 db의 데이터가 변하면 자동으로 뷰까지 갱신된다는 것이다!
        <TextView
            android:text="@{viewModel.todos.toString()}"
        </TextView>
  • 양방향 databinding
    : 양방향 바인딩을 하게 되면, editText에서 텍스트를 변경했을 때 자동으로 viewModel의 newTodo 변수의 데이터도 변한다.
        <EditText
            android:text="@={viewModel.newTodo}"
        </EditText>
  • callback binding
    : onClick 시 실행될 함수를 람다식으로 지정할 수도 있다.
        <Button
            android:onClick="@{() -> viewModel.insert(viewModel.newTodo)}"
        </Button>

정리

databinding은 장단점이 있는 패러다임이지만 장점이 워낙 매력적이고, 무엇보다 view와 데이터 혹은 로직 사이의 의존성을 최소화할 수 있어 MVVM 패턴에 필수적이다.

아직 소개하지 않은 기능이 있는데, bindingAdapter를 사용하게 되면 recyclerview의 리스트 갱신 같은 복잡한 결합도 선언적으로 가능하게 할 수 있다. 다음에는 bindingAdapter를 사용하는 이유, 방법 등에 대해 글을 써 보겠습니다!


p.s 최근 베타 버전으로 업그레이드 된 컴포즈 라이브러리 또한 선언형 UI 작성 toolkit이다. 지금은 xml과 데이터바인딩을 주로 사용한다고 하더라도, 언젠가 자연스럽게 컴포즈로 대체되지 않을까 싶다.

profile
여정이 곧 보상

1개의 댓글

comment-user-thumbnail
2022년 5월 9일

와 정리가 너무 훌륭하네요.... 데이터바인딩을 왜 MVVM에서 사용하는지 헷갈렸었는데 개념 정리가 한번에 싹 됐어요!! 리액티브 프로그래밍에 대해서도 다뤄주세요!!!

답글 달기