Computer >> 컴퓨터 >  >> 시스템 >> Android

안드로이드 데이터 바인딩 완벽 가이드: XML 레이아웃과 데이터를 연결하는 실전 방법

데이터 바인딩(Data Binding)은 데이터를 UI 요소와 연결하는 기술입니다. 이 방식을 사용하면 입력 요소가 업데이트될 때마다 그 뒤에 있는 데이터도 함께 갱신되므로, 코드를 훨씬 깔끔하게 관리할 수 있습니다.

사실 데이터 바인딩은 전혀 새로운 개념이 아닙니다. AngularJS, React, Vue 같은 프론트엔드 프레임워크들은 이미 오래전부터 이 개념을 핵심 설계에 도입해 왔습니다.

하지만 이 글의 초점은 프론트엔드 프레임워크가 아니라 모바일 개발에 있습니다. 구글은 안드로이드 Jetpack의 일부로 Data Binding Library를 제공하며, 이를 활용하면 반복적인 코드 작성을 크게 줄일 수 있습니다.

Jetpack 라이브러리에 익숙하지 않다면, 구글이 기존 서포트 라이브러리(Support Library) 개발을 중단하고 AndroidX 라이브러리(서포트 라이브러리의 최신 버전)로 전환하고 있다는 점을 기억해 두시기 바랍니다.

어댑터(Adapter)를 활용한 데이터 바인딩 설명은 이미 많은 글에서 다루고 있기 때문에, 이 글에서는 어댑터 없이 최소한의 코드로 데이터 바인딩을 구현하는 가장 기본적인 방법을 소개하겠습니다.

왜 데이터 바인딩을 사용해야 할까요?

아직 확신이 서지 않는다면, 간단한 예제를 통해 데이터 바인딩의 장점을 살펴보겠습니다. 세 개의 커스텀 버튼으로 구성된 메뉴 화면이 있고, 각 버튼은 자체적인 레이아웃을 가지고 있다고 가정해 봅시다.

안드로이드 데이터 바인딩 완벽 가이드: XML 레이아웃과 데이터를 연결하는 실전 방법
예제 레이아웃

이 화면을 만드는 한 가지 방법은 네 개의 서로 다른 XML 레이아웃 파일을 작성하는 것입니다. 하나는 메인 레이아웃용, 나머지 셋은 각각의 버튼용입니다.

버튼마다 사용자를 앱의 다른 화면으로 안내해야 하므로, 각각 다른 텍스트와 다른 이미지가 필요하기 때문입니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_gravity="center_horizontal"
    android:layout_height="wrap_content"
    android:layout_width="wrap_content">


    <ImageView
        android:id="@+id/imageView"
        android:layout_height="100dp"
        android:layout_width="100dp"
        android:src="@drawable/image_name"
        android:adjustViewBounds="true"
        android:scaleType="centerInside"
        />

    <TextView
        android:id="@+id/textView"
        android:gravity="center_horizontal"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:text="Image Text"
        android:textSize="16sp" />

</LinearLayout>
버튼 하나의 레이아웃

물론 레이아웃이 세 개뿐이라 코드 중복이 크게 느껴지지 않을 수 있습니다. 하지만 잘 생각해 보면, 이런 반복 코드를 일일이 관리하는 것은 상당히 비효율적입니다. 제품 목록과 이미지를 보여주는 것처럼 더 복잡한 레이아웃을 가진 애플리케이션이라면, 지루한 코드 복제가 눈덩이처럼 불어날 것입니다.

반면 데이터 바인딩을 활용하면, 단 하나의 XML 레이아웃만 만들어 모든 버튼에 재사용할 수 있습니다.

어디서부터 시작해야 할까?

먼저 프로젝트에서 데이터 바인딩을 활성화해야 합니다. 애플리케이션의 build.gradle 파일에 dataBinding 요소를 추가하면 됩니다.

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.tomerpacific.example"
        minSdkVersion 15
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        dataBinding {              //<-------
          enabled = true
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}
build.gradle 설정

프로젝트를 동기화한 후에는, 레이아웃에 바인딩할 데이터 클래스를 생성합니다.

package com.tomerpacific.example

import android.graphics.drawable.Drawable

data class ButtonData(val buttonText: String, val buttonImageSrc : Drawable)
ButtonData.kt

ButtonData 클래스에는 두 개의 필드가 있습니다.

  • buttonText — 이미지 아래에 표시될 텍스트입니다.
  • buttonImageSrc — 버튼의 이미지를 담당합니다.

더 많은 데이터가 필요하다면, 데이터 클래스에 필드를 추가하기만 하면 됩니다.

실제 바인딩 구현하기

다음은 레이아웃에서 변수를 선언하여 사용할 수 있도록 만드는 단계입니다. 이 변수는 앞서 만든 데이터 클래스와 연결됩니다. 이를 위해 두 가지 작업이 필요합니다.

  • 루트 레이아웃 요소를 <layout> 태그로 감쌉니다.
  • 변수(buttonData) 선언을 담을 <data> 태그를 추가합니다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="https://schemas.android.com/apk/res/android">  // <---- 1
                                                                              
    <data>
        <variable name="buttonData" type="com.tomerpacific.example.ButtonData"/> // <---- 2
    </data>
    
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:app="https://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">


        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Example"
            android:textSize="30dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/linearLayout3"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView2">

            <ImageButton
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:adjustViewBounds="true"
                android:scaleType="centerInside"
                android:src="@drawable/android">

            </ImageButton>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:text="Image Text"
                android:textSize="16sp" />

        </LinearLayout>

        <LinearLayout
            android:id="@+id/linearLayout3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView2"
            app:layout_constraintVertical_bias="0.504">

            <ImageButton
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:adjustViewBounds="true"
                android:scaleType="centerInside"
                android:src="@drawable/android_p_logo">

            </ImageButton>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:text="Image Text"
                android:textSize="16sp" />

        </LinearLayout>

        <LinearLayout
            android:id="@+id/linearLayout2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.200"
            app:layout_constraintStart_toEndOf="@+id/linearLayout3"
            app:layout_constraintTop_toBottomOf="@+id/textView2"
            app:layout_constraintVertical_bias="0.504">

            <ImageButton
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:adjustViewBounds="true"
                android:scaleType="centerInside"
                android:src="@drawable/android_studio_icon">

            </ImageButton>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:text="Image Text"
                android:textSize="16sp" />

        </LinearLayout>


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

루트 <layout> 태그로 스키마가 옮겨졌으므로, 기존 메인 레이아웃의 스키마 선언은 제거해도 됩니다. 또한 추가한 변수가 데이터 클래스와 직접 연결된다는 점에 주목하세요.

이제 MainActivity 파일에 바인딩을 처리하는 코드를 추가해야 합니다.

package com.tomerpacific.example

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import com.tomerpacific.example.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val binding: ActivityMainBinding = DataBindingUtil.setContentView(
            this, R.layout.activity_main)

        binding.buttonData = ButtonData("First", resources.getDrawable(R.drawable.android))
    }
}
MainActivity.kt

레이아웃 내에 변수를 선언하면, 해당 레이아웃을 위한 바인딩 클래스가 자동으로 생성됩니다. 이 예제에서 레이아웃 이름은 activity_main이므로, 바인딩 클래스 이름은 ActivityMainBinding이 됩니다.

네이밍 규칙은 항상 '레이아웃 이름 + Binding'입니다.

레이아웃에서 buttonData라는 변수를 선언했기 때문에, 이 변수는 바인딩 객체에 포함되며 ButtonData 클래스의 새 인스턴스를 할당할 수 있습니다.

여기까지 완료했다면, 드디어 레이아웃에서 방금 바인딩한 데이터를 사용할 수 있습니다.

<LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/linearLayout3"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView2">

            <ImageButton
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:adjustViewBounds="true"
                android:scaleType="centerInside"
                android:src="@{buttonData.buttonImageSrc}">  // <----

            </ImageButton>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:text="@{buttonData.buttonText}"   // <----
                android:textSize="16sp" />
바인딩된 변수 buttonData 사용하기

실행 결과는 다음과 같습니다.

안드로이드 데이터 바인딩 완벽 가이드: XML 레이아웃과 데이터를 연결하는 실전 방법
꽤 매끄럽죠?

잠깐, 문제가 있습니다

버튼은 세 개인데 데이터 클래스는 하나의 버튼에만 사용할 수 있다면 어떻게 해야 할까요?

이럴 때는 여러 버튼 데이터를 담는 리스트 형태의 클래스를 만들어 해결할 수 있습니다.

package com.tomerpacific.example

data class ButtonsData(val buttonsData : List<ButtonData>) {

    fun get(index: Int) : ButtonData {
        return buttonsData.get(index)
    }
}
ButtonsData.kt

레이아웃 내에서 사용할 때 이 메서드가 인식되어야 하기 때문에, get 메서드를 오버라이드해야 합니다.

그다음 activity_main.xml의 참조를 변경합니다.

 <data>
        <variable name="buttonsData" type="com.tomerpacific.example.ButtonsData"/>
 </data>
변수 이름과 클래스를 ButtonsData에 맞게 변경

새로운 데이터 클래스에 대한 바인딩도 새로 생성해야 합니다.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val binding: ActivityMainBinding = DataBindingUtil.setContentView(
            this, R.layout.activity_main)

        val firstButton : ButtonData = ButtonData("First", resources.getDrawable(R.drawable.android))

        val secondButton : ButtonData = ButtonData("Second", resources.getDrawable(R.drawable.android_p_logo))

        val thirdButton : ButtonData = ButtonData("Third", resources.getDrawable(R.drawable.android_studio_icon))

        val buttonsData : ButtonsData = ButtonsData(listOf(firstButton, secondButton, thirdButton))

        binding.buttonsData = buttonsData
    }
MainActivity.kt

ButtonData 클래스의 인스턴스 세 개를 생성한 뒤, 이를 담는 ButtonsData 객체를 만들어 바인딩 객체에 연결합니다.

마지막으로, 이제 레이아웃에서 데이터 클래스를 제대로 활용할 수 있습니다.

 <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/linearLayout3"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView2">

            <ImageButton
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:adjustViewBounds="true"
                android:scaleType="centerInside"
                android:src="@{buttonsData.get(0).buttonImageSrc}">   // <-------

            </ImageButton>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:text="@{buttonsData.get(0).buttonText}"       // <--------
                android:textSize="16sp" />

        </LinearLayout>
buttonsData 요소 사용 예시
안드로이드 데이터 바인딩 완벽 가이드: XML 레이아웃과 데이터를 연결하는 실전 방법
완벽하게 동작합니다

마치며

이 글은 필자가 다음 애플리케이션을 개발하면서 얻은 경험을 바탕으로 작성되었습니다.

(어쩌면 여러분에게도 유용할지 모르니 소개해 봅니다)

Laundry Symbols — Google Play 앱: 세탁 기호의 의미를 알기 어려웠던 적이 있나요? 이 앱은 사용하기 쉬운 인터페이스와 그림과 함께 낯선 세탁 기호들의 의미를 모두 설명해 줍니다.

안드로이드 데이터 바인딩 완벽 가이드: XML 레이아웃과 데이터를 연결하는 실전 방법tomerpacificApps on Google Play안드로이드 데이터 바인딩 완벽 가이드: XML 레이아웃과 데이터를 연결하는 실전 방법
Google Play 스토어에서 다운로드하세요

전체 소스 코드는 GitHub에서 확인할 수 있습니다:

TomerPacific/LaundrySymbols — 다양한 세탁 기호와 그 설명을 보여주는 Kotlin으로 작성된 애플리케이션입니다.

안드로이드 데이터 바인딩 완벽 가이드: XML 레이아웃과 데이터를 연결하는 실전 방법TomerPacificGitHub안드로이드 데이터 바인딩 완벽 가이드: XML 레이아웃과 데이터를 연결하는 실전 방법
전체 소스 코드 보기