본문 바로가기
Android/DataBinding

[Android] 데이터바인딩(DataBinding) 이란?

by 태크민 2023. 9. 7.

데이터바인딩(DataBinding)

데이터 바인딩은 레이아웃의 뷰를 앱 코드에 저장된 데이터와 연결하는 간단한 방법을 제공하는 라이브러리이다.

예를 들어, EditText 뷰를 ViewModel의 LiveData와 연결한다고 가정해보자.

여기서 단방향 바인딩을 사용하게 된다면, LiveData가 변경되는 즉시 EditText의 값도 자동으로 해당 값으로 바뀌게 된다.

만약 양방향 바인딩을 사용하게 된다면, EditText에 작성된 값이 자동으로 LiveData의 값을 갱신할 수도 있다.

여기서 뷰모델 데이터를 UI뷰에서 참조할 수 있는 방법단방향 바인딩이라고 하고, 반대로 UI뷰의 데이터를 뷰모델의 데이터가 참조하고 갱신할 수 있는 방법양방향 바인딩이다.

 

데이터바인딩(DataBindng)을 왜 사용할까?

데이터 바인딩을 사용하면, 데이터를 UI 요소에 연결하기 위해 필요한 코드를 최소화 할 수 있다.

데이터 바인딩을 사용했을 때, 당장 가시적으로 보이는 장점들을 꼽자면 다음과 같다.

  • findViewbyid()를 호출하지 않아도, 자동으로 xml에 있는 View들을 만들어준다.
  • data가 바뀌면 자동으로 View를 변경하게 할 수 있다.
  • xml 리소스만 보고도 View에 어떠 데이터가 들어가는지 파악이 가능하다.
  • 코드 가독성이 좋아지고, 상대적으로 코드량이 줄어든다.

 


사용 방법

프로젝트 빌드 구성

프로젝트에서 데이터 바인딩을 사용하려면, app 수준의 build.gradle에 다음과 같은 항목을 작성하여 데이터 바인딩을 사용할 수 있도록 구성해야한다.

android {
	.
    .
    buildFeatures {
    	dataBinding true
    }
}

 

데이터 바인딩 레이아웃 파일

앱의 UI는 XML 레이아웃 파일에 포함된다. 데이터 바인딩을 사용하려면 기존 XML 레이아웃 파일을 데이터 바인딩 레이아웃 파일로 변환해야 한다.

변환하기 전의 XML 레이아웃 파일을 보면 다음과 같다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.main.MainFragment">

        <EditText
            android:id="@+id/dollerText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="89dp"
            android:ems="10"
            android:inputType="number"
            android:minHeight="48dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

이곳에서는 ConstraintLayout이 루트 뷰가 된다. 그러나 데이터 바인딩을 사용하려면 다음 코드와 같이 layout 컴포넌트가 루트 뷰가 되어야한다.

<?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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.main.MainFragment">


        <EditText
            android:id="@+id/dollerText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="89dp"
            android:ems="10"
            android:inputType="number"
            android:minHeight="48dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

레이아웃 파일 data 요소

레이아웃 파일의 뷰는 프로젝트 내부의 클래스(예를 들어 ViewModel)와 바인딩 되어야 하고, 이 클래스 이름을 바인딩 레이아웃에 선언해주어야 한다.

다음 코드는 레이아웃 파일에 ViewModel 클래스를 선언해주는 코드이다.

ㅍㅁ<?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="myViewModel"
            type="com.example.viewmodelexample.ui.main.MainViewModel" />
    </data>
----------------------------------------------
    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.main.MainFragment">


        <EditText
            android:id="@+id/dollerText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="89dp"
            android:ems="10"
            android:inputType="number"
            android:minHeight="48dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

여기서 추가적으로 레이아웃 파일 안에서 클래스에 대해 쉽게 참조할 수 있도록 하려면 import를 선언한다. data 태그 내에서 선언하며, 만약 레이아웃 파일내에서 View 클래스를 참조한다고 가정하면 아래와 같이 사용할 수 있다. 즉, 코드에서 import를 통해 클래스를 쉽게 참조하는 것처럼, View에서도 코드와 같은 방식으로 호출하는 것이다.

<data>
    <import type="android.view.View"/>
</data>

<TextView
   android:text="@{user.lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

참조한 클래스는 바인딩 표현식(@{})안에서 위와 같이 사용할 수 있다. 위의 예제느 View 클래스의 상수에 해당하는 VISIBLE과 GONE을 사용하고 있다.

 

바인딩 클래스

data 요소에서 참조하는 각 클래스에 대응되는 바인딩클래스는 안드로이드 스튜디오에서 자동으로 생성해준다.

 

클래스 이름은 ViewDataBinding 클래스의 서브클래스이며, 이는 레이아웃 파일 이름을 대문자로 바꾸고, 제일 뒤에 Binding을 붙인다. 예를 들어, activity_main 이라는 파일이 있다면, 이것의 바인딩 클래스는 MainAcitivtyBinding이 되는 것이다.

이제 이 바인딩 클래스를 액티비티에서 인스턴스화 해보자

기존에는 onCreate()안에 다음과 같은 코드로 레이아웃 파일을 인플레이트 했다.

setContentView(R.layout.activity_main)

만약 이 액티비티에서 바인딩 클래스를 인스턴스화 할때는, 위 코드 대신 초기화 코드를 다음과 같이 입력해주어야 한다.

lateinit var binding: ActivityMainBinding

binding = DataBindUtil.inflate(
	inflater, R.layout.activity_main, container, false)

이제 ViewModel을 인스턴스화 한 다음, 앞서 레이아웃 파일에 선언했던 변수에 ViewModel인스턴스를 저장해 주면 된다 . 다음과 같은 코드로 말이다.

var viewModel: MainViewModel =
	ViewModelProvider(this).get(MainVeiwModel::class.java)
binding.setVariable("myViewModel", viewModel)

 

바인딩 표현식 (단방향)

바인딩 표현식으로 ViewModel에 저장된 어떤 데이터 값이 어떤 형식으로 TextView에 나타나는지 정의할 수 있다.

바인딩 표현식은 @로 시작하여 중괄호 안에 표현식을 넣어서 표현한다.

<TextView
android:id="@+id/resultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
          
android:text='@{myViewModel.result}'
          
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"            
app:layout_constraintTop_toTopOf="parent" />

 

safeUnBox

만약 float타입의 값을 텍스트뷰에 띄우고 싶다면, 표준 자바 언어의 String 클래스를 사용하여 다음과 같이 나타낼 수 있다.

android:text="@{String.valueOf(myViewModel.result)}"

그후 실행을 하게 된다면, 안드로이드 콘솔에 다음과 같은 경고 문구가 뜬다.

"warning: myViewModel.result.getValue() is a boxed field but needs to be un-boxed to execute String.valueOf(viewModel.result.getValue())."

메세지를 해석해보면 ViewModel에서 가져오는 값은 boxed 타입인데 String.valueOf() 함수를 사용하려면 unboxed 타입으로 넣어야 한다는 말이다.

 

자바에서는 int와 같은 기본 타입과, Integer과 같은 객체 타입이 있다. 여기서 unboxed 타입은 기본타입을 뜻하는 것이고, boxed 타입은 객체 타입을 지칭하고 있다.

 

그렇기 때문에 String.valueOf() 함수는 unboxed타입을 인자로 받기 때문에 안에 들어가는 값을 safeUnbox() 함수를 이용하여 안전하게 기본타입 값으로 추출해야 한다.

다음 코드와 같이 말이다.

android:text="@{String.valueOf(safeUnbox(myViewModel.result))}

 

바인딩 표현식(양방향)

단방향 표현식과 반대되는 개념으로, 지금까지 배운 단방향은 대응되는 데이터를 읽어들여 TextView에 표시했다면 양방향은 반대로 TextView에 있는 내용을 대응되는 데이터에 갱신해주는 것이다.

사용방법은 단방향 표현식과 유사하다. 단뱡향 표현식에서 '@'를 '@="로 바꿔주면 된다

android:text="@={myViewModel.result}"

 

이벤트와 리스너 바인딩

데이터 바인딩으로 뷰의 이벤트에 대한 응답으로 함수를 호출할 수 있다.

android:onClick="@{uiController::convertCurrency}"

위 코드는 uiController 변수가 가르키는 뷰에서 convertCurrency 함수를 호출하는 것이다.

다음은 리스너 바인딩이 있다. 이는 인자(argument)를 전달하면서 함수를 호출할 수 있다.

android:onClick="@{() -> myViewModel.method(viewModel.result, 10)}"

 

데이터 바인딩 어댑터 (Data Binding Adapter)

바인딩 어댑터는 View에 개발자가 정의하는 메소드를 호출하여 값을 설정하는 작업을 담당한다.

다음 예제를 보면 View의 속성(android:id, android:layout_width.....)이 여러가지가 있는 것을 확인할 수 있다. 

  <TextView
      android:id="@+id/txt_number"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textSize="24sp"
      android:text="안녕하세요."
      android:textColor="@color/black"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

쉽게 말하자면 바인딩 어댑터View의 속성을 custom하게 작성하여 추가하는 것이다.

 

예제를 통해 쉽게 알아보자.

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rv_movies"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:clipToPadding="false"
    android:orientation="vertical"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/guideline"
    app:layout_constarintVertical_bias="0.0"
    bind:endlessScroll="@{vm}"
    bind:setItems="@{vm.movieList}" />

위의 xml을 보면 기본 방법과는 다른 방법으로 사용했음을 알 수 있다.

우선, bind라고 선언된 부분은 필자가 직관적으로 구분하기 위하여 추가적으로 작성한 부분이다.  xml 선언 부 사진을 보면, xmlns:bind="~"라고 선언된 부분을 확인할 수 있을 것이다. 이처럼 선언을 하게 된다면 xml 옵션을 추가할 때 위의 이미지 처럼 추가할 수 있는 것이다.

물론, bind 대신 기존에 작성 되어있던 app:을 그대로 써도 상관 없으며, 아예 해당 부분을 생략해도 상관 없다.

사용자의 편의에 따라서 맞춰서 작성하면 된다.

 

그렇다면, 뒤의 endlessScroll, setItems는 무엇인가?

필자가 데이터 바인딩 어댑터로 사용하기 위하여 어노테이션에 추가해 놓은 이름이다.

이게 무슨말인지는 하단의 코드를 보면 알 수 있다.

@BindingAdapter("setItems")
fun RecyclerView.setAdapterItems(items: MutableList<Movie>?){
    items?.let{
        (adapter as MovieAdater).submitList(it.toMutableList())
    }
}

@BindingAdapter("endlessScroll")
fun RecyclerView.setEndlessScroll(
        viewModel: MovideSearchViewModel
){
    val scrollListener =
        object : EndlessRecyclerViewScrollListener(layoutManager as LinearLayoutManager) {
            override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?){
                viewModel.requestPagingMovie(totalItemsCount + 1)
            }
        }
    addOnscrollListener(scrollListener)
}

코드를 확인해보면, @BindingAdpater 라는 어노테이션을 선언하고 그 안에 value 값으로 xml 에 추가된 이름을 확인할 수 있을 것이다. 이처럼 BindingAdapter 어노테이션을 선언하고 이름은 넣어주게 되면 해당 이름으로 선언된 함수를 bind하여 사용할 수 있게 된다.

즉, 위의 코드 중 setItems로 예를 들자면 "rv_movies라는 id를 가지고 있는 RecyclerView에 setItems라는 이름으로 BindingAdpater가 선언되어 있는 함수를 수행시키고, 그 인자 값으로 vm.moviewList를 넣어라." 라는 얘기가 된다.

 

그렇다면 데이터 바인딩 어댑터는 왜 쓰는 것일까?

사실 앞서 작성한 예제는

그냥 액티비티 안에서 뷰를 불러오는 방법을 사용하면 더 적은 양의 코드로 해결할 수 있다.

그럼에도 불구하고 이렇게 Binding Adapter를 사용하는 근본적인 이유는 '역할의 분리'라고 생각한다.

 

내가 여태 배웠던 클린 아키텍처, MVVM, View Model, Data Binding ···

전부 궁극적으로는 역할의 분리를 위한 것들이었다.

하나의 Activity에 모든 기능을 다 때려 넣는 게 아니라 각자 역할과 기능을 가지는 모듈로 잘게 쪼개자는 것이다.

Binding Adapter도 이런 것들과 마찬가지로 바인딩이라는 작업을 액티비티로부터 떼어내기 위한 기술인 것이다.

 

데이터바인딩의 단점은?

- Databinding은 xml 파일마다 추가적으로 BindingClass가 추가적으로 생성되기 때문에, 앱의 기본적인 용량 상승을 피하거 어렵고 Binding 속도가 느려져 전체적인 개발 속도에 영향을 미칠 수 있다.

- DataBinding은 생성된 코드(generate code)이기 때문에 일반 코드와 비교해서 디버깅하기가 어렵다.

기본적으로 XML에서 어디 라인에서 에러가 났습니다를 알려주는 경우도 분명 존재하지만, 특정 에러를 제외하고는 XML에서 에러가 나는 경우 전혀 다른 Exception을 발생하는 등의 문제를 일으키는 문제가 있기 때문에 잘 살펴봐야 한다.

 

vs ViewBinding

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

databinding과 비슷한 역할과 특징을 갖지만, 차이점으로 xml파일에 태그가 필요하지 않고 컴파일 속도가 빠르며 앱 용량이 좀 더작다는 장점이 있다.

하지만 양방향 바인딩, binding apater 등을 통한 동적 변경이 불가능하다는 단점이 있다.


참고한 사이트

https://velog.io/@blucky8649/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B2%B0%ED%95%A9Data-Binding

 

[안드로이드] 데이터 결합(Data Binding)

데이터 바인딩은 레이아웃의 뷰를 앱 코드에 저장된 데이터와 연결하는 간단한 방법을 제공하는 라이브러리입니다.예를 들어, EditText 뷰를 ViewModel의 LiveData 와 연결한다고 가정해봅시다.여기서

velog.io

https://heegs.tistory.com/59

 

[Android] 데이터 바인딩 어댑터 (Data Binding Adapter) 사용 방법

필자가 예전에 DataBinding 관련한 공부를 진행하면서 기본적인 사용 방법에 대해 간단히 작성해 둔 게시글이 있다. 이번에 클린 아키텍처 관련 예제를 작성하다 데이터 바인딩을 사용한 조금 더

heegs.tistory.com

https://no-dev-nk.tistory.com/82

 

AAC 데이터 바인딩이란?

오랜만에 면접을 보게 되었습니다. 무조건 '이직을 해야해' 하고 봤던 면접이 아니어서인지, 요즘 너무 공부를 안해서인지는 잘 모르겠지만, 스스로가 생각하기에도 제대로 답변을 못했다는 느

no-dev-nk.tistory.com