끈기 있는 개발 공간
데이터 바인딩 본문
데이터 바인딩이란?
데이터 바인딩은 명령형 방식이 아닌 선언적 형식으로 레이아웃의 UI 구성요소를 앱의 데이터와 결합할 수 있는 라이브러리이다.
선언형 프로그래밍 vs 명령형 프로그래밍
선연형 프로그래밍이란 문제에 대한 답을 정의하기보다는 문제를 설명하는 것이다. 명령형 프로그래밍은 어떤 방법으로 할 지에 중점을 두는 반면, 선언형 프로그래밍은 무엇을 할 지에 중점을 둔다.
데이터 바인딩을 사용하면 다음과 같은 효과를 기대할 수 있다.
- 보일러 플레이트 코드를 줄일 수 있다.
- findViewById 메서드를 호출할 필요가 없어 메모리 누수 및 Null Pointer Exception을 방지할 수 있다.
데이터 바인딩 설정
애플리케이션 단위 build.gradle
android {
...
buildFeatures {
databinding = true
}
}
데이터 바인딩의 설정이 끝나면 다음과 같은 기능이 활성화된다.
- 구문 강조
- 데이터 바인딩 표현식 오류 검출
- XML 코드 자동 완성
- 빠른 코드 참조
바인딩 클래스 생성
데이터 바인딩 라이브러리는 레이아웃의 변수와 뷰를 참조할 수 있는 바인딩 클래스를 생성한다. 생성되는 모든 바인딩 클래스는 ViewDataBinding을 상속한다.
xml 레이아웃 파일에서 가장 상위 레이아웃을 <layout> 태그로 감싸면 바인딩 클래스가 자동으로 생성된다. 생성되는 바인딩 클래스 이름은 기존 xml 레이아웃 파일명을 파스칼 케이스로 변경한 뒤 접미어 Binding을 붙인다.
<?xml version="1.0" encoding="utf-8"?>
<layout>
<LinearLayout />
</layout>
레이아웃에 대한 표현은 ~Binding 클래스로 작성되지만, 실제 비즈니스 로직을 추적 또는 디버깅하려면 ~BindingImpl을 참조해야 한다.
바인딩 클래스로 바인딩 객체 생성하기
바인딩 객체를 생성하는 일반적인 방법으로는 바인딩 클래스의 static 메서드를 사용하는 것이다. 먼저 inflate() 메서드를 사용해서 레이아웃 전개와 함께 바인딩 객체를 생성한다.
val binding: ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater)
만약 바인딩 클래스를 통해 레이아웃을 전개하지 않고 전개 후에 바인딩한다면 다음과 같이 bind() 메서드를 사용한다.
val binding: ActivityMainBinding = ActivityMainBinding.bind(rootView)
바인딩 클래스 이름 사용자화
기본적으로 바인딩 클래스 이름은 레이아웃 파일명을 바탕으로 생성된다. 만약 이를 변경하고 싶다면 태그 내에 class 속성을 사용할 수 있다. 예를 들어, 현재 모듈의 패캐지명이 com.tenutz라고 가정하고 com.tenutz.databinding 패키지에 ContactItem 바인딩 클래스를 생성하고 싶다면 다음과 같이 바인딩 클래스 이름을 지정할 수 있다.
<data class="ContactItem">
...
</data>
databinding 패키지가 아닌 다른 패키지에 저장하려면 온점을 통해 현재 모듈 내의 패키지에 클래스를 생성할 수 있다. com.tenutz.ContactItem 클래스를 만드는 예제는 다음과 같다.
<data class=".ContactItem">
...
</data>
전체 패키지명을 다시 지정할 수도 있는데 com.example 패키지에 ContactItem 바인딩 클래스를 생성한다고 가정하면 다음 예제와 같다.
<data class="com.example.ContactItem">
...
</data>
ID로 View 참조
바인딩 클래스를 사용하면 findViewById()를 호출할 필요가 없다. 바인딩 클래스 내부에서 미리 findViewById()를 호출한 결과를 캐싱해 두기 때문이다.
<TextView
android:id="@+id/tv"
... />
val textView: TextView = binding.tv
레이아웃에 변수 선언
데이터 바인딩 라이브러리를 사용하면 뷰의 변경을 위해 레이아웃에 id를 선언하고 뷰에 접근할 필요가 없어진다. 간단히 레이아웃에 변수를 선언하고, 변수에 값을 대입하는 것으로 뷰의 상태를 변경할 수 있다.
변수 선언은 <layout> 태그 내의 <data> 태그를 사용해야 한다. <data> 태그 내에 선언하고 싶은 변수를 <variable> 태그를 사용하여 선언하면 된다. <variable> 태그는 여러 개 선언 가능하며, name과 type 두 가지 속성을 갖는다. name에는 변수의 이름을 선언하고, type에는 변수의 자료형을 선언한다.
<layout>
<data>
<variable
name="text"
type="String" />
</data>
</layout>
POJO 클래스도 레이아웃 내에 변수 선언이 가능하다.
바인딩 표현식
일반적인 기능
바인딩 표현식에 꽤 많은 문법이 존재한다. 다음 연산자들을 xml 레이아웃에서 사용할 수 있다.
- 산술 연산자: + - / * %
- 문자열 연결: +
- 논리 연산자: && ||
- 비트 연산자: & | ^
- 단항 연산자: + - ! ~
- 비트 이동 연산자: >> >>> <<
- 비교 연산자: == > < >= <=
<는 < 로 이스케이핑 해야 한다는 점을 주의해야 한다. 데이터 바인딩은 다음 키워드들도 지원한다.
- instanceof 지원
- 그루핑은 ( ) 사용
- 문자, 문자열, 수, null 지원
- 캐스팅 지원
- 메서드 호출 지원
- 필드 접근 지원
- 배열 접근 지원
- 삼항 연산자 지원
어떤 데이터 형식을 문자열로 바꿀 때 개인적으로 toString() 메서드를 많이 쓰곤 한다. 하지만 바인딩 표현식에서는 문자열 변환을 위해 String.valueOf() 메서드를 이용해야 한다는 것을 주의해야 한다.
지원하지 않는 기능
다음 기능 및 키워드는 바인딩 표현식에서 지원하지 않는다.
- this
- super
- new
- 명시적 제네릭 호출
Null 병합 연산자 사용
null 병합 연산자(??)는 왼쪽의 피연산자부가 null이라면 오른쪽 피연산자부를 선택하도록 하는 기능이다.
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
위 코드를 null 병합 연산자를 사용하면 다음과 같이 축소할 수 있다.
android:text="@{user.displayName ?? user.lastName}"
Null Pointer Exception 회피
자동으로 생성된 바인딩 클래스는 자동으로 null을 검사하고 NPE를 회피한다. 예를 들어, @{user.name}이라는 표현식이 있고 user가 null이라면 user.name은 기본적으로 null로 배치된다. 만약 int 타입의 user.age를 참조한다면 데이터 바인딩은 기본값을 0으로 사용한다.
Collections 클래스 사용
배열, 리스트, 맵 등과 같은 일반적인 Collections 클래스들은 다음과 같이 [ ] 연산자를 사용할 수 있다.
<layout>
<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List<String>"/>
<variable name="sparse" type="SparseArray<String>"/>
<variable name="map" type="Map<String, String>"/>
<variable name="index" type="int"/>
<variable name="key" type="String"/>
</data>
...
android:text="@{list[index]}"
...
android:text="@{sparse[index]}"
...
android:text="@{map[key]}"
...
</layout>
map의 경우 @{map[key]} 대신 @{map.key}를 사용할 수도 있다.
문자열 그대로 사용
작은따옴표를 사용하면 문자열 그대로의 값을 사용할 수 있다.
android:text='@{map["firstName"]}'
큰따옴표가 바깥쪽으로 감싸진다면 문자열을 작은따옴표로 감싼다.
android:text="@{map['firstName']}"
안드로이드 리소스 참조
리소스에 참조하려면 다음과 같은 표현식을 사용한다.
andorid:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
strings의 경우 매개 변수를 가질 수도 있는데 그 경우에는 다음과 같이 사용한다.
android:text="@{string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
여러 개의 매개 변수를 갖는 복수형의 경우 모든 매개 변수를 전달한다.
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
<import> 사용
<data> 태그 내 <import> 태그를 사용하여 참조하고 싶은 클래스를 레이아웃 파일에 간단히 불러올 수 있다.
<data>
<import type="android.view.View"/>
</data>
View 클래스를 참조했으므로 View.VISIBLE과 View.GONE과 같은 상수를 참조할 수 있게 되었다.
<TextView
...
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}" />
만약 같은 이름을 갖는 클래스 두 개 이상을 참조해야 하는 경우에 충돌을 피하고자 클래스의 이름을 변경하여 참조할 수 있다.
<import type="android.view.View" />
<import type="com.example.real.estate.View"
alias="Vista" />
불러온 타입을 사용하여 변수를 선언할 때 사용할 수도 있다.
<data>
<import type="com.example.User" />
<import type="java.util.List" />
<variable name="user" type="User" />
<variable name="userList" type="List<User>" />
</data>
불러온 타입을 사용하여 바인딩 표현식에서 캐스팅하는 것도 가능하다.
<TextView
android:text="@{((User)(user.connection)).lastName}"
... />
불러온 타입의 static 필드나 메서드 참조도 가능하다. 다음 MyStringUtils 클래스의 capitalize 메서드를 참조한 모습을 확인한다.
<data>
<import type="com.example.MyStringUtils" />
<variable name="user" type="com.example.User" />
</data>
...
<TextView
android:text="@{MyStringUtils.capitalize(user.lastName)}"
... />
자바 코드와 동일하게 java.lang.*은 자동으로 import되므로 생략 가능하다.
<include> 사용
레이아웃 파일 내에서 다른 레이아웃을 포함하는 경우 <include> 태그를 사용할 수 있다. <include>에 참조되는 레이아웃 파일 또한 데이터 바인딩을 사용하는 경우 app 네임 스페이스와 변수 이름을 사용하여 데이터를 넘길 수 있다.
<!-- activity_main.xml -->
<layout>
<data>
<variable
name="user"
type="com.tenutz.jetpacklibrarysample.User" />
</data>
<LinearLayout
... >
<include layout="@layout/contact"
app:user="@{user}" />
</LinearLayout>
</layout>
<!-- contact.xml -->
<layout>
<data>
<variable
name="user"
type="com.tenutz.jetpacklibrarysample.User" />
</data>
<TextView
...
android:text="@{user.contact}" />
</layout>
주의해야 할 점은 데이터 바인딩은 하위에 를 허용하지 않는다.
이벤트 처리
데이터 바인딩은 onClick() 메서드 등과 같은 이벤트를 뷰로부터 가져와서 처리할 수 있는 기능을 제공한다. 이벤트 속성 이름들은 리스터 메서드의 이름을 따른다. 예를 들어, View.OnClickListener가 가진 메서드는 onClick()이다. 그러므로 이벤트 속성의 이름은 android:onClick()이다.
이벤트를 다루는 방법에는 메서드 참조와 리스너 바인딩 두 가지 방법이 있다.
메서드 참조
이벤트는 핸들러 메서드에 직접적으로 바인딩할 수 있다. 메서드 참조의 주요 장점 중 하나는 컴파일 타임에 해당 표현식을 검사한다는 것이다. 그러므로 해당 메서드가 존재하지 않거나 메서드 시그니처가 정확하지 않은 경우에 컴파일 타임 에러를 확인할 수 있다.
메서드 참조와 리스너 바인딩의 가장 큰 차이점은 데이터의 바인딩이 일어날 때 실제 리스너의 생성 여부이다. 만약 이벤트가 발생했을 때 바인딩 표현식을 평가하고 싶다면 리스너 바인딩을 사용하는 것이 좋다.
class MyHandlers {
fun onClickFriend(view: View) { ... }
}
<layout>
<data>
<variable name="handlers" type="com.example.MyHandlers" />
<variable name="user" type="com.example.User" />
</data>
<LinearLayout
... >
<TextView
...
android:text="@{user.firstName}"
android:onClick="@{handlers::onClickFriend}" />
</LinearLayout>
</layout>
바인딩 표현식에서 이벤트 리스너의 메서드 시그니처와 핸들러의 메서드 시그니처가 정확하게 일치해야만 한다.
리스너 바인딩
리스너 바인딩은 이벤트가 발생할 때 실행하는 바인딩 표현식이다. 메서드 참조와 비슷하지만 메서드 참조의 경우 바인딩 표현식을 임의로 실행한다.
메서드 참조에서는 메서드의 시그니처가 반드시 이벤트 리스너의 메서드 시그니처와 일치해야 한다. 하지만 리스너 바인딩에서는 단지 반환되는 타입만 이벤트 리스너의 반환 타입과 일치시킨다.
class Presenter {
fun onSaveClick(task: Task) { ... }
}
<layout>
<data>
<variable name="task" type="com.android.example.Task" />
<variable name="presenter" type="com.android.example.Presenter" />
</data>
<LinearLayout
... >
<Button
...
android:onClick="@{() -> presenter.onSaveClick(task)}" />
</LinearLayout>
</layout>
바인딩 표현식에서 콜백이 사용될 때 데이터 바인딩은 자동으로 필요한 리스너를 생성하고 해당 뷰에 리스너를 설정한다. 뷰에서 이벤트가 발생하면 데이터 바인딩은 주어진 표현식을 평가한다. 일반적인 바인딩 표현식에서는 이런 평가가 진행되는 동안 null과 스레드 안전성이 확보된다.
앞의 예제에서 onClick(View)에 제공할 view 매개 변수를 정의하지 않았다. 리스너 바인딩은 두 가지 옵션을 제공한다.
- 모든 매개 변수를 무시하고 사용하지 않는 것
- 매개 변수의 이름을 정하고 바인딩 표현식에서 그것들을 사용하는 것
class Presenter {
fun onSaveClick(view: View, task: Task) { ... }
}
android:onClick="@{(view) -> presenter.onSaveClick(view, task)}"
만약 매개 변수가 2개 이상이면 다음과 같이 람다 표현식을 사용할 수 있다.
class Presenter {
fun onCompleteChanged(task: Task, completed: Boolean) { ... }
}
```
<CheckBox
...
android:onCheckedChanged="@{(cb, isChecked) -> presenter.onCompleteChanged(task, isChecked)}" />
```
만약 리스닝을 하고 있는 이벤트의 반환형이 void가 아니면 표현식에서도 반드시 같은 타입의 값을 반환해야 한다. 데이터 바인딩을 처음 사용할 때 가장 실수를 많이 하는 부분인데, long click 이벤트 리스너를 설정하고 싶다면 반드시 boolean을 반환해야 한다.
class Presenter {
fun onLongClick(view: View, task: Task) { ... }
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
바인딩 표현식에서 null 객체 때문에 평가될 수 없을 때, 데이터 바인딩은 해당 타입의 기본값을 반환한다. 예를 들어, int 타입은 0을 반환하고, boolean 타입은 false를 반환한다.
만약 void 타입을 반환하는 람다 표현식에서 삼항 연산자를 사용해야 하는 경우 void 키워드를 그대로 사용할 수 있다.
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
리스너 표현식은 매우 강력하고 코드의 가독성을 높여 준다. 다른 한편으로는 리스너 표현식을 복잡하게 할 수도 있는데, 이 경우 읽기가 어렵고 유지 보수가 힘들어진다. 리스너 표현식 사용 시 데이터를 전달하는 수준의 간단한 코드로 작성하는 것을 추천하며, 비즈니스 로직 같은 경우는 콜백 메서드 내부에서 구현하고 리스너 표현식에서 이를 호출해야 한다.
참고
- 아키텍처를 알아야 앱 개발이 보인다, 옥수환, 2020
'Android' 카테고리의 다른 글
| [Android] Jitpack으로 라이브러리 배포 - 3 (0) | 2024.08.05 |
|---|---|
| [Android] Jitpack으로 라이브러리 배포 - 2 (0) | 2024.08.05 |
| [Android] Jitpack으로 라이브러리 배포 - 1 (0) | 2024.08.05 |