바인딩 어댑터

결합 어댑터는 적절한 프레임워크를 호출하여 값을 설정해야 합니다. 한 가지 예는 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)addDrawerListener(DrawerListener) 메서드를 각각 app:scrimColorapp:drawerListener 속성의 setter로 사용합니다.

<androidx.drawerlayout.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")])

Java

@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())
}

Java

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

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

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

다음 예와 같이 여러 속성을 수신하는 어댑터도 있을 수 있습니다.

Kotlin

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

Java

@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}" />

imageUrlerrorImageView 객체에 사용되고 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);
    }
}

Java

@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(newPadding,
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    view.getPaddingBottom())
    }
}

Java

@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)의 두 가지 메서드가 있습니다. 라이브러리는 인터페이스 속성과 핸들러를 구별하기 위해 두 개의 인터페이스를 제공합니다.

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)
}

Java

@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)
        }
    }
}

Java

@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 클래스는 결합 어댑터에서 리스너를 삭제할 수 있도록 이러한 리스너를 추적하는 데 도움이 됩니다.

객체 변환

자동 객체 변환

결합 표현식에서 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)

Java

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

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

// The @drawable and @color represent different value types in the same
// expression, which causes a build error.
<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

추가 리소스

데이터 결합에 관한 자세한 내용은 다음 리소스를 참고하세요.

샘플

Codelab

블로그 게시물