[Android] DataBinding의 동작방식 - 3. BindingAdapter의 사용시 팁

이번에는 지난번에 이야기 했던 BindingAdapter의 사용시 팁들에 대해서 정리해보려고 한다.

"[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의 Overloading

  Overloading라 하면, Method 이름은 같은데 들어가는 Parameter들이 조금씩 다른 경우들인데, BindingAdapter도 비슷한 사용방법이 있다. 안타깝게도 Dev 페이지의 문서에는 설명이 되어있지 않아서 몇가지 예를 들어가며 설명을 해보려 한다.


@BindingAdapter("loadImage")
public static void loadImage(ImageView view, String path) {
    GlideApp.with(view.getContext())
            .load(path)
            .into(view);
}

위의 코드는 ImageView에 Image Path를 넣어서 load하도록 하는 BindingAdapter이다. 위의 BindingAdapter만 정의하면, XML 상에서 [
app:loadImage="@{imagePath}" ]와 같은 형태로 Image Load를 할 수 있다.
 하지만 개발을 진행하다보면, 꼭 String 형태의 path뿐 아니라 Uri라던가 혹은 integer형의 Resource ID로 Load를 하고 싶을 경우들이 생기기 마련이다. 그런 경우에는 어떻게 해야할까? 별도의 이름을 갖는 BindingAdapter를 정의해야할까?
 사실 이미 위에서 언급한 것과 같이 Overloading처럼 작성을 해주기만 하면 된다. 아래 코드를 보는 편이 훨씬 빠르게 이해가 될 것이다.

@BindingAdapter("loadImage")
public static void loadImage(ImageView view, String path) {
    GlideApp.with(view.getContext())
            .load(path)
            .into(view);
}
@BindingAdapter("loadImage")
public static void loadImage(ImageView view, Uri uri) {
    GlideApp.with(view.getContext())
            .load(uri)
            .into(view);
}
@BindingAdapter("loadImage")
public static void loadImage(ImageView view, int resId) {
    GlideApp.with(view.getContext())
            .load(resId)
            .into(view);
}

 더 이상의 부연설명이 없을 정도로 심플하다. 이렇게만 작성을 해주면, Binding Code 생성시에 자동으로 적절한 Method와 매핑된다.

아래와 같은 방식도 가능하다. 이건 참고용으로만 알아두자.

@BindingAdapter("android:visibility")
public static void setVisibility(View view, int visibility) {
    view.setVisibility( visibility );
}
@BindingAdapter("android:visibility")
public static void setVisibility(TextView view, int visibility) {
    view.setText(visibility +"");
}


2. BindingAdapter의 requireAll=false의 활용

  requireAll의 경우는 Default값이 true이다. 즉, BindingAdapter에 넣은 모든 Attribute의 값이 들어와야 해당 Method가 호출되는 방식이다. 하지만, 그렇지 않아야 하는 경우들도 분명히 존재한다. 물론, 위에서 봤던 Overloading처럼 구현하는 것도 가능하다. 마치 아래의 예시처럼..

@BindingAdapter(value = "android:visibility")
public static void setVisibility(View view, int visibility) {
    ...
}
@BindingAdapter({"android:visibility", "visibleColor"})
public static void setVisibility(TextView view, int visibility, int visibleColor) {
    ...
}
@BindingAdapter({"android:visibility", "visibleColor", "goneColor"})
public static void setVisibility(TextView view, int visibility, int visibleColor, int goneColor) {
    ...
}

하지만, 그 Attribute가 많아질 수록, Attribute간의 여러가지 조합이 가능한 케이스가 된다면 위의 방식대로 구현하는 것은 엄청난 노동이 된다.

 필자의 경우도, View의 Visible/Gone Animation을 구현하기 위해 얼마전까지 위의 방식대로 구현을 했었다. 처음엔 Visibility / visibleAnimType/ goneAnimType 이렇게 3개의 Attribute만 받아서 처리하였기 때문에 그 구현이 어렵지 않았다. 이 상황에서 Attribute들이 조합되는 경우의 수도 2가지 정도밖에 되지 않았기 때문이다. 하지만 거기에 Animation Duration, Animation Offset, Visibility를 Boolean으로 처리 등이 추가되면서 이 BindingAdapter의 복잡도는 엄청 높아졌었다.

 이렇게 복잡한 상황에 몇가지 Attribute만 있어도 Binding이 될 수 있는 단 하나의 BindingAdapter Method를 만드는 것이 필요했고, 이 때 requireAll=false를 이용할 수 있다.


@BindingAdapter(value = {"android:visibility", "visibleAnimType", "goneAnimType "}, requireAll = false)
public static void setVisibility(View view, int visibility, int visibleAnimType, int goneAnimType ) {
}

 위와 같이 작성해두고 처음엔 아주 쉽게 생각했다. 하지만 문제는 바로 발생했다. 위에서 보면 "android:visibility"는 기본적으로 android에서 많이 쓰이는 View들의 visibility 처리용 Attribute이다. 하지만 여기서 requireAll=false로 해두고 해당 Attribute에 대해 BindingAdapter를 걸었으니, 애니메이션이 필요 없는 View들도 모두 이 BindingAdapter Method에 Binding 되는 것이다.
 그리고 내부에서 처리되는 로직을 강제로 타다보니 Crash속출...

 즉, 반.드.시. 예외케이스들에 대한 처리를 해줘야 한다.
 그리고 그 예외케이스 핸들링에 대한 팁을 하나 소개할까 한다.
 팁은 간단하다. "만약, BindingAdapter에 정의된 Attribute들을 사용하지 않았을 때 넘어오는 값은?" 만 알게되면 추후 예외처리등이 간단해진다.
 우선 확실하게 알고 넘어가기 위해 BindingAdapter.java의 requireAll에 달린 주석을 확인하고 넘어가면 아래와 같다.

Whether every attribute must be assigned a binding expression or if some can be absent. When this is false, the BindingAdapter will be called when at least one associated attribute has a binding expression. The attributes for which there was no binding expression (even a normal XML value) will cause the associated parameter receive the Java default value. Care must be taken to ensure that a default value is not confused with a valid XML value.

즉, Java의 기본값을 넘겨준다는 것인데, Object 형태라면 null, primitive type이라면 0을 넘겨준다. 이 부분이 중요한 것인데, requireAll=false를 사용하려면 정상인 경우에 넘어오는 값들 중에, 위와같은 값이 넘어와서는 안되는 것이다. 위와같은 null이나 0을 정상적으로 넘겨주는 경우가 있다면 값이 넘어온 경우인지, 값을 설정하지 않은 경우인지 판단이 어려운건 당연한 이야기 이겠다.

사실 위와같이 코딩하면, Handling 해야하는 예외케이스들이 꽤나 많이 생길수도 있다. 하지만 Overloading 하는 식으로 수 많은 Method를 만드는 것 보다는 유지보수 비용이 적게 들 수 있다.
 참고용으로 아래 예시는 Image Loading을 requireAll=false로 구현한 예제이다.

@BindingAdapter(value = {"loadImage", "emptyImage", "orientation", "imageMaxWidth", "imageMaxHeight", "imageMemoryCache", "imageFadeDuration"}, requireAll = false)
public static void loadImage(@NonNull ImageView view, String path, Drawable emptyImage, int orientation, float width, float height, boolean useMemoryCache, int imageFadeDuration) {
    GlideRequest<Drawable> request = GlideApp.with(view.getContext()).asDrawable();
    if(emptyImage != null) {
        request.load(!TextUtils.isEmpty(path) ? path : emptyImage)
                .error(emptyImage);
    } else if( TextUtils.isEmpty(path) ) {
        return;
    } else {
        request.load(path);
    }

    if(width > 0) {
        request.override((int)width, (int)height)
                .fitCenter();
    }

    if(orientation != 0) {
        request.transform(new RotateTransformation(view.getContext(), orientation));
    }

    if(imageFadeDuration > 0) {
        request.transition(DrawableTransitionOptions.withCrossFade(imageFadeDuration));
    }

    request.skipMemoryCache( !useMemoryCache );
    request.into(view);
}

현재까지는 BindingAdapter의 기본적인 사용 팁을 알아봤는데,
이 다음부터는 조금 심화과정인 <include>와 ViewStub에 대해서 알아보려고 한다. 재미있는건 Google이 의도를 한것인지, 만들어 놓고 모르는 것인지는 모르겠지만 ViewStub에 대한 내용이 정말 빈약하게 문서화 되어있고, 심지어 ViewStub을 Binding하기 위해서는 InflateListener를 이용하라고 되어있다. 하지만, 정말 이렇게 해야한다면 엄청난 제약이 아닐 수 없다.
 그래서 다음 포스트에서는 Android Binding의 크게 두가지 허들인 Adapter 패턴과 ViewStub중 ViewStub에 대해 자세하게 알아보겠다.

댓글

댓글 쓰기

이 블로그의 인기 게시물

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

[Android] DataBinding의 동작방식 - 5. Listener, Callback (CustomView의 Callback을 람다식으로 Binding하기)

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