ユースケースと罠

Data Binding Libraryの機能については概ね説明しました。ここからはData Binding Libraryのユースケースや利用時に遭遇する罠とその解決方法について解説していきます。

ユースケース : ViewHolderの代わりに使う

恐らくData Binding Libraryを導入する上で一番リスクが少ない方法はViewHolderの代わりに利用するというものでしょう。Bindingクラスは内部にレイアウトのViewを保持しバインド処理の際に利用しています。通常それらのViewはprivateですがリスト37のようにLayout XML上でidを付けているViewはpublicなフィールドとなり、外部から利用できます。

リスト2.37 idを付けたViewはpublicになる

<layout
      xmlns:android="http://schemas.android.com/apk/res/android">
  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView
      android:id="@+id/title"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"/>
    <TextView
      android:id="@+id/description"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"/>
  </LinearLayout>
</layout>

Layout XMLを書いたと同時にViewHolderと同等のものが生成されるわけですからかなりの負担減となります。リスト38はArrayAdapterでBindingクラスをViewHolderの代わりに使う例です。通常のViewHolderのパターンをほぼそのまま置き換えられます。

リスト2.38 BindingクラスをViewHolderの代わりに使う

public class ArticleAdapter extends ArrayAdapter<Article> {

  public ArticleAdapter(Context context) {
    super(context, -1);
  }

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
    ListitemArticleBinding binding = null;
    if (convertView == null) {
      binding = ListitemArticleBinding
              .inflate(LayoutInflater.from(getContext()));
      convertView = binding.getRoot();
      convertView.setTag(binding);
    } else {
      binding = (ListitemArticleBinding) convertView.getTag();
    }
    Article article = getItem(position);
    binding.title.setText(article.getTitle());
    binding.description.setText(article.getDescription());

    return convertView;
  }
}

ユースケース : プログレスとエラー表示を持つListView

よくあるUIとして、ListViewにAPIと通信した結果を表示する画面について考えてみます。Data Binding Libraryを用いて実装するとどのような設計になるでしょうか。

3つの状態を持つ画面

この画面では次の3つの状態を持つことが考えられます。

  • 通信を待つ間プログレスバーを表示する
  • 通信が失敗した場合エラーを表示する
  • 通信が成功した場合ListViewにデータを表示する

これらの状態に関係するデータをバインドすればよさそうですね。まずはバインドに利用するクラスを定義します。これらはViewが表示するデータと状態を管理するデータを持ちます(リスト39)。

リスト2.39 ArticleListViewModel.java

public class ArticleListViewModel {
  // 状態
  private boolean isLoading;

  // 状態
  private boolean isShowError;

  // データ
  private List<Article> articles;

  // ListAdapterを実装したクラス
  private ArticleAdapter adapter;
}

isLoadingは通信結果を表示しているかを表します。trueなら通信中ということになります。isShowErrorはエラーかどうかを表します。articlesは通信結果を表します。adapterはListViewにセットするListAdapterです。

ArticleListViewModelの構造に基づいてLayout XMLを記述しますリスト40。

リスト2.40 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:bind="http://schemas.android.com/apk/res-auto">

  <data>
    <import type="android.view.View"/>
    <variable name="model"
              type="com.sys1yagi.sample.viewmodels.ArticleListViewModel"/>
  </data>

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">
    <ProgressBar
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:visibility='@{model.loading ? View.VISIBLE : View.GONE}'
      />
    <TextView
      android:text="Network Error"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textSize="20sp"
      android:textStyle="bold"
      android:layout_gravity="center"
      android:visibility="@{!model.loading &amp;&amp; model.showError ?
        View.VISIBLE : View.GONE}"
      />
    <ListView
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:visibility="@{model.loading ? View.GONE : View.VISIBLE}"
      bind:adapter="@{model.adapter}"
      />
  </LinearLayout>
</layout>

ProgressBar、エラー表示用のTextView、ListViewは状態によって表示、非表示が変わるのでそれぞれのvisibility属性に状態に合わせた式を記述しています。またListViewにはbind:adapter属性を指定してArticleAdapterをバインドしています。

次にActivityを作成し、BindingクラスにArticleListViewModelをセットする部分やAPIと通信してArticleListViewModelにデータをセットする部分を記述しますリスト41。

リスト2.41 MainActivity.java

public class MainActivity extends AppCompatActivity {

  ActivityMainBinding binding;
  ArticleListViewModel viewModel;
  ArticleApiClient articleApiClient = new ArticleApiClient();

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    viewModel = new ArticleListViewModel(this);
    binding.setModel(viewModel);

    viewModel.setLoading(true);
    articleApiClient.get()
      .subscribeOn(Schedulers.io())
      .observeOn(AndroidSchedulers.mainThread())
      .subscribe(
        new Action1<List<Article>>() {
          @Override
          public void call(List<Article> articles) {
            viewModel.setArticles(articles);
          }
        },
        new Action1<Throwable>() {
            @Override
          public void call(Throwable throwable) {
            viewModel.setShowError(true);
          }
        }
      );
  }
}

ArticleApiClientはAPIと通信するクラスですが、今回の関心とは関係がないので詳細については省略します。通信処理の直前でviewModelに通信状態であることをセットしています。通信が成功した場合はviewModelにデータを渡し、エラーだった場合はviewModelにエラーの状態をセットしています。

さて、データと状態の定義、レイアウトとバインドの定義、通信処理結果をデータへセットする部分まで完了しました。これだけではまだ正しく動作しません。最後にBaseObservableクラスを使ってArticleListViewModelが更新されたときにバインド処理が走るようにしていきます。

まずArticleListViewModelの中で@Bindableアノテーションを付与するフィールドを検討します。状態遷移の関係は単純化すると次のように表せます。

loading <-> (success | error)

つまりloading状態(isLoadingがtrue)ならプログレスバーを表示し、そうでなければListViewかエラーのどちらかを表示するということです。loadingの状態が変化したときにバインド処理を行えばすべての状態を表示できそうです(リスト42)。

リスト2.42 isLoadingに@Bindableを付与する

public class ArticleListViewModel extends BaseObservable
  // 省略
  @Bindable
  public boolean isLoading() {
    return isLoading;
  }
  // 省略
}

次に通信が成功した場合を考えます。ArticleListViewModelクラスのsetArticles()メソッドでデータが渡されたとき、ArticleAdapterを更新して変更を通知すると良さそうです(リスト43)。

リスト2.43 ArticleAdapterを更新して変更を通知する

public void setArticles(List<Article> articles) {
  this.articles = articles;
  adapter.clear();
  adapter.addAll(this.articles);
  setAdapter(adapter);
}
public void setAdapter(ArticleAdapter adapter) {
  this.adapter = adapter;
  setLoading(false);
}
public void setLoading(boolean loading) {
  this.isLoading = loading;
  notifyPropertyChanged(BR.loading);
}

リスト43で注目する点はsetAdapter()メソッドの中でさらにsetLoading(false)を呼び出していることでしょう。これによりバインド処理が実行されListViewにadapterがセットされ、表示状態になります。setLoading(true)を呼び出すとListViewが非表示となり、プログレスバーが表示状態となります。次にエラーですがこちらはもう説明は不要でしょう(リスト44)。setAdapter()メソッドと同様にsetLoading(false)を呼び出しています。

リスト2.44 setShowError(boolean showError)

public void setShowError(boolean showError) {
  this.isShowError = showError;
  setLoading(false);
}

これでListViewにAPIと通信した結果を表示する画面の実装は完了です。どこにもViewを操作するコードが登場しませんでした。View操作にまつわる部分をBindingクラスに委譲することでスッキリとした記述が可能になったことを実感できたのではないでしょうか。また、Data Bindingを利用する為にデータと状態を洗い出してViewModelに落とし込むプロセスはPresentation Domain Separation原則PresentationDomainSeparationを意識するのに有効なのでは無いかと思います。

empty. 複雑にしすぎても良くないので、結果が空だった場合の状態は省きました
PresentationDomainSeparation. http://martinfowler.com/bliki/PresentationDomainSeparation.html

罠 : multi dex

multi dexを有効にしたアプリケーションでData Binding Libraryを利用すると問題がでる場合があります。具体的にはアプリケーションのメソッド数が上限に達してdexファイルが分割されるときmultidex、生成されたBindingクラスがセカンダリdexに含まれてしまった場合にクラスローダでverifyエラーが発生してアプリケーションが動作しなくなります。Bindingクラスがセカンダリdexに含まれることが原因なのでプライマリdexにBindingクラスを含めるように設定すれば解決します。build.gradleのdefaultConfigでmultiDexKeepProguardを設定し、ProguardファイルにData Bindingの設定を記述してください。

リスト2.45 build.gradle

android {
  defaultConfig {
    multiDexEnabled = true
    multiDexKeepProguard file('multi-dex-keep.txt')
  }
}

リスト2.46 multi-dex-keep.txt

-keep public class * extends android.databinding.ViewDataBinding {
 *;
}

これでBindingクラスがプライマリdexに常に含まれるようになり、multi dexを有効にしたアプリケーションでもData Bindingを利用できます。

multidex. https://developer.android.com/tools/building/multidex.html

罠 : testing

リスト47のようにテストコード上でBindingクラスに触ると、リスト48に示したエラーが発生します。

リスト2.47 テストを実行するとエラーが発生する

@Test
public void test() {
  ActivityMainBinding binding =
  ActivityMainBinding.inflate(LayoutInflater.from(
    InstrumentationRegistry.getTargetContext()
  ));
  assertThat(binding, is(notNullValue()));
}

リスト2.48 テストを実行するとエラーが発生する

java.lang.IllegalAccessError:
Class ref in pre-verified class resolved to unexpected implementation

このエラーを回避するには、テストを実行する端末のRuntimeをARTにしておく必要があります。Bindingクラスを使ったテストを書く場合はARTの環境を用意しましょう。Bindingクラスを使うとActivityやFragmentに依存せずにUIのテストが書けるようになります。バインドするデータに状態も持たせておくとさまざまな状態変化に伴うUIのテストを容易に行えるでしょう。また、Bindingクラスをmockすることでドメインロジックのテストを書くときにUIへの副作用を無視できるでしょう。