Butter Knife、今までありがとう。 Data Binding、これからよろしく。

## Butter Knife、今までありがとう
あるアプリのmaster branchに，[**Butter Knife**][butterknife]への依存をなくすPull Requestをmergeした．

![image.png (12.9 kB)](https://img.esa.io/uploads/production/attachments/2063/2015/12/31/3140/2400b1e5-700f-43eb-b272-6d27167febb3.png)

いままでButter Knifeが担っていた仕事はすべて[**Data Binding**][databinding]が受け持つことになる．Data Bindingは公式はbeta releaseと言っているものの，[限りなく1.0に近いRCなんじゃないか](https://bintray.com/android/android-tools)という感じがしたため実戦に投入している．

実行時に全力でReflectionするButter Knifeと違い，Data BindingはAnnotation Processingで事前に色々やってくれる方式というのも嬉しい（c.f. Butter KnifeもAnnotation Processingする方式に切り替えるっぽい？ => [Split the compiler and runtime into separate artifacts. by serj-lotutovici · Pull Request #323 · JakeWharton/butterknife](https://github.com/JakeWharton/butterknife/pull/323)）．

「DataBindingでButterKnifeを置き換えることが出来る！」とはよく言われることではあるが，実際の事例をあまり見たことが無い気がしたのでここで紹介しておく．

## Yet another Butter KnifeとしてのData Binding
### View binding
#### before
（おそらく）みんながButter Knifeに一番求めている機能．`findViewById(int id)`が不要になるというアレ．

> ```java
> class ExampleActivity extends Activity {
>   @Bind(R.id.title) TextView title;
>   @Bind(R.id.subtitle) TextView subtitle;
>   @Bind(R.id.footer) TextView footer;
> 
>   @Override
>   public void onCreate(Bundle savedInstanceState) {
>     super.onCreate(savedInstanceState);
>     setContentView(R.layout.simple_activity);
>     ButterKnife.bind(this);
>   }
> }
> ```

ここはすべてDataBindingで置き換え可能になる．

#### after
レイアウト全体を`<layout></layout>`でくくってあげる．

```xml
<layout>
  <LinearLayout>
    <TextView android:id="@+id/title">
    <TextView android:id="@+id/subtitle">
    <TextView android:id="@+id/footer">
  </LinearLayout>
</layout>
```

すると，`activity_sample.xml`なら`ActivitySampleBinding`というクラスが生成される．`DataBindingUtils.setContentView(Activity activity, int id)`がBindingインスタンスを返すので，それを保持しておけばいい．

```java
class ExampleActivity extends Activity {
  private ActivitySampleBinding binding;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtils.setContentView(this, R.layout.simple_activity);
  }
}
```

このBindingインスタンスがidが設定された各Viewのインスタンスを保持してくれている．

```java
String text = binding.footer.getText();
```

### Non-Activity binding
#### before
Activity以外，例えばFragmentに対するView binding．

> ```java
> public class FancyFragment extends Fragment {
>   @Bind(R.id.button1) Button button1;
>   @Bind(R.id.button2) Button button2;
> 
>   @Override
>   public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
>     View view = inflater.inflate(R.layout.fancy_fragment, container, false);
>     ButterKnife.bind(this, view);
>     // TODO Use fields...
>     return view;
>   }
> }
> ```


#### after
生成されたbindingクラスに`bind(View view)`というstatic methodが生えているので，それを利用すればOK．あとはよしなに…．

```java
public class FancyFragment extends Fragment {
  private FragmentFancyBinding binding;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_fancy, container, false);
  }

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    binding = FragmentFancyBinding.bind(getView());
  }
}
```

### View binding (ViewHolder)
#### before
Butter Knifeを利用すれば`ListView`における`ViewHolder`の実装をサボることが出来る．

> ```java
> public class MyAdapter extends BaseAdapter {
>   @Override
>   public View getView(int position, View view, ViewGroup parent) {
>     ViewHolder holder;
>     if (view != null) {
>       holder = (ViewHolder) view.getTag();
>     } else {
>       view = inflater.inflate(R.layout.list_item_sample, parent, false);
>       holder = new ViewHolder(view);
>       view.setTag(holder);
>     }
> 
>     holder.name.setText("John Doe");
>     // etc...
> 
>     return view;
>   }
> 
>   static class ViewHolder {
>     @Bind(R.id.title) TextView name;
>     @Bind(R.id.job_title) TextView jobTitle;
> 
>     public ViewHolder(View view) {
>       ButterKnife.bind(this, view);
>     }
>   }
> }

#### after (ListView)
Data Bindingを利用すれば，Bindingクラスが`ViewHolder`と同等の働きをしてくれるので，そもそも`ViewHolder`が不要になる（`RecyclerView`についてはちょっと違う話になるので後述する）．
Data Bindingを利用しているので，「各Viewに値をsetしていく」なんてこともしなくていい（snippet内comment参照）．

 ```java
public class MyAdapter extends BaseAdapter {
  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
      ListItemSampleBinding binding;
      if (convertView == null) {
          binding = DataBindingUtil.inflate(inflater, R.layout.list_item_sample, parent, false);
          convertView = binding.getRoot();
          convertView.setTag(binding);
      } else {
          binding = (ListItemSampleBinding) convertView.getTag();
      }

      // たとえばUserのリストなら，項目ごとではなくUserのインスタンスをsetできる
      binding.setUser(getItem(position));
      // binding.name.setText("John Doe");

      return convertView;
  }
}
```

#### after (RecyclerView)
`RecyclerView`の場合，`RecyclerView.ViewHolder`が必須になる．このViewHolderに`BindingHolder`みたいな名前つけて，Bindingクラスのラッパとして働いてもらうといい．

```java
public class SampleRecyclerAdapter extends RecyclerView.Adapter<SampleRecyclerAdapter.BindingHolder> {

    @Override
    public RegisterableDeviceListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
      final View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sample, parent, false);
      return new BindingHolder(v);
    }

  @Override
  public void onBindViewHolder(BindingHolder holder, int position) {
    // BindingHolder#getBinding()がViewDataBindingを返すのでsetVariable()を呼んでいる
    // 専用のBinding（この場合だとListItemSampleBinding）を返すことが出来るなら普通にsetUser()でOK
    holder.getBinding().setVariable(BR.user, getItem(position));
  }

  static class BindingHolder extends RecyclerView.ViewHolder {
    private final ViewDataBinding binding;

    public BindingHolder(View itemView) {
      super(itemView);
      binding = DataBindingUtil.bind(itemView)
    }

    public ViewDataBinding getBinding() {
      return binding;
    }
  }
}
```

### Listener binding (onClick)
#### before
`@OnClick`や`@OnItemClick`などのアノテーションを付けてあげることで，勝手に`setOnClickListener()`したみたいな動きをしてくれる．

> ```java
> @OnClick(R.id.submit)
> public void submit(View view) {
>   // TODO submit data to server...
> }
> ```

#### after
レイアウトにActivityのインスタンス渡してあげて，`Button`の`android:onClick`属性にリスナメソッドを渡してあげる．

```xml
<layout>
  <data>
    <variable name="activity" type="info.izumin.android.databindingsample.SampleActivity" />
  </data>
  <LinearLayout>
    <Button android:onClick="@{activity.onSampleButtonClick}">
  </LinearLayout>
</layout>
```

Activityの`onCreate()`ではbindingインスタンスにActivityのインスタンスをセットしておけば，あとはいい感じにやってくれる．

```java
class SampleActivity extends Activity {
  private ActivitySampleBinding binding;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtils.setContentView(this, R.layout.simple_activity);
    binding.setActivity(this);
  }

  public void onSampleButtonClick(View view) {
    // do something...
  }
}
```

ただ，先の例だとActivityはLayoutを参照しており，LayoutはActivityを参照している…のように，完全な密結合になってしまう．これが嫌な場合，イベントハンドリングだけを担当するinterfaceを作ってあげればいい．これで程よい感じの疎結合化・責務の分離が実現できる．

```java
interface SampleActivityHandlers {
  void onSampleButtonClick(View view);
}
```

[Clean Architecture][clean]でいうところのControllerに該当するかな？

## その他DataBindingの便利機能
### Listener bindings
DataBindingは`@BindingAdapter`や`@BindingMethod`といったアノテーションを利用した~~変態~~annotation processingにより，`OnClickListener`以外のイベントリスナも設定できる．

```xml
<Button android:onClick="@{handlers.onPrevButtonClick}" />
<Button android:onClick="@{handlers.onNextButtonClick}" />
<EditText android:onTextChanged="@{handlers.onTextChanged}" />
<ListView android:onScroll="@{handlers.onScroll}" />
```

```java
interface SampleActivityHandlers {
  void onPrevButtonClick(View view);
  void onNextButtonClick(View view);
  void onTextChanged(CharSequence s, int start, int before, int count);
  void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount);
}
```

標準で用意されているAdapterについては[extensions/baseAdapters/.../data-binding/][adapters]以下のファイルを参照されたい．

### Adapter binding
`@BindingAdapter`及び`@BindingMethod`悪用事例．

```java
@BindingMethods({
    @BindingMethod(type = SwipeRefreshLayout.class, attribute = "android:onRefresh", method = "setOnRefreshListener"),
    @BindingMethod(type = RecyclerView.class, attribute = "android:adapter", method = "setAdapter")
})
public final class ViewBindingUtils {
}
```

```java
public interface SampleActivityHandlers {
  void onRefresh();
}
```

```xml
<layout>
  <data>
    <variable name="handlers"
      type="info.izumin.android.databindingsample.SampleActivityHandlers" />
    <variable name="adapter"
      type="android.support.v7.widget.RecyclerView.Adapter" />
  </data>
  <android.support.v4.widget.SwipeRefreshLayout
    android:onRefresh="@{handlers.onRefresh}" >

    <android.support.v7.widget.RecyclerView
      android:adapter="@{adapter}" />

  </android.support.v4.widget.SwipeRefreshLayout>
</layout>
```

ここまでやる必要性がどこにあるかはわからない．

### Image source binding
「何らかの値に合わせて表示する画像を切り替える」みたいなのも，BindingAdapterを利用すればサクッと書ける．static methodになるのでテストも楽？

```java
public final class ViewBindingUtils {
  @BindingAdapter("signalStrength")
  public static void setSignalStrengthIcon(ImageView imageView, BluetoothDevice device) {
    int resId = R.mipmap.ic_signal_weak;
    final int rssi = device.getRssi();
    if (rssi >= -40) {
      resId = R.mipmap.ic_signal_strong;
    } else if (rssi < -40 && rssi > -60){
      resId = R.mipmap.ic_signal_medium;
    }
    imageView.setImageResource(resId);
  }
}
```

```xml
<layout>
  <data>
    <variable name="device" type="android.bluetooth.BluetoothDevice" />
  </data>
  <LinearLayout>
    <ImageView app:signalStrength="@{device}" />
    <TextView android:text="@{device.getName()}" />
    <TextView android:text="@{device.getAddress()}" />
  </LinearLayout>
</layout>
```

`BindingAdapter`で増やす属性のnamespace，何も付けない（`app`）にするのと`android`にするのとどちらがいいんでしょうね．

### Data Binding on custom view
当然ながら，Data BindingはCustom Viewに対しても利用できる．`@BindingAdapter`を~~不~~適切に利用すれば，`attrs.xml`書いて`TypedArray`からcustom attrsを取得して…みたいなのを全力でサボることが出来る．~~それの良し悪しは別として．~~

```java
public class Pagination extends RelativeLayout {
  private ViewPaginationBinding binding;

  public Pagination(Context context) {
    this(context, null);
  }

  public Pagination(Context context, AttributeSet attrs) {
    super(context, attrs);
    binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.view_pagination, this, true);
  }

  public static void setListener(Pagination paginate, View target, OnPaginationClickListener listener) {
    if (listener != null) {
      target.setOnClickListener(_v -> listener.onClick(paginate));
    }
  }

  @BindingAdapter({"android:onPrevButtonClicked"})
  public static void setPrevClickListener(Pagination view, OnPaginationClickListener listener) {
    setListener(view, view.binding.btnPrevPage, listener);
  }

  @BindingAdapter({"android:onNextButtonClicked"})
  public static void setNextClickListener(Pagination view, OnPaginationClickListener listener) {
    setListener(view, view.binding.btnNextPage, listener);
  }

  public interface OnPaginationClickListener {
    void onClick(Pagination pagination);
  }
}
```

## Butter Knife -> Data Bindingで置き換え不可な機能
### Resource binding
これはDataBindingには存在しない．

> ```java
> class ExampleActivity extends Activity {
>   @BindString(R.string.title) String title;
>   @BindDrawable(R.drawable.graphic) Drawable graphic;
>   @BindColor(R.color.red) int red; // int or ColorStateList field
>   @BindDimen(R.dimen.spacer) Float spacer; // int (for pixel size) or float (for exact value) field
>   // ...
> }
> ```

### View lists
[View lists](http://jakewharton.github.io/butterknife#view-lists)は複数のViewをひとまとめにして扱える機能…らしい．~~この記事書いてるときにはじめて知った．~~

```java
// こうして…
@Bind({ R.id.first_name, R.id.middle_name, R.id.last_name })
List<EditText> nameViews;

// こういうのを用意しておいてあげると…
static final ButterKnife.Action<View> DISABLE = new ButterKnife.Action<View>() {
  @Override public void apply(View view, int index) {
    view.setEnabled(false);
  }
};

// こんな感じにまとめて適用できるよ！
ButterKnife.apply(nameViews, DISABLE);
```

これもData Bindingには存在しない．

## まとめ
* Butter Knifeの機能はだいたいData Bindingで代替可能
  - Resource binding，View listsのみData Bindingでは代替不可
* `ViewHolder`パターンはすべて置き換え可能
  - `RecyclerView.ViewHolder`はBindingのwrapperとして振る舞わせればOK
* `@BindingAdapter`と`@BindingMethod`を~~悪用~~利用すればわりとなんでもできる

みんなもData Bindingで遊ぼう．

## References
* [Butter Knife][butterknife]
* [Data Binding Guide | Android Developers][databinding]
* [[Android] – Data Bindingつかってみた – NET BIZ DIV. TECH BLOG](http://tech.recruit-mp.co.jp/mobile/android-data-binding/)
* [The Clean Architecture | 8th Light][clean]
* [extensions/baseAdapters/src/main/java/android/databinding/adapters - platform/frameworks/data-binding - Git at Google][adapters]

[databinding]: http://developer.android.com/tools/data-binding/guide.html
[butterknife]: http://jakewharton.github.io/butterknife/
[clean]: http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
[adapters]: https://android.googlesource.com/platform/frameworks/data-binding/+/android-6.0.0_r7/extensions/baseAdapters/src/main/java/android/databinding/adapters