Skip to content

双方向Bindingが可能なAndroid Binding v0.45を使ってみた

  • 前置き
    Android Bindingが0.45にバージョンアップしていたので試してみたところ、何と双方向Bindingに対応しているではないですか!…とTwitterにつぶやいたら@nakajiに「blogに書け!」と言われたので、纏めてみようと思います。
  • はじめに
    Android Bindingって何?ですが、一言で言うと「ViewとModel層を祖結合にしてくれるライブラリ」ってところです。これを使うとどう素敵になるの?という前置きは、@nakajiコチラに書かれていますのでご参照ください。@nakaji記事にもある通り、昔はView–>Modelへの片方向Bindingのみがサポートされていましたが、v0.45ではView<–>Modelの双方向Bindingが実現出来ていました。スバ、ラッシィ。
  • 事前準備
    • Androidの開発環境(eclipse+Android SDK)を整えておく
    • Android Bindingのサイトから android-binding-0.45-update.jar をダウンロードしておく
  • 開発手順
    1. Androidプロジェクトを作成します。AndroidBindingSampleと名付けました。
      プロジェクトの作成
    2. 事前にダウンロードしておいた android-binding-0.45-update.jar をプロジェクトの外部ライブラリとして参照追加します。プロジェクトの「Property」ダイアログを開き「Java Build Path」を選択し「Add External JARs…」ボタンを押下します。
      外部JARの追加
      android-binding-0.45-update.jar を選択します。
      外部JARの選択
      プロジェクトに参照が追加されました。
      外部JARの追加完了
    3. 次に画面を作り…たいのですが、その準備として、画面に表示する文字情報をリソース(string.xml)に定義します。
      <!--?xml version="1.0" encoding="utf-8"?-->
       
          Android Binding
          Enter your Height
          Enter your Weight
          Calc BMI
    4. 画面を作りましょう。…とその前に何のアプリケーションを作るかを決めていませんでした。@nakaji記事と比較しやすいように、入力された身長と体重からBMI値を計算し表示するアプリケーションにします。画面に身長(EditText)、体重(EditText)、計算ボタン(Button)、BMI計算結果(TextView)を貼り付けます。貼り付けついでに、EditText:hintプロパティとButton:textプロパティに、string.xmlで定義した文字列を設定しておきます。
      画面レイアウト
      layout.xmlの内容はこんな感じです。

      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:binding="http://www.gueei.com/android-binding/"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent"
          android:orientation="vertical" >
       
          <EditText
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:inputType="number"
              android:hint="@string/hint_height">
              <requestFocus />
          </EditText>
       
          <EditText
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:inputType="number"
              android:hint="@string/hint_weight"/>
       
          <Button
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="@string/caption_calcbmi"/>
       
          <TextView
              android:id="@+id/textView1"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"/>
       
      </LinearLayout>

      通常は各ウィジットにandroid:id=”@+id/hoge”という感じでIDを振りコードからfindViewById(”hoge”)という感じで参照します。Android BindingではBindingによる参照を行うため、潔く取っ払いました。

    5. 次に、画面の入力を受け取り結果を出力するModelクラスを追加します。(先ほどのlayout.xmlと関連のある)Activityと同じ階層にクラスを追加します。
      クラス名をBmiModelにしました。継承元クラスはデフォルトのjava.lang.Objectです。
      BmiModelクラスの追加
    6. Modelを実装します。
      package jp.tworks.android.bmi;
       
      import android.view.View;
      import gueei.binding.Command;
      import gueei.binding.observables.StringObservable;
       
      public class BmiModel {
       
      	// for EditText TextProperty
      	public StringObservable Height = new StringObservable();
      	public StringObservable Weight = new StringObservable();
      	public StringObservable Bmi = new StringObservable(); 
       
      	// for Button on Click
      	public Command calcBmi = new Command() {
       
      		public void Invoke(View view, Object... args) {
      			int height = Integer.valueOf(Height.get());
      			int weight = Integer.valueOf(Weight.get());
      			int bmi = (int)(weight / Math.pow(height / 100.0, 2));
      			Bmi.set(String.valueOf(bmi));
      		}
      	};
       
      }

      画面に配置した各ウィジットの属性(Property)と紐付け(Binding)するものはObservable(を継承したクラス)、onClickなどのイベントはCommandで実装します。EditTextに入力された情報はtextプロパティ(文字列型)で参照するため、StringObservableの変数としました。BMI計算結果を表示するTextViewについてもtextプロパティ(文字列型)ですのでStringObservable、Buttonのクリックイベントを処理するCommand名をcalcBmiとしました。
      続いてCommandのInvokeにBMI計算のロジックを記述します。StringObservableのHeight/Weightから値を取り出し、計算した結果をStringObservableのBmiに設定しました。

    7. 画面にModelとの紐付けを追記します。AndroidBindingライブラリが使用できるようにnamespaceを追加し、各ウィジットのプロパティにModelで実相したObservable/Commandを紐づけていきます。「binding:xxx=”yyy”」の箇所がそれです。
      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:binding="http://www.gueei.com/android-binding/"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent"
          android:orientation="vertical" >
       
          <EditText
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:inputType="number"
              android:hint="@string/hint_height"
              binding:text="Height">
       
              <requestFocus />
          </EditText>
       
          <EditText
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:inputType="number"
              android:hint="@string/hint_weight"
              binding:text="Weight"/>
       
          <Button
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="@string/caption_calcbmi"
              binding:onClick="calcBmi"/>
       
          <TextView
              android:id="@+id/textView1"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              binding:text="Bmi" />
       
       
      </LinearLayout>
    8. 画面(View)とそれを扱うクラス(Model)を紐付ける処理を記述します。通常はActivityを継承したクラスで画面を生成しますが、Android BindingではActivityクラスの代わりにBindingActivityクラスを継承します。
      package jp.tworks.android.bmi;
       
      import gueei.binding.app.BindingActivity;
      import android.os.Bundle;
       
      public class BmiActivity extends BindingActivity {
          /** Called when the activity is first created. */
          @Override
          public void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              // setContentView(R.layout.main);
       
              // モデルを生成
              BmiModel model = new BmiModel();
       
              // Layout.xmlの binding:xxx と modelを関連付ける
              setAndBindRootView(R.layout.main, model);
          }
      }

      setContentViewの代わりにsetAndBindRootViewというメソッドを使用します。このタイミングでViewとModelが実際に紐づきます。

    9. これで完成…と思いきや実はおまじないが足りておらず、これだけではウンともスンとも動きません(ここでハマること30分)。アプリケーションが起動するタイミングでAndroid Bindingライブラリの初期化を行う必要があります。android.app.Applicationクラスを継承したクラスを追加し、アプリケーション起動時にそれを呼ぶようにAndroid.Mainfestを変更します。
      プロジェクトの適当な場所に、android.app.Applicationを継承したクラスを追加します。
      クラス名をBmiApplicationにしました。継承元クラスはandroid.app.Applicationです。
      Applicationクラスの追加
      onCreateメソッドをオーバーライドし、Android Bindingの初期化処理を記述します。

      package jp.tworks.android.bmi;
       
      import gueei.binding.Binder;
      import android.app.Application;
       
      public class BmiApplication extends Application {
       
      	@Override
      	public void onCreate() {
      		super.onCreate();
      		Binder.init(this);
      	}
       
      }

      Android.Mainfestの「Application Attributes」–>「Name」に、追加したクラス名を設定します。
      Applicationクラスの置き換え設定

    10. これで完成です。身長と体重を入力してCalc BMIボタンを押すと、計算結果が表示されますね!
      実行結果1
    11. Android Bindingはこれ以外に入力チェック(Validation)の仕組みも持っています。今回は必須入力チェック(Required)をつけてみました。入力チェックはObservableな変数にアノテーションで指定します。更にModelValidator.ValidateModelメソッドで入力チェックを動かします。
      package jp.tworks.android.bmi;
       
      import android.view.View;
      import gueei.binding.Command;
      import gueei.binding.observables.StringObservable;
      import gueei.binding.validation.ModelValidator;
      import gueei.binding.validation.ValidationResult;
      import gueei.binding.validation.validators.Required;
       
      public class BmiModel {
       
      	// for EditText TextProperty
      	@Required(ErrorMessage = "Height is required.")
      	public StringObservable Height = new StringObservable();
       
      	@Required(ErrorMessage = "Weight is required.")
      	public StringObservable Weight = new StringObservable();
       
      	public StringObservable Bmi = new StringObservable(); 
       
      	// for Button on Click
      	public Command calcBmi = new Command() {
       
      		public void Invoke(View view, Object... args) {
      			// アノテーションで指定した入力チェックを実行する
      			ValidationResult result = ModelValidator.ValidateModel(BmiModel.this);
       
      			if (result.isValid()) {	// Validation OK
      				int height = Integer.valueOf(Height.get());
      				int weight = Integer.valueOf(Weight.get());
      				int bmi = (int)(weight / Math.pow(height / 100.0, 2));
      				Bmi.set(String.valueOf(bmi));
      			}
      			else {// Validation NG
      				// エラーを表示
      				String message = "";
      				for(String error : result.getValidationErrors()){
      					message += error + "\n";
      					Bmi.set(message);
      				}
      			}
      		}
      	};
       
      }

      実際に動かしてみるとこんな感じになります。手軽ですね。
      実行結果2

  • 所感
    AndroidプログラミングではView層とModel(ドメインロジック層)が強結合になりがちですが、Android Bindingを使うと少ない手間で祖結合にすることが出来ます。祖結合になることでUIから独立したドメインロジックを記述し易くなり、自動テストの範囲を広めるなどのメリットをもたらします。
  • 注意
    …とここまで書いて割と良いことだらけに見えますが、1つ大きな落とし穴がありました。Andorid Bindingライブラリのライセンスは本稿執筆時点でLGPLとなっています。AndroidにおけるLGPLというのは致命的で(理由はググってください)ソースを非公開にしたい商用アプリケーションなどでは使用できない状況です。ここだけ何とかならないかな…。
  • ライセンスは2012年6月にLGPLからMITに変更になりました。これで使用範囲を広げられそうですね!