[Android] DataBinding의 동작방식 - 2. BindingAdapter의 기본 및 사용 시점

이번에는 DataBinding을 사용할 때 그리 멀지 않은 시점에 찾게되는 BindingAdapter에 대해서 정리를 해보려고 한다. (InverseBindingAdapter에 대해서는 다음에 다루기로 한다)


"[Android] DataBinding의 동작방식" 전체목록
   1. Setter Method와의 연결
   2. BindingAdapter의 기본 및 사용 시점
   3. BindingAdapter의 사용시 팁
   4. include Tag 혹은 ViewStub 사용시의 Binding
   5. Listener, Callback
   6. InverseBinding (InverseBindingAdapter) + Two way Binding


1. BindingAdapter의 기본

: BindingAdapter는 "현재 정의되지 않은 Binding Attribute를 정의하고, 그 내부 로직을 작성"할때 쓰인다.
 사실 Android 기본 UI들을 위한 대부분의 BindingAdapter는 이미 정의가 되어있다. 예를 들어서 TextView에 관련된 BindingAdapter는 TextViewBindingAdapter.java 클래스 안에 정의가 되어있다. 일부만 살펴보면 아래와 같다.
public class TextViewBindingAdapter {

    private static final String TAG = "TextViewBindingAdapters";
    public static final int INTEGER = 0x01;
    public static final int SIGNED = 0x03;
    public static final int DECIMAL = 0x05;

    @BindingAdapter("android:text")
    public static void setText(TextView view, CharSequence text) {
        final CharSequence oldText = view.getText();
        if (text == oldText || (text == null && oldText.length() == 0)) {
            return;
        }
        if (text instanceof Spanned) {
            if (text.equals(oldText)) {
                return; // No change in the spans, so don't set anything.
            }
        } else if (!haveContentsChanged(text, oldText)) {
            return; // No content changes, so don't set anything.
        }
        view.setText(text);
    }

    @InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
    public static String getTextString(TextView view) {
        return view.getText().toString();
    }

    @BindingAdapter({"android:autoText"})
    public static void setAutoText(TextView view, boolean autoText) {
        KeyListener listener = view.getKeyListener();

        TextKeyListener.Capitalize capitalize = TextKeyListener.Capitalize.NONE;

        int inputType = listener != null ? listener.getInputType() : 0;
        if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
            capitalize = TextKeyListener.Capitalize.CHARACTERS;
        } else if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
            capitalize = TextKeyListener.Capitalize.WORDS;
        } else if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
            capitalize = TextKeyListener.Capitalize.SENTENCES;
        }
        view.setKeyListener(TextKeyListener.getInstance(autoText, capitalize));
    }

하지만, 여기서 정의되지 않은 것들을 수행하고 싶은 경우가 있는데, 그때는 직접 BindingAdapter를 정의해주는 것이다.
여기서 의문이 드는 분들이 있을지도 모르겠다. "예를들면 어떤것들이 있을까?"

한가지 예를 들어보면 android:visibility가 있다.

- android:visibility
 원래 이 XML Attribute는 BindingAdapter가 정의 되어있지 않기 때문에, View Class에 있는 setVisibility()를 직접 호출하도록 되어있다. 그렇기 때문에 반드시 XML Attribute의 Value Type이 integer이어야 한다.
하지만 필자는, DataBinding으로 작업을 하면서 이 Visibility와 관련된 두가지 Needs가 생겼는데, 하나는 Boolean 값을 이용한 visibility 관리이고, 또 다른 하나는 View visibility 변화에 따른 Animation 처리이다.( 'Java 코드에서 해주면 되지 않나?' 이라는 생각은 이곳에선 하지 않는다. )

우선 boolean 값을 이용한 visibility 처리는 어떻게 하는지 보면 아래와 같다.
@BindingAdapter("android:visibility")
public static void setVisibility(View view, boolean visible) {
    view.setVisibility( visible ? View.VISIBLE : View.GONE);
}

이 문서를 보는 사람들은 이미 알고있어야 하지만, BindingAdapter로 정의되는 모든 Method는 최소 2개의 Parameter를 가져야 하고, 그중 첫번째 Parameter는 View를 상속받은 Class이어야 한다. 그리고 그 뒤에 따라오는 Parameter는 BindingAdapter에 정의한 XML Attribute와 1:1 매칭이 되어야 한다.
즉, android:visibility로 받는 값의 Type은 Method의 두번째 Parameter의 Type인 boolean으로 넘어오게 된다는 것이다.
 그렇다면 이 BindingAdapter를 사용해서 XML을 작성한다면 자동생성된 Binding Code에서는 어떻게 작성되는지 보면 아래와 같다.
if ((dirtyFlags & 0x3L) != 0) {
    // api target 1
    kr.sdusb.libs.TestBinding.setVisibility(this.mboundView1, userIsMan);
}

 앞서 작성된 setVisibility(View view, boolean visible) Method를 호출하는 것을 볼 수 있다. 여기서 확실하게 알아야 할 것은, 위의 호출방식을 보면 Class의 Method를 직접 호출하므로 Method는 static으로 정의되어야 한다는 점이다.
 이와같은 내용을 활용해서 다양한 BindingAdapter를 정의해서 사용할 수 있다.


2. BindingAdapter의 사용시점

 이 BindingAdapter를 어떻게 사용하는지 간단하게 알았다면, 그렇다면 언제 이것을 사용하면 좋은가? 에 대한 이야기를 해보려고 한다. 사실 BindingAdapter는 간단하게만 생각하면 Custom View Class를 직접 정의해서 사용하기는 조금 귀찮거나 버거운 일이고, 혹은 꽤 많은 View들에 사용해야 하는데, 그때마다 Custom View Class를 정의하기는 어려울 때 사용한다고 보면 될것 같다.
 BindingAdapter의 무궁무진한(?) 사용방법을 위와같이 심플하게 표현하려고 하는 시도자체도 무리한 일인것 같기도 하지만, 심플하게만 생각한다면 위의 말은 말이 될것 같기도 하다.

 무슨말인지 설명해보면... 위에 예시로 든 android:visibility를 들 수가 있다.
@BindingAdapter("android:visibility")
public static void setVisibility(View view, boolean visible) {
    view.setVisibility( visible ? View.VISIBLE : View.GONE);
} 

 이 BindingAdapter는 사실 View를 상속받은 Custom View Class의 Method를 새로 정의해준다면 어찌저찌 될수도 있다. (물론 XML에서만 되는것은 아니다. 만약 1편에서 설명한 Setter method와의 연결만을 생각한다면 가능하기도 하지만...)

한번 작성을 해보면 아래와 같은 코드를 작성할 수 있다.
public void setVisibility(boolean visibility) {
    super.setVisibility(visibility ? VISIBLE : GONE);
}
@Override
public void setVisibility(int visibility) {
    super.setVisibility(visibility);
}

그리고 만약 이것을 DataBinding의 Setter Method 연결방식을 사용한다면 아래와 같이 사용할 수도 있다.
<kr.sdusb.libs.TestView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="@{true}" />

 위와같이 사용한다면, 자동 생성된 Binding Code에서 TestView의 setVisibility(true)를 호출해주는 코드가 생성된다. 하지만 잘 생각해보면, 이와같은 사용을 하기 위해서는 매번 Custom View Class를 정의해 주어야 한다는 커다란 단점이 생긴다.
 여기서 알 수 있는 것은 "여러 View들에 공통적으로 쓰일 수 있는 것이라면, BindingAdapter로 정의해 사용하는 것이 편하다" 라는 점이다.
 위의 예시뿐 아니라 ImageView에서 Image를 Load하는 Binding을 만든다거나, View들의 visibility에 따른 애니메이션을 구현한다거나 하는 공통적인 부분들을 정의하는데 유용하다.

 하지만 사실 실제로 작업을 하다보면, 많은 View들에 공통적으로 쓰일 수 있는 BindingAdapter를 정의하는 것도, 정의할 상황을 만나는 것도 그리 많지 않다. 
 필자의 경우는 DataBinding을 이용한 MVVM 패턴을 적용해서 앱을 만들고 있다. MVVM에서 중요한 포인트 중 하나는 "ViewModel에서 View 관련 처리를 하지 않는다." 라는 부분이다. 즉, ViewModel에서는 로직에만 집중하고, View는 그 로직의 결과를 알아서(Observable 활용) 적용한다는 것이 중요한데, 그러다보니 View에서 어떤 작업을 처리해야 하는데, ViewModel에서는 처리할 수 없으니, 처리할 공간이 필요했다. 그때 BindingAdapter를 통해 처리한다. (이 경우는 너무 다양하기에 하나의 예를 드는 것이 어렵다.)

 MVVM 관점에서 보게 된다면 BindingAdapter와 CustomView는 그 기능이 겹치는 부분이 있지만, 필자의 느낌에는 아래와 같다.

  -  BindingAdapter < CustomView  -

 무슨말인고 하니... CustomView까지 만드는 것은 조금 더 많은 작업을 수행한다거나 조금 더 세부적인 View 작업이 필요한 경우. 그렇지 않은 가벼운 View 작업이 필요한 경우는 BindingAdapter로 생각할 수 있었다. (100% 맞는 말은 아니지만, 이것 역시 가볍게 본다면 그럴 수 있다는 것이다.)
 사실 그렇다고 할지라도, 최대한 공통화 될 수 있는 부분을 BindingAdapter로 만들어 주는것이 좋다.


위의 내용을 정리해서 BindingAdapter의 사용시점을 정리하면 아래와 같다.
 1) 여러 View들이 갖는 공통적인 작업을 정의할 때
    : View Visibility에 따른 처리, Image Loading 등
 2) View 작업을 해야하는데, Custom View까지 만들기는
    그 작업이 그렇게 크지 않을때.
 
위의 경우 정도로 생각하면 쉽다.
사실 BindingAdapter에 관련된 Listener 처리를 생각하면 2번항목이 조금더 크게 느껴질 수 있지만, 그것은 다른 포스트에서 설명하도록 한다.



오늘은 BindingAdapter의 기본과 언제 사용해야하는지에 대해서 알아봤다. 이 다음 포스트에서는 사용할 때의 여러가지 팁들에 대해서 정리해보려고 한다. 

댓글

이 블로그의 인기 게시물

[Android] DataBinding의 동작방식 - 4. include Tag 혹은 ViewStub 사용시의 Binding

[Android] Retrofit2, OkHttpClient Method 정리

[Android] Layout별 성능 비교[Measure 호출횟수 비교] (LinearLayout vs RelativeLayout vs ConstraintLayout)