결합 어댑터

결합 어댑터는 적절한 프레임워크를 호출하여 값을 설정하는 작업을 담당합니다. 한 가지 예로 setText() 메서드를 호출하는 것과 같이 속성 값을 설정하는 작업을 들 수 있습니다. 또 다른 예로는 setOnClickListener() 메서드를 호출하는 것과 같이 이벤트 리스너를 설정하는 작업이 있습니다.

데이터 결합 라이브러리를 사용하면 값을 설정하기 위해 호출되는 메서드를 지정하고 고유한 결합 로직을 제공하며 어댑터를 사용함으로써 반환된 객체의 유형을 지정할 수 있습니다.

속성 값 설정

결합된 값이 변경될 때마다 생성된 결합 클래스는 결합 표현식을 사용하여 뷰에서 setter 메서드를 호출해야 합니다. 데이터 결합 라이브러리에서 메서드를 자동으로 결정하거나 메서드를 명시적으로 선언하거나 맞춤 로직을 제공해 메서드를 선택하도록 허용할 수 있습니다.

자동 메서드 선택

이름이 example인 속성의 경우 라이브러리는 호환 가능한 유형을 인수로 허용하는 setExample(arg) 메서드를 자동으로 찾으려고 합니다. 속성의 네임스페이스는 고려되지 않으며 메서드 검색 시 속성 이름 및 유형만 사용됩니다.

예를 들어 android:text="@{user.name}" 표현식이 있다고 한다면 라이브러리는 user.getName()에서 반환한 유형을 허용하는 setText(arg) 메서드를 찾습니다. user.getName()의 반환 유형이 String이면 라이브러리는 String 인수를 허용하는 setText() 메서드를 찾습니다. 표현식이 int를 대신 반환하면 라이브러리는 int 인수를 허용하는 setText() 메서드를 검색합니다. 표현식은 올바른 유형을 반환해야 합니다. 필요하다면 반환 값을 변환할 수 있습니다.

지정된 이름의 속성이 없더라도 데이터 결합은 작동합니다. 그때는 데이터 결합을 사용하여 setter에 필요한 속성을 생성할 수 있습니다. 예를 들어 지원 클래스 DrawerLayout에는 어떤 속성도 없지만 많은 setter가 있습니다. 다음 레이아웃은 자동으로 setScrimColor(int)setDrawerListener(DrawerListener) 메서드를 각각 app:scrimColorapp:drawerListener 속성의 setter로 사용합니다.

<android.support.v4.widget.DrawerLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:scrimColor="@{@color/scrim}"
        app:drawerListener="@{fragment.drawerListener}">
    

맞춤 메서드 이름 지정

일부 속성에는 이름이 일치하지 않는 setter가 있습니다. 이러한 상황에서 속성은 BindingMethods 주석을 사용하여 setter와 연결될 수 있습니다. 주석은 클래스와 함께 사용되며 이름이 바뀐 각 메서드에 하나씩 여러 BindingMethod 주석을 포함할 수 있습니다. 결합 메서드는 앱의 어떤 클래스에도 추가할 수 있는 주석입니다. 다음 예에서 android:tint 속성은 setTint() 메서드가 아닌 setImageTintList(ColorStateList) 메서드와 연결됩니다.

Kotlin

    @BindingMethods(value = [
        BindingMethod(
            type = android.widget.ImageView::class,
            attribute = "android:tint",
            method = "setImageTintList")])

    

자바

    @BindingMethods({
           @BindingMethod(type = "android.widget.ImageView",
                          attribute = "android:tint",
                          method = "setImageTintList"),
    })

    

일반적으로 Android 프레임워크 클래스에서 setter의 이름을 바꿀 필요가 없습니다. 이름 규칙을 사용하여 일치하는 메서드를 자동으로 찾는 속성이 이미 구현되었습니다.

맞춤 로직 제공

일부 속성에는 맞춤 결합 로직이 필요합니다. 예를 들어 android:paddingLeft 속성에는 연결된 setter가 없습니다. 대신 setPadding(left, top, right, bottom) 메서드가 제공됩니다. BindingAdapter 주석이 있는 정적 결합 어댑터 메서드를 사용하면 속성의 setter가 호출되는 방식을 맞춤설정할 수 있습니다.

Android 프레임워크 클래스의 속성에는 BindingAdapter 주석이 이미 생성되어 있습니다. 예를 들어 다음 예는 paddingLeft 속성의 결합 어댑터를 보여줍니다.

Kotlin

    @BindingAdapter("android:paddingLeft")
    fun setPaddingLeft(view: View, padding: Int) {
        view.setPadding(padding,
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    view.getPaddingBottom())
    }

    

자바

    @BindingAdapter("android:paddingLeft")
    public static void setPaddingLeft(View view, int padding) {
      view.setPadding(padding,
                      view.getPaddingTop(),
                      view.getPaddingRight(),
                      view.getPaddingBottom());
    }

    

매개변수 유형은 중요합니다. 첫 번째 매개변수는 속성과 연결된 뷰의 유형을 결정합니다. 두 번째 매개변수는 지정된 속성의 결합 표현식에서 허용되는 유형을 결정합니다.

결합 어댑터는 다른 유형의 맞춤설정에 유용합니다. 예를 들어 맞춤 로더는 작업자 스레드에서 호출되어 이미지를 로드할 수 있습니다.

개발자가 정의하는 결합 어댑터는 충돌이 발생하면 Android 프레임워크에서 제공하는 기본 어댑터보다 우선 적용됩니다.

또한 다음 예에서와 같이 여러 속성을 받는 어댑터도 있을 수 있습니다.

Kotlin

    @BindingAdapter("imageUrl", "error")
    fun loadImage(view: ImageView, url: String, error: Drawable) {
        Picasso.get().load(url).error(error).into(view)
    }

    

자바

    @BindingAdapter({"imageUrl", "error"})
    public static void loadImage(ImageView view, String url, Drawable error) {
      Picasso.get().load(url).error(error).into(view);
    }

    

다음 예에서와 같이 레이아웃에서 어댑터를 사용할 수 있습니다. 여기서 @drawable/venueError는 앱의 리소스를 나타냅니다. 리소스를 @{}로 묶으면 유효한 결합 표현식이 됩니다.

<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />
    

imageUrlerror가 모두 ImageView 객체에 사용되는데 imageUrl은 문자열이고 errorDrawable이라면 어댑터가 호출됩니다. 어떤 속성이라도 설정될 때 어댑터를 호출하려면 다음 예에서와 같이 어댑터의 requireAll 플래그(선택사항)를 false로 설정하면 됩니다.

Kotlin

    @BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
    fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
        if (url == null) {
            imageView.setImageDrawable(placeholder);
        } else {
            MyImageLoader.loadInto(imageView, url, placeholder);
        }
    }

    

자바

    @BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
    public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
      if (url == null) {
        imageView.setImageDrawable(placeholder);
      } else {
        MyImageLoader.loadInto(imageView, url, placeholder);
      }
    }

    

결합 어댑터 메서드는 선택적으로 핸들러의 이전 값을 사용할 수 있습니다. 이전 값과 새 값을 사용하는 메서드는 아래 예에서와 같이 속성의 모든 이전 값을 먼저 선언한 후 새 값을 선언해야 합니다.

Kotlin

    @BindingAdapter("android:paddingLeft")
    fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
        if (oldPadding != newPadding) {
            view.setPadding(padding,
                        view.getPaddingTop(),
                        view.getPaddingRight(),
                        view.getPaddingBottom())
        }
    }

    

자바

    @BindingAdapter("android:paddingLeft")
    public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
      if (oldPadding != newPadding) {
          view.setPadding(newPadding,
                          view.getPaddingTop(),
                          view.getPaddingRight(),
                          view.getPaddingBottom());
       }
    }

    

이벤트 핸들러는 다음 예에서와 같이 하나의 추상 메서드가 있는 인터페이스 또는 추상 클래스에서만 사용할 수 있습니다.

Kotlin

    @BindingAdapter("android:onLayoutChange")
    fun setOnLayoutChangeListener(
            view: View,
            oldValue: View.OnLayoutChangeListener?,
            newValue: View.OnLayoutChangeListener?
    ) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            if (oldValue != null) {
                view.removeOnLayoutChangeListener(oldValue)
            }
            if (newValue != null) {
                view.addOnLayoutChangeListener(newValue)
            }
        }
    }
    

자바

    @BindingAdapter("android:onLayoutChange")
    public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
           View.OnLayoutChangeListener newValue) {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
          view.removeOnLayoutChangeListener(oldValue);
        }
        if (newValue != null) {
          view.addOnLayoutChangeListener(newValue);
        }
      }
    }

    

다음과 같이 레이아웃에서 이 이벤트 핸들러를 사용할 수 있습니다.

<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>
    

리스너에 여러 메서드가 있으면 여러 리스너로 분할해야 합니다. 예를 들어 View.OnAttachStateChangeListener에는 onViewAttachedToWindow(View)onViewDetachedFromWindow(View)의 2개 메서드가 있습니다. 라이브러리는 2개의 인터페이스를 제공하여 이러한 메서드의 속성 및 핸들러를 구별합니다.

Kotlin

    // Translation from provided interfaces in Java:
    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
    interface OnViewDetachedFromWindow {
        fun onViewDetachedFromWindow(v: View)
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
    interface OnViewAttachedToWindow {
        fun onViewAttachedToWindow(v: View)
    }
    

자바

    @TargetApi(VERSION_CODES.HONEYCOMB_MR1)
    public interface OnViewDetachedFromWindow {
      void onViewDetachedFromWindow(View v);
    }

    @TargetApi(VERSION_CODES.HONEYCOMB_MR1)
    public interface OnViewAttachedToWindow {
      void onViewAttachedToWindow(View v);
    }
    

하나의 리스너를 변경하면 다른 리스너에도 영향을 줄 수 있으므로 어느 한 속성 또는 둘 다에서 작동하는 어댑터가 필요합니다. 다음 예에서와 같이 주석에서 requireAllfalse로 설정하여 모든 속성에 결합 표현식을 할당할 필요는 없다는 것을 지정할 수 있습니다.

Kotlin

    @BindingAdapter(
            "android:onViewDetachedFromWindow",
            "android:onViewAttachedToWindow",
            requireAll = false
    )
    fun setListener(view: View, detach: OnViewDetachedFromWindow?, attach: OnViewAttachedToWindow?) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
            val newListener: View.OnAttachStateChangeListener?
            newListener = if (detach == null && attach == null) {
                null
            } else {
                object : View.OnAttachStateChangeListener {
                    override fun onViewAttachedToWindow(v: View) {
                        attach.onViewAttachedToWindow(v)
                    }

                    override fun onViewDetachedFromWindow(v: View) {
                        detach.onViewDetachedFromWindow(v)
                    }
                }
            }

            val oldListener: View.OnAttachStateChangeListener? =
                    ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener)
            if (oldListener != null) {
                view.removeOnAttachStateChangeListener(oldListener)
            }
            if (newListener != null) {
                view.addOnAttachStateChangeListener(newListener)
            }
        }
    }
    

자바

    @BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
    public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
        if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
            OnAttachStateChangeListener newListener;
            if (detach == null && attach == null) {
                newListener = null;
            } else {
                newListener = new OnAttachStateChangeListener() {
                    @Override
                    public void onViewAttachedToWindow(View v) {
                        if (attach != null) {
                            attach.onViewAttachedToWindow(v);
                        }
                    }
                    @Override
                    public void onViewDetachedFromWindow(View v) {
                        if (detach != null) {
                            detach.onViewDetachedFromWindow(v);
                        }
                    }
                };
            }

            OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener,
                    R.id.onAttachStateChangeListener);
            if (oldListener != null) {
                view.removeOnAttachStateChangeListener(oldListener);
            }
            if (newListener != null) {
                view.addOnAttachStateChangeListener(newListener);
            }
        }
    }

    

위의 예는 View 클래스가 OnAttachStateChangeListener에 setter 메서드 대신 addOnAttachStateChangeListener()removeOnAttachStateChangeListener() 메서드를 사용하기 때문에 일반적인 예보다 약간 더 복잡합니다. android.databinding.adapters.ListenerUtil 클래스를 통해 이전 리스너를 계속 추적할 수 있습니다. 따라서 결합 어댑터에서 이전 리스너를 삭제할 수 있습니다.

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)OnViewDetachedFromWindowOnViewAttachedToWindow 인터페이스에 주석을 지정하면 데이터 결합 코드 생성기는 리스너가 addOnAttachStateChangeListener() 메서드에서 지원되는 것과 동일한 버전인 Android 3.1(API 레벨 12) 이상에서 실행될 때만 생성되어야 함을 인식하게 됩니다.

객체 변환

자동 객체 변환

Object가 결합 표현식에서 반환되면 라이브러리는 속성 값을 설정하는 데 사용되는 메서드를 선택합니다. Object는 선택된 메서드의 매개변수 유형으로 변환됩니다. 이 동작은 다음 예에서와 같이 ObservableMap 클래스를 사용하여 데이터를 저장하는 앱에서 유용합니다.

<TextView
       android:text='@{userMap["lastName"]}'
       android:layout_width="wrap_content"
       android:layout_height="wrap_content" />
    

표현식의 userMap 객체는 값을 반환하며 이 값은 android:text 속성 값을 설정하는 데 사용되는 setText(CharSequence) 메서드에서 볼 수 있는 매개변수 유형으로 자동으로 변환됩니다. 매개변수 유형이 불명확하면 표현식에서 반환 유형을 변환해야 합니다.

맞춤 변환

어떤 상황에서는 특정 유형 간에 맞춤 변환이 필요합니다. 뷰의 android:background 속성에 Drawable이 필요한데 지정된 color 값이 정수인 상황을 예로 들 수 있습니다. 다음 예는 Drawable이 필요한데 정수가 대신 지정된 속성을 보여줍니다.

<View
       android:background="@{isError ? @color/red : @color/white}"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
    

Drawable이 필요한데 정수가 반환될 때마다 intColorDrawable로 변환되어야 합니다. 다음과 같이 BindingConversion 주석이 있는 정적 메서드를 사용하여 변환을 실행할 수 있습니다.

Kotlin

    @BindingConversion
    fun convertColorToDrawable(color: Int) = ColorDrawable(color)
    

자바

    @BindingConversion
    public static ColorDrawable convertColorToDrawable(int color) {
        return new ColorDrawable(color);
    }

    

그러나 결합 표현식에 지정하는 값 유형은 일관되어야 합니다. 다음 예에서와 같이 동일한 표현식에 서로 다른 유형을 사용할 수 없습니다.

<View
       android:background="@{isError ? @drawable/error : @color/white}"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
    

추가 리소스

데이터 결합에 관해 자세히 알아보려면 다음 추가 리소스를 참조하세요.

샘플

Codelab

블로그 게시물