Android アプリ開発の現場に DataBinding が導入されて久しく経ちます。
View と Controller, Model を結びつける方法は Data Binding 以前から Butter Knife や Android 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>
こういったコードのテストを行うためには 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 はボタンをクリックすると
- メッセージが更新される
- 描画が更新される
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 です。