Butter Knife、今までありがとう
あるアプリのmaster branchに,Butter Knifeへの依存をなくすPull Requestをmergeした.
いままでButter Knifeが担っていた仕事はすべてData Bindingが受け持つことになる.Data Bindingは公式はbeta releaseと言っているものの,限りなく1.0に近いRCなんじゃないかという感じがしたため実戦に投入している.
実行時に全力で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).
「DataBindingでButterKnifeを置き換えることが出来る!」とはよく言われることではあるが,実際の事例をあまり見たことが無い気がしたのでここで紹介しておく.
Yet another Butter KnifeとしてのData Binding
View binding
before
(おそらく)みんながButter Knifeに一番求めている機能.findViewById(int id)
が不要になるというアレ.
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>
でくくってあげる.
<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インスタンスを返すので,それを保持しておけばいい.
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のインスタンスを保持してくれている.
String text = binding.footer.getText();
Non-Activity binding
before
Activity以外,例えばFragmentに対するView binding.
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.あとはよしなに….
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
の実装をサボることが出来る.
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参照).
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クラスのラッパとして働いてもらうといい.
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()
したみたいな動きをしてくれる.
@OnClick(R.id.submit) public void submit(View view) { // TODO submit data to server... }
after
レイアウトにActivityのインスタンス渡してあげて,Button
のandroid:onClick
属性にリスナメソッドを渡してあげる.
<layout>
<data>
<variable name="activity" type="info.izumin.android.databindingsample.SampleActivity" />
</data>
<LinearLayout>
<Button android:onClick="@{activity.onSampleButtonClick}">
</LinearLayout>
</layout>
ActivityのonCreate()
ではbindingインスタンスにActivityのインスタンスをセットしておけば,あとはいい感じにやってくれる.
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を作ってあげればいい.これで程よい感じの疎結合化・責務の分離が実現できる.
interface SampleActivityHandlers {
void onSampleButtonClick(View view);
}
Clean ArchitectureでいうところのControllerに該当するかな?
その他DataBindingの便利機能
Listener bindings
DataBindingは@BindingAdapter
や@BindingMethod
といったアノテーションを利用した変態annotation processingにより,OnClickListener
以外のイベントリスナも設定できる.
<Button android:onClick="@{handlers.onPrevButtonClick}" />
<Button android:onClick="@{handlers.onNextButtonClick}" />
<EditText android:onTextChanged="@{handlers.onTextChanged}" />
<ListView android:onScroll="@{handlers.onScroll}" />
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/以下のファイルを参照されたい.
Adapter binding
@BindingAdapter
及び@BindingMethod
悪用事例.
@BindingMethods({
@BindingMethod(type = SwipeRefreshLayout.class, attribute = "android:onRefresh", method = "setOnRefreshListener"),
@BindingMethod(type = RecyclerView.class, attribute = "android:adapter", method = "setAdapter")
})
public final class ViewBindingUtils {
}
public interface SampleActivityHandlers {
void onRefresh();
}
<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になるのでテストも楽?
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);
}
}
<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を取得して…みたいなのを全力でサボることが出来る.それの良し悪しは別として.
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には存在しない.
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は複数のViewをひとまとめにして扱える機能…らしい.この記事書いてるときにはじめて知った.
// こうして…
@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で遊ぼう.