[안드로이드] Data Binding - 2

hee09·2022년 2월 2일
0
post-thumbnail

개요

기본적인 데이터 바인딩에 대한 내용은 Data Binding - 1에 정리하였고, 이 글은 Data Binding의 표현식 일부와 Binding Adapter에 대한 내용을 다루기위해 작성하였습니다.

import, variable, include

데이터 바인딩 라이브러리는 data 태그 안에서 import, variable, include 태그를 제공하고 있습니다.

  • import : 레이아웃 파일안에서 클래스에 대한 참조를 쉽게 만듭니다.
  • variable : 바인딩 표현식(@{})에 사용할 수 있는 변수(프로퍼티)를 나타냅니다.
  • include : 앱 전체에서 복잡한 레이아웃을 재사용할 수 있습니다.

variable

data 요소 내에서 여러 variable를 선언할 수 있는데, XML에서 사용할 변수라고 생각하면 됩니다. 즉, variable 요소는 레이아웃 파일 내 표현식에 사용될 프로퍼티입니다. 아래는 user, imagenote라는 variable을 선언한 예제입니다.

<data>
    <import type="android.graphics.drawable.Drawable"/>
    <variable name="user" type="com.example.User"/>
    <variable name="image" type="Drawable"/>
    <variable name="note" type="String"/>
</data>
  • 생성된 바인딩 클래스에는 각 variable에 대한 setter와 getter가 정의되어 있습니다. variable에 대해서 setter가 호출되기 전까지는 default 값을 사용합니다. 예를 들어 참조 타입은 null과 같은 값을 가지고 있습니다.

  • 최상위 태그를 <layout>으로 지정한 데이터 바인딩이 적용된 XML에서 Context 객체에 접근할 수 있습니다. 즉 Variable로 선언하지 않고도 사용할 수 있는데, 이는 루트 뷰의 getContext() 메서드에서 가져온 Context 객체입니다.


import

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을 사용하고 있습니다.


타입

import된 타입은 변수나 표현식에서 타입 참조로 사용할 수 있습니다. 아래의 예제는 import 태그를 통해 UserList를 참조하고, 이를 variable의 타입으로 사용하는 코드입니다.

<data>
    <import type="com.example.User"/>
    <import type="java.util.List"/>
    <variable name="user" type="User"/>
    <variable name="userList" type="List&lt;User>"/>
</data>

참고로 List<>와 같이 화살괄호가 들어가는 타입을 바인딩 표현식 안에서나 변수로 선언할 때는 화살괄호를 그대로 작성할 수 없고 <은 <로 >는 >로 표현하여야 합니다. 추가적인 정보는 Stack overflow - android databinding using && logical operator에 나와 있습니다.


정적 필드 및 메소드 참조

표현식에서 정적 필드 및 메소드를 참조할 때 import한 타입을 사용할 수 있습니다. 예를 들어, 최상위 함수나 최상위 프로퍼티가 선언된 클래스 파일을 import하여 가져온 후 레이아웃 파일안에서 그 클래스를 사용하여 참조할 수 있습니다.

<!-- 최상위 함수 capitalize 함수가 선언된 파일 이름이 MyStringUtils라고 가정 -->
<data>
    <import type="com.example.MyStringUtils"/>
    <variable name="user" type="com.example.User"/>
</data><TextView
    android:text="@{MyStringUtils.capitalize(user.lastName)}"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

include

<include> 태그를 통해 하나의 XML에 작성된 내용을 다른 XML로 작성해서 포함할 수 있습니다. 단, 주의할 점이 하나 있습니다. include되고 바인딩이 사용된 xml을 sub.xml, include하는 xml을 main.xml이라고 가정한다면 단지 main.xml에서 sub.xml을 inclue한다고 바인딩 표현식에서 사용한 값들이 나타나지 않습니다. 아래와 같이 사용해야 합니다.

include되는 sub.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="model" type="com.example.Model"/>
   </data>
   <TextView
       android:text='@{"include xml... data : " + model.name}' />
</layout>

include하는 main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="model" type="com.example.Model"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <include layout="@layout/sub"
           bind:model="@{user}"/>
   </LinearLayout>
</layout>
  • include에 의해 데이터 바인딩이 정상적으로 되려면 include 하는 XML(main.xml)에서 명시적으로 include 대상이 되는 XML(sub.xml)에 데이터를 넘겨줘야 하며, include 대상이 되는 XML(sub.xml) 내에도 이를 받을 준비가 되어야 합니다.

  • <include> 태그에 bind:model과 같이 이용할 variable을 bind:variable 형식으로 지정합니다.


Binding Adapter

바인딩 어댑터는 뷰에 개발자가 정의하는 메소드를 호출하여 값을 설정하는 작업을 담당합니다. 예를 들면 setText() 메서드를 통해 뷰의 text 값을 설정하거나, setOnClickListener() 메서드를 통해 이벤트 리스너를 설정하는 작업이 해당됩니다. 즉, 쉽게 말하자면 바인딩 어댑터는 view의 속성을 custom하게 작성하여 추가하는 것입니다. 예제를 통해 알아보겠습니다.


build.gradle 수정

// DataBinding
buildFeatures { dataBinding = true }

dependencies {
...

def lifecycle_version = "2.4.0"

// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}

dataBinding을 위한 부분은 buildFeatures { dataBinding = true } 이고, 아래의 예제에서 ViewModel과 LiveData도 같이 사용하기에 의존성을 추가하였습니다.


xml

<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="kr.co.lee.databindingexample.viewmodels.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
        
        <!-- visible 이라는 Custom 속성을 정의 -->
        <TextView
            app:visible="@{viewModel.isVisible}"
            android:id="@+id/binding_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Binding Adapter 테스트"
            android:textSize="20sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

      	<!-- viewModel의 setVisible() 메소드를 통해 값 변경 -->
        <Button
            android:id="@+id/visible_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="visible or inVisible ?"
            app:layout_constraintTop_toBottomOf="@id/binding_view"
            android:onClick="@{() -> viewModel.setVisible()}"/>


    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>
  • <layout> 태그를 최상위 태그로 설정하고 XML 파일안에서 ViewModel에 접근하기 위해서 <variable>로 선언합니다.

  • 바인딩 어댑터와 관련된 부분은 TextView의 app:visible 속성입니다.

    • visible이라는 속성은 일반적으로 존재하지 않는 속성입니다.
    • 이와 같이 개발자는 임의로 속성을 선언하고 BindingAdapter라는 기능을 통해 뷰에 대한 커스텀 속성을 정의하는 것입니다.
    • 일반적으로 android:visibility 라는 속성이 있지만 예제를 위해서 사용하지 않고 Binding adapter를 만든 것입니다.

BindingAdapter

@BindingAdapter("visible")
fun visible(view: View, isVisible: Boolean) {
    view.visibility = if(isVisible) View.VISIBLE else View.INVISIBLE
}

// 확장함수 형태
@BindingAdapter("visible")
fun View.visible(isVisible: Boolean) {
	visibility = if(isVisible) View.VISIBLE else View.INVISIBLE
}
  • 바인딩 어댑터는 @BindingAdapter 애노테이션을 통해 생성합니다.
    • 인자를 여러 개 넘기고 싶다면 value 파라미터로 String 배열과 requireAll 파라미터로 true 또는 false를 지정하면 됩니다.
  • 커스텀 속성으로 지정한 이름을 애노테이션의 생성자 인자로 지정합니다.
  • 함수명은 개발자 임의로 지정하면 되고, 첫 번째 매개변수는 바인딩 어댑터를 적용할 View를 지정, 두 번째 매개변수는 XML의 표현식에서 지정한 매개변수를 넣으면 됩니다.
    • 확장 함수를 사용해서 선언하면 매개변수에 view를 넘길 필요가 없습니다.

위의 코드는 isVisible이라는 Boolean의 매개변수 값에 따라서 View를 보이게하고 안 보이게 하는 코드입니다.


ViewModel 및 액티비티

액티비티

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding
    lateinit var mainViewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // ViewModel 객체 생성
        mainViewModel = ViewModelProvider(this)[MainViewModel::class.java]

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

        binding.apply {
            // binding의 lifeCycle을 액티비티로 지정
            lifecycleOwner = this@MainActivity
            // XML의 variable로 선언한 ViewModel 객체 지정
            viewModel = mainViewModel
        }
    }
}
  • ViewModelProvider를 통해 ViewModel 객체를 생성
  • DataBindingUtil을 사용하여 레이아웃을 지정
  • binding의 lifecycleOwner를 액티비티로 지정하고, variable로 선언한 viewModel에 ViewMOdel 지정

ViewModel

class MainViewModel: ViewModel() {
    private val _isVisible = MutableLiveData<Boolean>(false)
    val isVisible: LiveData<Boolean> get() = _isVisible

    // 버튼을 클릭하면 호출되는 메소드
    fun setVisible() {
        _isVisible.value = (_isVisible.value != true)
    }
}
  • XML에서 visible 속성의 인자로 넣은 isVisible 데이터를 생성합니다.
    • 데이터의 타입은 LiveData<Boolean> 타입입니다.
    • setVisible 메소드를 통해 이 값을 변경하고 있습니다. 위의 예제에서는 이 코드를 버튼에 연결하였습니다.

정리하자면 BindingAdapter의 구조와 정의 아래와 같습니다.

  • 구조
    • XML에서 개발자가 원하는 이름으로 속성을 정의합니다.
    • 이 속성의 이름을 BindingAdapter 어노테이션을 통해 지정하고, 함수를 선언하여 해당 View에 대한 행동을 작성합니다.
  • 정의
    정리하자면 android에서 제공해주지 않는 속성을 직접 만들어 사용할 수 있는 것이 BindingAdapter입니다. 이를 응용하면 많은 기능을 작성할 수 있습니다.


양방향 바인딩(Two way Binding)

기본적으로 데이터 바인딩은 단방향 바인딩입니다. 단방향 바인딩이란 데이터 모델(주로 LiveData 등)의 값이 변경되면 연결된 레이아웃에 해당 값을 변경해주지만 레이아웃의 값이 변경되면 데이터 모델은 변경되지 않고 레이아웃의 값만 변경됩니다. 반면 양방향 바인딩은 기본적으로 단방향 바인딩의 기능을 가지고 있고 그에 더해 레이아웃의 값이 바뀌면 연결된 데이터 모델의 값도 변경됩니다.

간단한 예제를 확인해보겠습니다.

ViewModel 코드

class MainViewModel : ViewModel() {
    val checkFlag = MutableLiveData(false)
}

Activity 코드

class MainActivity : AppCompatActivity() {
    private lateinit var viewModel: MainViewModel
    private lateinit var binding: ActivityMainBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        viewModel = ViewModelProvider(this)[MainViewModel::class.java]

        binding.apply {
            vm = viewModel
            lifecycleOwner = this@MainActivity
        }
    }
}

xml 코드

<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="vm"
            type="com.example.samplecode.MainViewModel" />
    </data>

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

        <CheckBox
            android:text="Check test"
            android:checked="@={vm.checkFlag}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toTopOf="@id/label_check"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

        <TextView
            android:id="@+id/label_check"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.checkFlag.toString()}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

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

양방향 바인딩은 일반적인 바인딩 표현식과는 다르게 (@={})과 같이 사용합니다. 현재는 CheckBox의 checked에 양반향 바인딩을 설정하였기에 check 상태가 되면 ViewModel의 checkFlag 값이 true로 바뀌고, uncheck 상태가 되면 checkFlag 값이 false로 바뀌게 됩니다.


참조
안드로이드에서 DataBinding에 BindingAdapter 사용법
데이터바인딩 two-way binding 원리
안드로이드 developer - Binding adapters
안드로이드 ViewModel
안드로이드 LiveData
안드로이드 DataBinding

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

profile
되새기기 위해 기록

0개의 댓글