[안드로이드] Data Binding

hee09·2021년 11월 30일
0
post-thumbnail

데이터 바인딩

데이터 바인딩은 안드로이드 JetPack 라이브러리 중 하나입니다. 이름 그대로 액티비티/프래그먼트의 데이터를 화면에 출력하는 부분을 도와주는 AAC(Android Archiecture Component)의 기법입니다. 데이터 바인딩에 대해서 알아보기 위해 일반적으로 뷰 객체를 다루는 코드와 비교해보겠습니다.

XML 코드

<androidx.appcompat.widget.LinearLayoutCompat 
    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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/nameView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</androidx.appcompat.widget.LinearLayoutCompat>

findViewById() 방식

class MainActivity : AppCompatActivity(), View.OnClickListener {

    lateinit var nameView: TextView
    lateinit var button: Button
    val name: String = "홍길동"

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

        nameView = findViewById(R.id.nameView)
        button = findViewById(R.id.button)

        nameView.text = name

        button.setOnClickListener(this)
    }

    override fun onClick(view: View?) {
        Toast.makeText(this, "Show Toast", Toast.LENGTH_SHORT).show()
    }
}

findViewById()를 통해 뷰 객체를 다루는 코드입니다. 우선 뷰 객체를 획득하고 획득한 뷰 객체에 데이터를 대입합니다. 그리고 뷰에 이벤트를 등록합니다. 이러한 코드는 XML에 정의한 뷰를 획득하고자 findViewById() 메서드를 여러 번 호출해야 하며, 이벤트 처리를 위해 setOnXXXListener()등의 함수를 반복해서 호출해야 합니다.

데이터 바인딩 방식(액티비티 코드)

class MainActivity : AppCompatActivity() {
    lateinit var binding: ActivityMainBinding
    val name: String = "홍길동"

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

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.myData = this
    }

    fun onClick(view: View?) {
        Toast.makeText(this, "Show Toast", Toast.LENGTH_SHORT).show()
    }
}

데이터 바인딩 방식의 코드입니다. 위의 코드는 findViewById()를 사용하는 코드와 같은 역할을 합니다. 똑같이 뷰에 데이터를 대입하고 버튼에 클릭 리스너를 달아주는 코드입니다. 이 코드의 핵심은 뷰 변수를 선언하지 않았다는 점입니다. 변수를 선언하지 않았으므로 코드에서 뷰 객체에 데이터를 대입하거나 이벤트를 등록하는 코드를 작성할 필요가 없습니다. 대신 XML에 약간의 작업이 추가됩니다. 아래에서 자세히 알아보겠습니다.


데이터 바인딩 기초

우선 데이터 바인딩을 이용하기 위해서 모듈 수준의 build.gradle 파일에 아래와 같은 설정이 필요합니다.

dataBinding {
    enabled = true
}

위의 액티비티 코드를 보면 뷰와 관련된 내용을 작성하지 않았는데 데이터가 뷰에 대입되고 뷰의 이벤트 처리가 가능한 것은 데이터 바인딩이 이름 그대로 코드의 데이터를 자동으로 뷰에 바인딩해주기 때문입니다. 물론 이렇게 설정하려면 XML에서 코드를 추가해야 합니다.

데이터 바인딩을 사용하는 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">

    <data>
        <variable
            name="myData"
            type="kr.co.lee.databindingexample.MainActivity" />
    </data>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <TextView
            android:text="@{myData.name}"
            android:id="@+id/nameView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <Button
            android:onClick="@{myData::onClick}"
            android:id="@+id/button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </androidx.appcompat.widget.LinearLayoutCompat>
</layout>

데이터 바인딩에 이용하고자 하는 XML의 루트 태그는 <layout>으로 선언해야 합니다. layout 태그만 선언해도 자바 코드에서 findViewById() 함수나 변수 선언 없이 XML에 정의한 뷰 객체를 바로 이용할 수 있습니다.

<data> 태그를 선언하면 코드의 데이터가 자동으로 XML의 뷰 객체에 대입됩니다. 위에서는 data 태그 내에 myData라는 이름의 variable를 선언했습니다. 이 variable은 XML에서 이용하고자 변수를 선언한 것입니다. 이 변수에 대입할 데이터는 코드에서 작성을 하고, 데이터를 어떤 뷰에 대입해야 하는지는 코드가 아니라 XML에 명시한다는 개념입니다.

데이터 바인딩이라는 것은 결국 코드로 작성하던 부분을 XML에 작성하자는 개념입니다. 화면을 XML로 작성한다면 그 화면의 데이터 처리와 이벤트 처리 등도 XML에 작성해서 코드에는 해당 데이터를 위한 작업 중심의 코드만 작성하자는 개념입니다. 화면 처리와 작업 처리 부분을 분리해서 개발하는 기법이라고 보면 됩니다.

이 글에서는 예제를 위해서 View에 해당하는 액티비티에서 데이터를 처리하였지만 실제로 안드로이드 Jetpack 라이브러리의 AAC등을 사용한 MVVM 패턴으로 코드를 작성하면 데이터는 ViewModel에 선언하는 것이 좋습니다.

데이터 바인딩을 사용하는 액티비티 코드

class MainActivity : AppCompatActivity() {
    lateinit var binding: ActivityMainBinding
    val name: String = "홍길동"

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

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.myData = this
    }

    fun onClick(view: View?) {
        Toast.makeText(this, "Show Toast", Toast.LENGTH_SHORT).show()
    }
}
  • Binding 클래스
    데이터 바인딩을 이용하면 ActivityMainBinding과 같은 클래스가 자동으로 생성됩니다. 이 클래스는 layout 태그로 선언된 XML을 위해 자동으로 만들어지는 클래스이고 클래스명은 XML 파일명을 따릅니다. XML 파일명이 activity_main.xml이면 클래스명은 자동으로 ActivityMainBinding이 됩니다. 만약 XML이 aa.xml이라면 AaBinding이라는 클래스가 자동으로 만들어집니다.

  • DataBindiugUtil.setContentView
    이제 위와 같이 자동으로 만들어진 바인딩 클래스를 이용하여 레이아웃 XML을 지정하고 초기화합니다. 어떤 XML을 이용할지는 반드시 지정해야 합니다. 이때 액티비티의 setContentView() 메서드를 이용하지 않고, DataVindingUtil 클래스의 setContentView() 메서드를 이용합니다. 이렇게 되면 XML을 초기화하고 해당 XML을 이용하기 위한 바인딩 클래스의 객체를 반환합니다.

  • Binding.setMyData()
    XML에 데이터를 대입하는 코드입니다. 바인딩 클래스에는 XML에 variable로 선언한 데이터를 이용하기 위한 게터/세터 메서드와 XML에 id 값이 등록된 뷰 객체가 멤버로 선언되어 있습니다. 코드에서 이 멤버들을 이용하기만 하면 됩니다.

만약 Fragment, ListView, RecyclerView 등 직접 LayoutInflater를 이용하는 곳에서 바인딩 클래스를 이용하려면 방법은 액티비티와 같은데, 단지 바인딩 객체를 만들 때 LayoutInflater 객체를 이용하면 됩니다.

Fragment에서 데이터 바인딩

class ExampleFragment : Fragment() {
    private var _binding: FragmentExampleBinding? = null

    private val binding
        get() = _binding!!


    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // 바인딩 클래스를 통해 지정(XML 지정 필요 없음)
        _binding = FragmentExampleBinding.inflate(inflater, container, false)
        // 직접 XML 파일 지정
        _binding = DataBindingUtil.inflate(inflater, R.layout.fragment_example, container, false)

        val view = binding.root

        return view
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }
}

액티비티와 비슷한데 직접 바인딩 클래스를 이용하여 inflate하면 XML 파일을 지정할 필요가 없고 DataBindingUtil.inflate하면 XML 파일을 지정해야 합니다. 또한 액티비티와는 다르게 바인딩 변수를 null로 선언한 것은 Single Activity에서 Fragment가 계속해서 변경될 때, onDestroy안에서 바인딩 변수를 null로 처리하여 메모리를 절약하기 위해서입니다.

데이터 바인딩 정리

UI 프로그램에서는 UI를 구성해야 하고 뷰 객체 획득, 이벤트 등록, 데이터 발생, 데이터를 뷰에 대입하는 코드가 필요합니다. 데이터 바인딩을 이용하지 않았을 때는 이러한 코드를 코틀린 파일에 작성해야 합니다. 그런데 UI를 구성하는 뷰가 등록된 곳이 XML이므로 뷰와 관련된 대부분 코드를 XML에 직접 작성하자는 개념입니다. 그렇게 하면 자바 코드는 데이터를 생성하는 업무 로직 중심의 코드만 남게 되어 구조적으로 더 좋아집니다.


다양한 타입의 데이터 바인딩

위에서 바인딩에 사용한 데이터는 String 타입인데, 이외에도 다양한 데이터를 XML에 바인딩할 수 있습니다. 모델 클래스(POJO), 리소스, 컬렉션 타입(배열, 리스트, 맵 등) 등의 데이터를 바인딩할 수 있는데 모델 클래스를 사용하여 바인딩하는 예제를 확인하겠습니다.

데이터 클래스

data class Person(
    val firstName: String,
    val lastName: String,
    val phone: String
)

위와 같이 선언된 모델 클래스를 XML에서 이용하려면 아래와 같이 작성합니다.

<?xml version="1.0" encoding="utf-8"?><!-- 데이터 바인딩을 사용할 XML은 layout 태그로 시작 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <!-- XML내에서 사용할 변수 선언 -->
        <!-- 패키지명을 포함한 클래스의 경로를 type으로 설정 -->
        <!-- name으로 선언된 이름을 XML내에서 사용 -->
        <variable
            name="person"
            type="kr.co.lee.databindingexample.Person" />
    </data>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <!-- @{}는 뷰에 데이터 바인딩을 사용하는 코드 -->
        <!-- person이라고 선언된 변수의 프로퍼티를 가져와 셋팅 -->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{person.firstName}" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{person.lastName}" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{person.phone}" />

    </androidx.appcompat.widget.LinearLayoutCompat>
</layout>

모델 클래스를 가져와 사용하기 위해 data 태그 안에 variable을 선언합니다. 이 태그 안의 코드는 XML에서 이용할 모델을 선언하는 부분입니다. name="person"에서 person은 XML 내에서 모델을 사용하기 위한 일종의 변수명입니다. 그리고 type은 해당 모델의 패키지명이 포함된 경로입니다. 이렇게 선언하면 바인딩 클래스에 모델 데이터를 대입할 수 있는 세터 함수와 데이터를 얻을 수 있는 게터 함수가 자동으로 만들어집니다.

뷰에서 데이터 바인딩의 기본 코드는 @{ } 코드입니다. @{ } 코드에 의해 모델 객체가 이용되며 '게터 -> public 필드 -> 함수명' 순으로 판단하여 데이터를 가져옵니다. 위의 예시에서 @{person.firstName}을 선언하였다면 모델 클래스에서 getFirstName() 함수가 호출되고, 선언되어 있지 않다면 public String name 필드에 접근합니다. 만약, 게터 함수와 public 필드가 없다면 firstName() 함수를 호출하고, 이 함수마저 없다면 에러가 발생합니다.

이제 액티비티 코드에서는 바인딩 객체에 모델 객체를 대입하면 됩니다.

class ExampleActivity : AppCompatActivity() {
    lateinit var binding: ActivityExampleBinding

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

        // DataBinding 레이아웃 지정
        binding = DataBindingUtil.setContentView(this, R.layout.activity_example)

        // 모델 클래스 생성 및 초기화
        val examplePerson = Person("홍", "길동", "01000000000")
        // XML에 선언된 variable(person)에 값 셋팅
        binding.person = examplePerson
    }
}

액티비티의 코드는 정말 간단합니다. 원래 ViewModel을 사용하면 ViewModel에서 Model에 일을 시켜 데이터를 가공하고 처리하여 가져온 후 Activity에 전달하겠지만 아직 ViewModel을 사용하지 않았기에 액티비티에서 데이터를 처리하고 있습니다. 단지 XML에 선언된 변수의(person) setter를 호출하여 데이터만 넘기면 데이터가 XML에 바인딩되는 구조입니다.

ViewModel + LiveData + DataBinding 구조

(ViewModel + LiveData + Activity)

// ViewModel + LiveData
class ExampleViewModel: ViewModel() {
	private val _person = MutableLiveData<Person>()
    val person: LiveData<Person> = _person
    
    fun setPerson() {
    	// Data 영역(내부 DB, 서버) 등에서 이름 획득...
        val personItem = Person(얻어온 정보를 사용하여 객체 생성)
        
    	_person.value = personItem
    }
}

// ExampleActivity
class ExampleActivity : AppCompatActivity() {
    lateinit var binding: ActivityExampleBinding
    // Activity-KTX 사용하여 ViewModel 초기화
	val viewModel: ExampleVieModel by viewModels()

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

        // DataBinding 레이아웃 지정
        binding = DataBindingUtil.setContentView(this, R.layout.activity_example)
        binding.apply {
        	// LiveData를 DataBinding에서 사용한다면 다음과 같이 설정하여 
            // LiveData의 LifeCycleOwner가 현재 activity라는 것을 명시해야함
        	lifecycleOwner = this@ExampleActivity
            vm = viewModel
        }
    }
}

데이터 바인딩을 사용하는 레이아웃

<?xml version="1.0" encoding="utf-8"?><!-- 데이터 바인딩을 사용할 XML은 layout 태그로 시작 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <!-- XML내에서 사용할 변수 선언 -->
        <!-- 패키지명을 포함한 클래스의 경로를 type으로 설정 -->
        <!-- name으로 선언된 이름을 XML내에서 사용 -->
        <variable
            name="vm"
            type="kr.co.lee.databindingexample.ViewModel" />
    </data>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <!-- @{}는 뷰에 데이터 바인딩을 사용하는 코드 -->
        <!-- person이라고 선언된 변수의 프로퍼티를 가져와 셋팅 -->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{vm.person.firstName}" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{vm.person.lastName}" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{vm.person.phone}" />

    </androidx.appcompat.widget.LinearLayoutCompat>
</layout>

XML에서의 표현식은 아래의 사이트에 추가로 나와있습니다.
안드로이드 developer - Layouts and binding expressions


이벤트 바인딩

뷰의 이벤트 처리는 데이터 바인딩을 이용해 XML에 등록할 수 있습니다. 이벤트 바인딩은 함수 참조리스너 바인딩 두 가지 형태로 제공됩니다. 둘의 가장 큰 차이점은 바인딩을 참조하는 시점에 있습니다. 함수 참조 바인딩은 컴파일 단계에서 바인딩이 참조되며 리스너 바인딩은 런타임 시전에 바인딩이 참조됩니다.


함수 참조 이벤트 바인딩

데이터 바인딩을 통해 이벤트를 등록하려면 함수를 직접 호출하는 것이 아니라 함수 참조값을 등록해 놓으면 됩니다. 이를 함수 참조에 의한 이벤트 바인딩이라고 부릅니다. 함수 참조란, ::를 이용하여 이벤트가 발생할 때 호출되어야 하는 함수 이름을 지정하는 방식입니다. 즉, 함수를 호출하는게 아니라 호출해야 하는 함수의 정보를 등록하는 것입니다. 이때, 함수의 이름은 마음대로 지정할 수 있지만, 매개변수 부분은 같아야 합니다.

onClick(view: View)의 매개변수와 같게 지정한 메서드

fun onClickHandler(view: View) {
    Toast.makeText(this, "Sample Code", Toast.LENGTH_SHORT).show()
}

xml 코드

<data>
    <!-- XML내에서 사용할 변수 선언 -->
    <!-- 패키지명을 포함한 클래스의 경로를 type으로 설정 -->
    <!-- name으로 선언된 이름을 XML내에서 사용 -->
    <variable
        name="handler"
        type="kr.co.lee.databindingexample.ExampleActivity" />
</data>

<Button
    android:id="@+id/btn_example"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="@{handler::onClickHandler}" />

메서드가 선언되어 있는 액티비티를 variable로 선언하여 XML에서 사용할 변수로 만들어준 후 함수 참조(::)를 사용하여 이벤트를 바인딩하는 코드입니다. 결국, 함수 참조에 의한 이벤트 바인딩은 이벤트 함수명은 임의로 정의할 수 있지만, 매개변수 부분이 리스너 객체의 메서드와 맞아야 합니다.


리스너 이벤트 바인딩

이벤트 등록을 위한 함수를 개발자 임의의 시그니처로 등록하려면 리스너 바인딩 방식을 이용합니다. 리스너 바인딩 방식은 이벤트 핸들러로 람다 함수를 등록하는 방식입니다. 개발자가 등록한 람다 함수가 이벤트가 발생할 때 그대로 이벤트 함수 내에서 실행되므로 개발자가 임의로 시그니처를 지정할 수 있습니다.

이벤트 코드

fun onClickListener() {
    Toast.makeText(this, "Sample Code", Toast.LENGTH_SHORT).show()
}

fun onClickListener2(person: Person) {
    Toast.makeText(this, "Sample Code - ${person.lastName}", Toast.LENGTH_SHORT).show()
}

위와 같이 버튼을 클릭할 시 호출될 함수를 선언했습니다. 함수 참조와는 다르게 매개변수를 리스너 객체의 메서드와 같게 선언하지 않았습니다. 이벤트를 바인딩하기 위해서 XML에서는 아래와 같이 사용하면 됩니다.

<Button
    android:onClick="@{() -> mainActivity.onClickListener()}"
    android:onClick="@{() -> mainActivity.onClickListener2(person)"
    ... />

이벤트가 발생할 때 호출될 함수를 직접 등록한 것이 아니라 실행되어야 하는 내용을 람다 함수로 정의한 것입니다. 이벤트 발생 시 호출되는 함수는 자동으로 만들어지며 해당 함수에 람다 함수 내용이 등록되어, 결국 이벤트 발생 시 람다 함수에 등록한 부분이 실행되는 원리입니다.
(SAM 활용에서 컴파일러에 의해 무명 객체가 생성되고 메서드가 자동으로 들어가는 방식과 비슷하다고 생각)

  • 리턴 타입이 void가 아닐 때 이벤트 바인딩을 하려면 함수 방식이던 리스너 방식이던 최종 리턴 타입에 맞는 결과를 반환하는 메서드를 작성해야 합니다.

  • 바인딩 식에서 context는 내장 변수로 존재하고 Context 객체를 지칭합니다. 따라서 variable 태그로 선언하지 않고 바로 사용할 수 있습니다.


결론

  • 데이터 바인딩의 주목적은 UI 레이아웃의 뷰를 앱 코드에 저장된 데이터(대개는 ViewModel에 존재하는 데이터들)와 연결하는 간단한 방법을 제공하는 것입니다.

  • 데이터 바인딩은 이외에도 바인딩 어댑터, 양방향 바인딩 등의 기능을 지원합니다. 해당 내용은 Data Binding - 2에서 확인할 수 있습니다.


참조
깡쌤의 안드로이드 프로그래밍
안드로이드 developer - Data binding library
안드로이드 developer - Layouts and binding expressions
안드로이드 developer - Work with Observable data objects
MVVM 패턴에서 이벤트 처리하기

틀린 부분 댓글로 남겨주시면 수정하겠습니다..!!

profile
되새기기 위해 기록

0개의 댓글