티스토리 뷰

안드로이드/코드

데이터 바인딩 사용해보기

알렌보이스 2022. 5. 28. 21:58
728x90

이번에는 데이터 바인딩에 대하여 포스팅을 해보려고 합니다.

데이터 바인딩 (DataBinding)은 Android Jetpack 라이브러리 중 하나로,

 

UI를 바인딩할 수 있는 지원 라이브러리입니다.
선언문을 사용하여 레이아웃의 구성 요소를 앱의 데이터 소스로 이동 포맷할 수 있습니다.

 

이렇게 정의되어 있습니다.

Activity, Fragment 등에서 직접 UI 작업을 진행했던 것을 Data Binding을 활용하여 데이터에 따라 UI를 변경하는 방식입니다.

 

1.  사용 준비

android {
    compileSdk 32

    defaultConfig {
        applicationId "com.example.standardstudy"
        minSdk 26
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildFeatures {
        dataBinding true
    }
    ...
}
...

Data Binding을 사용하기 위해서는 app 수준의 build.gradle에 buildFeatures를 해당 위치에 넣어 주시면 끝입니다.

그다음은 xml 파일입니다.

<?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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".databinding.BindingTestActivity">


    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

원래 사용하던 최상단의 뷰를 <layout>으로 감싸줍니다.

지금은 뷰 밖에 없지만 아래에서 <layout> 태그 안에 <data>도 추가될 예정입니다

 

다음은 Activity 입니다.

Activity에서 데이터 바인딩을 세팅해주어야 하는데 2가지 방식이 존재합니다.

class BindingTestActivity : AppCompatActivity() {

    private lateinit var binding : ActivityBindingTestBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityBindingTestBinding.inflate(layoutInflater)
        setContentView(binding.root)

    }
}

위의 두 단계를 진행하면 다음과 같은 형식으로 된 클래스 파일이 존재할 겁니다.

저의 경우 BindingTestActivity이므로 ActivityBindingTestBinding로 나옵니다.

그다음 inflate의 매개변수로 LayoutInflate를 넣어주면 됩니다.

주의할 점은 기존에 있던 setContentView안에 R.layout.ooo으로 된 부분 대신 binding.root로 넣어주셔야 데이터 바인딩을 이용할 수 있습니다.

private val binding : ActivityBindingTestBinding by lazy { ActivityBindingTestBinding.inflate(layoutInflater) }

 

저는 이 방식을 이용할 땐 전역 변수로 위와 같이 선언해 둡니다.

class BindingTestActivity : AppCompatActivity() {

    private lateinit var binding : ActivityBindingTestBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_binding_test)

    }
}

두 번째 방식은 DatabindingUtil을 이용하는 방식입니다.

기존에 있던 setContentView를 조금만 수정하면 사용 가능해서 이 부분이 편할 수도 있습니다

어떤 방식을 사용할지는 개인적인 취향에 맞게 정하시면 됩니다.

Fragment나 Adapter등에서도 크게 사용법은 다르지 않아서 따로 다루지는 않겠습니다.

 

2. Activity에서 Binding 사용하기

<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="30sp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"/>

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="24sp"
    app:layout_constraintTop_toBottomOf="@id/textView"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"/>

<Button
    android:id="@+id/button"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="click"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"/>

위와 같이 세 개의 뷰를 만들었습니다.

val textView = findViewById<TextView>(R.id.textView)
textView.text = "txt"

binding.textView.text = "text"

위의 1~2 라인은 기본적으로 뷰를 연결해서 사용하는 방식입니다.

마지막 라인은 데이터 바인딩을 이용하여 사용하는 방법입니다.

binding 변수를 통해서 xml에 정의된 모든 뷰를 접근할 수 있습니다.

여기서 주의할 점은 id가 정의되어 있지 않으면 바로 접근은 불가능하고,

만약에 xml에 view의 id를 text_view로 지었다고 해도 binding에서는 카멜 표기법으로 변환되어 textView로 변경됩니다.

저는 검색할 때 그런 점이 불편해서 카멜 표기법으로 xml에서도 동일하게 id를 만드는 편입니다.

 

3. xml에서 Data 정의 추가

<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="number"
            type="androidx.databinding.ObservableInt" />
        
        <variable
            name="activity"
            type="com.example.standardstudy.databinding.BindingTestActivity" />
        
        <import type="android.view.View"/>
    </data>
...

data를 정의하는 방법은 3가지 정도로 구분할 수 있습니다.

첫 번째는 <variable>에서 type을 기본 제공하는 클래스로 정의해서 사용하는 방법입니다.

두 번째는 <variable>에서 사용자가 정의한 클래스의 위치를 지정하여 사용하는 방법입니다.

세 번째는 <import>에서 타입을 제공받아서 사용하는 방법입니다.

<variable>과 <import> 방식의 차이는
<variable>의 경우 activity에서 해당 클래스의 맞는 값을 넣어주어야 하고,
<import>는 activity와 별도 동작 없이 사용 가능합니다.

이제 데이터를 사용해봅시다.

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".databinding.BindingTestActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{`` + number}"
        android:textSize="30sp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="24sp"
        android:text="@{number%2==0 ? `짝수입니다.` : `홀수입니다.`}"
        android:visibility="@{number > 10 ? View.VISIBLE : View.GONE, default=visible}"
        app:layout_constraintTop_toBottomOf="@id/textView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <Button
        android:id="@+id/button"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="click"
        android:onClick="@{()->activity.setIncrease()}"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

DataBinding의 값을 사용할 땐 기본적으로 @{}를 이용해서 값을 넣어줍니다.

하나씩 봅시다

첫 번째 TextView에서는 "@{`` + number}"를 사용하였습니다.

우선 ``을 사용한 이유는 number가 ObserverableInt 타입이어서 String으로 만들기 위해서입니다.
String.valueOf(numger)를 넣으셔도 무관합니다.

그냥 Integer를 사용하지 않고 ObserverableInt를 사용한 이유는
Integer타입을 이용하게 되면 한 번만 적용되고 변경되었을 때 코드에서 바인딩 값을 수정해주어야 합니다.
그렇게 하지 않고 Observable 한 타입을 이용하면 한 번만 바인딩에 값을 할당해 주면
데이터가 변경될 때마다 UI가 자동으로 수정됩니다.

이점을 잘 고려해서 필요에 따라서 Observable타입을 이용할지 말지 정하시면 됩니다.
무조건 Observable을 사용할 필요는 없습니다

 

android:text="@{number%2==0 ? `짝수입니다.` : `홀수입니다.`}"
android:visibility="@{number > 10 ? View.VISIBLE : View.GONE, default=visible}"

두 번째에서는 비교 연산자와 삼항 연산자를 이용해봤습니다.

사용 가능한 연산자들은 아래와 같습니다.

더보기
  • Mathematical + - / * %
  • String concatenation +
  • Logical && ||
  • Binary & | ^
  • Unary + - ! ~
  • Shift >> >>> <<
  • Comparison == > < >= <= (Note that < needs to be escaped as &lt;)
  • instanceof
  • Grouping ()
  • Literals - character, String, numeric, null
  • Cast
  • Method calls
  • Field access
  • Array access []
  • Ternary operator ?:

DataBinding을 이용하는 건 삼항 연산자 정도까지만 이용하고 복잡한 로직은 함수로 만들어서 사용해야 합니다.

만약 strings나 color, drawable을 이용하고 싶을 경우에는

<string name="increase">누적 : %d</string>
<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{@string/increase(number)}"
    android:textSize="30sp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"/>

이런 식으로 이용이 가능합니다.

 

마지막은 함수 호출 관련입니다.

fun setIncrease() {
    number.set(number.get() + 1)
}

함수는 위와 같이 구현되어있습니다.

<Button
    android:id="@+id/button"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="click"
    android:onClick="@{()->activity.setIncrease()}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"/>

() -> activity.setIncrease()를 통해 함수 호출이 가능합니다.

fun setIncrease(view: View) {
    number.set(number.get() + 1)
}
<Button
    android:id="@+id/button"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="click"
    android:onClick="@{activity::setIncrease}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"/>

다른 방식은 activity::setIncrease를 사용하는 것인데요

차이점은 함수를 보면 view: View가 있는 것을 알 수 있습니다.

둘의 차이는 이렇게 이해하시면 편할 것 같습니다.

val listener : () -> Unit = {
    // 생략
}

binding.button.setOnClickListener {
    listener.invoke()
}

val onClickListener = object : View.OnClickListener {
    override fun onClick(view: View) {
        // 생략
    }
}

binding.button.setOnClickListener(onClickListener)

필요에 따라 맞게 사용하면 될 것 같습니다.

 

이제 xml에서의 사용법은 알아봤으니 activity에서 작업을 진행해 줍니다.

private var number : ObservableInt = ObservableInt(1)

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(binding.root)

    binding.number = number
    binding.activity = this

}

<variable>로 정의한 number와 activity는 위와 같이 초기 값을 넣어주어야 정상적으로 작동을 하게 됩니다.

 

4. BindingAdapter

지금까지 알아본 방식으로 UI를 위해 작성했던 코드를 많이 줄일 수 있었을 겁니다.

그런데 onClick처럼 제공하지는 않지만 자주 사용하는 로직이 있다고 하면
매번 다른 Activity에 정의하는 건 번거로운 일입니다.

그래서 있는 게 BindingAdapter입니다

@BindingAdapter("increaseText")
fun increaseText(view: TextView, number: Int) {
    view.text = view.context.getString(R.string.increase, number)
}

@BindingAdapter("increaseButtonClick")
fun increaseButtonClickListener(view: View, activity: Activity) {
    view.setOnClickListener {
        activity.toast("increaseButton을 클릭하였습니다.")
    }
}
<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="30sp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:increaseText="@{number}"/>
    
    ...

<Button
    android:id="@+id/button"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="click"
    android:onClick="@{()->activity.setIncrease(2)}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:increaseButtonClick="@{activity}"/>

단순 예제로 작성했기 때문에 의미가 있을까 싶겠지만..

    @BindingAdapter("imageUrl", "error")
    fun loadImage(view: ImageView, url: String, error: Drawable) {
        Picasso.get().load(url).error(error).into(view)
    }

위와 같이 이미지를 로딩하는 로직을 사용한다고 하면 활용도가 좋아지겠죠?ㅎㅎ

 

데이터 바인딩 사용하면 편리하고 좋은데 처음부터 모든 UI 코드를 xml에 데이터 바인딩 적용하는 것은 추천하지 않습니다.

잘 못 사용하게 되면 의도치 않게 UI가 변경되고 특히 Observable을 이용할 때 예상치 못 한 오류가 생길 수 있습니다.

이 부분은 코드로 작성한 게 아니어서 오류 찾기가 쉽지 않을 수 있으므로 많이 사용해보고

자신의 맞는 사용법을 익혀서 하나씩 적용해보는 것을 추천드립니다.

 

 

참고 자료

공식 문서 : https://developer.android.com/topic/libraries/data-binding?hl=ko 

 

데이터 결합 라이브러리  |  Android 개발자  |  Android Developers

데이터 결합 라이브러리 Android Jetpack의 구성요소. 데이터 결합 라이브러리는 프로그래매틱 방식이 아니라 선언적 형식으로 레이아웃의 UI 구성요소를 앱의 데이터 소스와 결합할 수 있는 지원

developer.android.com

 

728x90
댓글