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

DataBinding을 이용하면서 마음에 들었던 것중 하나는 Listener, Callback이다. 특히 람다식으로 Callback 등록하는 부분은 코드의 가독성까지 올려주기도 해서 개인적으론 참 좋아하며 자주 사용한다.
 하지만, 이와 관련해서 자세한 내용이 Developer Page에 나와있지 않기 때문에, 몇가지 팁들을 아래에 정리해본다. 작성의 편의를 위해 Callback, Listener를 모두 Callback이라고 작성한다.

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



1. Callback 등록 방법

 이건 Developer Page의 내용에도 나와있긴 하지만 한번 짚고 넘어가려한다. Callback을 등록하는 방법은 아래 두가지이다. android:onClick 부분을 비교해보자.

  - Setter Method 연결방식 : 직접 Callback 객체를 등록하는 방법
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.Handlers"/>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
           android:onClick="@{handlers::onClickFriend}"/>
   </LinearLayout>
</layout>

  - 람다를 이용한 방법
<?xml version="1.0" encoding="utf-8"?>
  <layout xmlns:android="http://schemas.android.com/apk/res/android">
      <data>
          <variable name="task" type="com.android.example.Task" />
          <variable name="presenter" type="com.android.example.Presenter" />
      </data>
      <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
          <Button android:layout_width="wrap_content" android:layout_height="wrap_content"
          android:onClick="@{() -> presenter.onSaveClick(task)}" />
      </LinearLayout>
  </layout>

 사실 맨 위에 있는 방식은 Callback 객체를 직접 setter에 넣어주는 방식이므로, 제일 간단하다. 그렇기 때문에 이번 글에서는 더이상 다루지 않는다.

 람다에 대해서 필자보다 잘 알고 있는 사람들도 많지만, 풀이를 하면 아래와 같다.
   () -> presenter.onSaveClick(task) 
  - "->" 를 중심으로 왼쪽은 기존의 Callback을 나타내고, 오른쪽은 새로 연결될 Callback의 Method를 나타낸다. 
  - 왼쪽이 기존의 Callback이라 한다면, 위의 예제에선 OnClickListener인 것인데, OnClickListener의 Method는 "void onClick(View view)"인 형태이다. 그렇다면 View는 어디로 갔는가? => 이것은 필요할 때 "()" 안쪽에 Type 없이 써주면 된다.
   ex ) (view) -> presenter.onSaveClick(view, task)


위의 내용을 바탕으로 실제 사용하는 기본적인 방법들을 아래에 나열해보면.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <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)}" />
        <Button ...
            android:onClick="@{(view) -> presenter.onSaveClick2(view)}" />
        <Button ...
            android:onClick="@{(v) -> presenter.onSaveClick3(v, task)}" />
    </LinearLayout>
</layout>

위의 내용에서 이해할 부분들은
 - 기본 OnClickListener에서 넘어오는 View instance가 필요 없으면 "()"
 - 필요하다면 Type을 제외한 나름의 변수명을 정의해서 사용

 - XML에 정의된 variable들도 Callback으로 넘겨줄 수 있음
   (사실 이뿐 아니라 같은 XML에 정의된 View들도, Resource등 다양하게 넘겨줄 수 있음.)




2. 기본 사용 예시와 생성된 Binding Code

여기서 우리는 이런 람다식이 실제로 Binding 시점에 어떻게 매핑되는지 확인할 필요가 있다. 어떻게 Binding Code가 생성될까?

 우선 우리가 사용할 Test용 Callback Method은 아래이다.
public class TestCallbacks {
    public void onClickTest1(User user){}
}

 그리고 XML은 아래와 같다.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="user"
            type="com.example.User" />
        <variable
            name="callback"
            type="com.example.TestCallbacks"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.firstName}" />

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{() -> callback.onClickTest1(user)}"/>
    </LinearLayout>
</layout>

이를 통해 생성된 Binding Code는 아래와 같다. (실제 코드가 불필요하게 복잡한 부분이 있어서 읽기 쉽게 약간 수정했다.)
// callback impls
public final void _internalCallbackOnClick(int sourceId , android.view.View callbackArg_0) {
    // localize variables for thread safety
    if (mCallback != null) {
        mCallback.onClickTest1(mUser);
    }
}

생성된 코드를 보면 세상간단하다. 혹자는 그럴수 있다.
"자. 난 다 이해했어. 아래쪽은 안봐도 되겠지?" 
천만의 말씀... 이제부터 시작이다.


3. DataBinding Callback 사용시 주의할 점

이제 이 람다식을 이용한 DataBinding Callback 등록을 사용할 때 주의할 점을 적어본다. 작성의 편의상 람다 Callback을 양쪽 두 파트로 나눠서 Original Callback과 New Callback으로 구분해 설명한다.

  1) Original Callback Interface는 반드시 1개의 Method만 있어야 한다.
     => Method가 두개 이상인 Callback Interface는 지원되지 않는다.
      ex) RecyclerView의 OnScrollListener
  2) 기본적으로 람다식을 이용한 Callback 등록은 "기본 Android Widget"만 가능하다.
     => Support Library의 View들도 지원되지 않는다.
  3) New Callback은 반드시 Original Callback의 return Type을 맞춰야 한다.
     ex) Long Click Listener의 Method는 Return type이 boolean
     => 그렇지 않으면 아래와 같이 예상할 수 없는 에러가 발생하게 된다.

Error:Execution failed for task ':app:compileDebugJavaWithJavac'.
> java.lang.RuntimeException: failure, see logs for details.
cannot generate view binders java.lang.StackOverflowError
   at android.databinding.tool.writer.LayoutBinderWriterKt$callbackLocalName$2.invoke(LayoutBinderWriter.kt)
   at android.databinding.tool.writer.LayoutBinderWriterKt$callbackLocalName$2.invoke(LayoutBinderWriter.kt)
   at android.databinding.tool.ext.LazyExt.getValue(ext.kt:27)
   at android.databinding.tool.writer.LayoutBinderWriterKt.getCallbackLocalName(LayoutBinderWriter.kt)
   at android.databinding.tool.writer.LayoutBinderWriterKt.scopedName(LayoutBinderWriter.kt:197)
   at android.databinding.tool.expr.Expr.toCode(Expr.java:776)
   ...

위의 내용에서 1)번과 3)번은 주의하면서 사용하면 되는 부분이다. 하지만 2)번의 경우는... 단순히 안된다! 라고 생각하고 넘어가기엔 좀 아쉽다.(사실 1번도 좀 아쉬운 부분이 있지만 아직 답을 찾지 못했다.) 그래서 이리저리 테스트를 해봤고... 방법을 알게되었다. 사실 아주 간단했다.


4. Custom View Class의 Listener를 람다식으로 Binding 하기

 사실 구구절절 글을 길게 쓸 필요도 없다. 너무나도 손쉽게 @BindingAdapter 하나만 정의해주면 되기 때문이다.
 구현상으로는 아래와 같이 하나의 Setter 역할을 해주는 BindingAdapter만 정의해주면 된다.

@BindingAdapter("onSeeking" )
public static void setSeekingListener(MediaControllerSeekBar view, MediaControllerSeekBar.OnSeekingListener listener) {
    view.setOnSeekingListener(listener);
}

그리고는 사용할 때 XML에 아래와 같이 등록만 해주면 된다.

app:onSeeking="@{(position, pos) -> viewModel.onSeeking(position, pos)}"

 위와같이만 작성해주면 DataBinding이 코드를 Binding해준다.
 사실 여기서 더 구구절절 설명을 할 필요가 없을 것 같긴 하다.


여기까지 Callback에 대한 처리를 알아봤다. 지난번 봤던 ViewStub에 비하면 참 간단한 듯도 하다.
그동안은 단방향 Binding에 대해서만 알아봤다면 다음 시간은 계획상 마지막인 InverseBinding에 대해서 알아보려고 한다.

댓글

  1. 이런 글을 써주셔서 감사합니다.

    답글삭제
  2. 감사합니다... 3번에 콜백때문에 고생하다가 구글링을 통해서 정보를 얻게되네요... 감사합니당..

    답글삭제

댓글 쓰기

이 블로그의 인기 게시물

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

[Android] Retrofit2, OkHttpClient Method 정리

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