なぜ我々はData Bindingを利用するのか、テストの観点から考えてみる

More than 1 year has passed since last update.

Android アプリ開発の現場に DataBinding が導入されて久しく経ちます。

View と Controller, Model を結びつける方法は Data Binding 以前から Butter KnifeAndroid Annotation の機能を利用してきました。 Activity で findViewById をやっていた頃が懐かしいですね。

Data Binding は非常に便利で variable タグによってレイアウト定義ファイルからデータへアクセスすることが可能になりますし、また Observable を利用すればデータとレイアウトの双方向の通信が可能になります。

Data Binding はコードの削減につながって便利ですが、それだけでしょうか?今回はテストを行うという観点から Data Binding を利用する利点を考えてみましょう。


結論から

ロジックと View を完全に分離できるためユニットテストを書きやすくなる。


簡単なアプリのテストをやってみる

例えばボタンを押すとメッセージを更新し、表示をするアプリを考えてみます。

public class MainActivity extends AppCompatActivity {

String message;
TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
message = "not clicked";
textView = (TextView) findViewById(R.id.text);
updateMessage();
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
message = "clicked";
updateMessage();
}
});
}

void updateMessage() {
textView.setText(message);
}
}

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="net.numa08.adventcalendar2016.MainActivity">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="text"
/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/text"
android:text="button"
tools:ignore="HardcodedText" />
</RelativeLayout>

cap.gif

こういったコードのテストを行うためには espresso を利用するのではないでしょうか。

@RunWith(AndroidJUnit4.class)

public class MainActivityTest {

@Rule
public final ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);

@Test
public void change_message() {
ViewInteraction textView = onView(withId(R.id.text));
textView.check(matches(withText("not clicked")));

ViewInteraction appCompatButton = onView(withId(R.id.button));
appCompatButton.perform(click());

textView.check(matches(withText("clicked")));
}

}

テストを書くことはできましたが、一体これは何をテストしているのでしょうか? MainActivity はボタンをクリックすると


  1. メッセージが更新される

  2. 描画が更新される

2つの動作を行います。そのため、テストの対象が曖昧になってしまいます。


データの更新と描画の更新を完全に分離する

このアプリのコードは ViewModel を利用することでデータの更新と描画の更新を完全に分離することができます。

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
final MainActivityViewModel viewModel = new MainActivityViewModel();
binding.setViewModel(viewModel);
}

}

public class MainActivityViewModel extends BaseObservable {

@Bindable
private String message;

public MainActivityViewModel() {
setMessage("not clicked");
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
notifyPropertyChanged(BR.message);
}

public void onClickButton(View view) {
setMessage("clicked");
}
}

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="net.numa08.adventcalendar2016.MainActivityViewModel"/>
</data>
<RelativeLayout
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="net.numa08.adventcalendar2016.MainActivity">

<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.message}"
tools:text="text" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/text"
android:text="button"
android:onClick="@{viewModel::onClickButton}"
tools:ignore="HardcodedText" />
</RelativeLayout>
</layout>

このように ViewModel とレイアウトで双方向の通信が可能なります。この ViewModel のテストは次のようになります。

public class MainActivityViewModelTest {

@Test
public void message_change_on_click() {
final MainActivityViewModel viewModel = new MainActivityViewModel();
assertThat(viewModel.getMessage(), is("not clicked"));
viewModel.onClickButton(null);
assertThat(viewModel.getMessage(), is("clicked"));
}

}

さきほどとくらべて大きな変化はないようにも思えますが、「クリックをするとメッセージが変わる」テストの部分が「特定のメソッドを呼ぶとメッセージが変わる」に変更されました。

DataBinding に与える ViewModel を定義することで UI に描画を変更する部分とデータの状態を変更する部分を切り離すことが可能になりました。

そのためテストでは UI のテストではなく ViewModel をテストすることで描画されるデータの内容をテストできるようになります。


まとめ

Data Binding が登場は findViewById からの解放だけではなく、ロジックの構造、アプリの設計を行う上で大きな変革となりました。コードの削減だけではなく、今まで以上にテストのしやすい設計を心がけていきたいですね。

サンプルリポジトリは numa08/Adventcalendar2016 です。