アーキテクチャパターン
特定のシステムを実装する為に、どのようなクラスがあるか?お互いにどのように相互作用するか?を説明する為の一連のルールであります。
一般的なアーキテクチャパターンは次のとおりです。
MVC、MVP、MVVMなどのAndroidの一般的なアーキテクチャパターンです。 そして、この記事ではData Bindingラブラリーを使用してMVVMを実装する方法を紹介したいと思います。
Data Bindingを使用するMVVM
MVVMにおける各コンポーネントの役割:
Model: アプリ内に使用するデータ
View: ユーザーにデータを表示する
ViewModel: ビジネスロジックを含み、Viewに表示するデータを提供する
Data BindingライブラリーはModelViewに宣言しているObservableフィルードをTextViewやImageViewなどUI要素をバインドして、ViewとViewModelの間に双方向で実行出来ます。
Data Bindingライブラリー
Data Bindingライブラリーは、Android 4.0(API レベル 14)以降を実行しているデバイスで使用できます。
環境を準備
Data Bindingライブラリーを使用するには、次のようにappモジュールのbuild.gradleファイルに dataBinding
を追加します。
android {
...
dataBinding {
enabled = true
}
}
ビルドプロセスをスピードアップし、不必要なエラーを回避します。 次のオプションを gradle.propertiesファイルに追加します。
android.databinding.enableV2=true
レイアウトとバインディング式
レイアウトファイルはlayoutタグで始まり、data要素とルートviewが続きます。次に例を出します。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
dataタグのuser変数は、ビューで使用できるオブジェクトを記述します。
データオブジェクト
レイアウトでデータオブジェクトを使用するには、dataタグで変数として宣言する必要があります。
レイアウトにuser.firstnameを宣言すれば、ライブラリーはuserオブジェクトにfirstnameフィルード、getFirstname()またはfirstname()メソッドをアクセスします。
データバインディング
ライブラリーはルートタグlayoutを持つレイアウトファイル毎に自動的にバインドディングクラスを生成します。バインドディングクラス名はデフォルトでレイアウトファイルの名前を基づいてパスカルケースに変換され、接尾辞にBindingを追加します。次のように例を出します。
レイアウトファイル | バイディングクラス |
activity_main.xml | ActivityMainBinding.java |
fragment_main.xml | FragmentMainBinding.java |
item_user.xml | ItemUserBinding.java |
以下は、データバインディングを実装する例です。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("Test", "User");
binding.setUser(user);
}
Fragment、ListViewまたは RecyclerViewのアダプタでデータバインディングを使用している場合は、以下のメソッドを使用出来ます。
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
イベント処理
データバインディングを使用すると、ビューからディスパッチされたイベントを処理する式を記述できます。 以下を使用してイベントを処理できます。
メソッド参照
構文はこれまで実装しているandroid:onClickと同じですが、ライブラリーは実行タイミングではなくコンパイルタイミングに処理メソッドを有効かどうかチェックします。エラーがある場合、コンパイルエラーを返します。
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
android:onClick="@{handlers::onClickFriend}"/>
注意: レイアウト内にメソッドのシグネチャはリスナーオブジェクト内のメッソッドのシグネチャと一致する必要があります。シグネチャとはメッソッド名、パラメータ一覧とパラメータタイプです。
リスナーのバインディング
ビューがイベントを発生させると、データバインディングはバインディング式を評価します。 リッスンしているイベントがvoid以外のデータ型を返す場合、バインディング式も同じデータ型を返す必要があります。 次のように例を出します。
public class Presenter {
public boolean onLongClick(View view, Task task) { }
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
インポート、変数とインクルード
インポート: バインディング式からレイアウトファイルの外部にあるクラスを簡単に参照できます。 次の例では、TextUtilsクラスをレイアウトファイルにインポートします。
<data>
<import type="android.text.TextUtils"/>
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.username}"
android:visibility="@{TextUtils.isEmpty(user.username) ? View.GONE : View.VISIBLE, default=`gone`}"
タイプエイリアス: 異なるパッケージに同じ名前のクラスが多数存在することによるクラス名の競合を避けるために、エイリアスを使用できます。 例えば:
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
インクルード: 子レイアウトを追加できます。 例えば:
<?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>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</LinearLayout>
</layout>
Observableオブジェクト
Observableは、observerパータンにおけるobserverを登録・解除、observerに自分のデータ変更がある際に通知することができます。ライブラリーでは、フィールド、オブジェクトまたはコレクションをObservableにする二つの方法があります。
ビルドインObservableクラスを使用する
次のようにライブラリーがビルドインObservableクラスを提供しています。
例えば:
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:checked="@{vm.checkbox}"/>
public final ObservableBoolean checkbox = new ObservableBoolean();
observableであるcheckboxの状態が変更のある際にchecbox.set(true or flase)
メソッドを呼び出すことでビューを自動的に更新する為に、通知することができます。
Observableインタフェースを実装する
Obserableインタフェースは手動でリスナーを登録・解除、通知することができます。されに簡単にしる為に、BaseObservableインタフェースを実装するべきです。GetterメソッドにBindableアノテーションを使用し、setterメソッドにnotifyPropertyChanged()を呼び出すことで、Observableオブジェクトにすることができます。次の例を出します。
private static class User extends BaseObservable {
private String username;
@Bindable
public String getUserName() {
return this.username;
}
public void setUserName(String username) {
this.username = username;
notifyPropertyChanged(BR.username);
}
}
コンパイルする際にBindableアノテーションがあるものに該当するBRファイル内に一つのIDを生成します。
バインディング アダプター
バインディングアダプタは、属性のデータが変更された際に新しいデータを属性に設定する為に、該当なsetterメソッドを呼び出します。
データバインディングはデフォルトのセッターメソッドを自動的に呼び出すか、代替のセッターメソッドを指定するか、デフォルトのセッターメソッドを上書きすることができます.
たとえば、example
属性の場合、ライブラリは自動的にsetExample(arg)
メソッドを検索し、exampleのデータが変更した時に新しいデータをexample属性に設定します。
置き換えるsetterメソッドを指定する
一部の属性には、setter メソッドの名前と一致しない名前が付いています。 次に、BindingMethodsを使用して代替setterメソッドを指定する必要があります。 たとえば、android:tint
属性には対応するsetterメソッドsetTint がありませんが、setterメソッド setImageTintList
にバインドされます。
@BindingMethods(value = [
BindingMethod(
type = android.widget.ImageView::class,
attribute = "android:tint",
method = "setImageTintList")])
カスタムロジック
たとえば、インターネットから画像をロードしてビューに表示するなど、希望に合わせてバインディングロジックをカスタマイズすることができます。
<ImageView
android:id="@+id/image_product"
android:layout_width="96dp"
android:layout_height="96dp"
app:image="@{product.image_url}"
bind:type="@{`product`}"/>
@BindingAdapter(value={"image", "type"}, requireAll=false)
public static void loadImage(ImageView view, String url, String type) {
ImageLoader.load(url).into(view);
}
注意:コンフリクトが発生した場合、定義したバインディングアダプターはデフォルトのバインディングアダプタを上書きします。
双方向データバインディング
これは双方向のデータバインディングです。 一つの方法は、UIの状態が変化するとデータリソースに更新され、逆にデータリソースに変化があれば UIに更新されます。
双方向のデータバインディングを実行するには、@
の後に=
を追加する必要があります。
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:checked="@={vm.checkbox}"/>
public final ObservableBoolean checkbox = new ObservableBoolean();
オブジェクトOのデータが変更されたときにロジックLを実行したいとします。どうすればよいでしょうか? 次に、OnPropertyChangedCallback リスナーをインスタンス化し、それをOオブジェクトに登録することでそれを行うことができます。 次の例のように、ユーザーがユーザー名、パスワード、チェックボックスの両方を入力したときにログイン ボタンを有効にするロジックを実装します。
public final ObservableField<String> username = new ObservableField<>();
public final ObservableField<String> password = new ObservableField<>();
public final ObservableBoolean checkbox = new ObservableBoolean();
public UserModelView(LoginNavigator navigator) {
Observable.OnPropertyChangedCallback callback = new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable sender, int propertyId) {
notifyPropertyChanged(BR.loginEnabled);
}
};
username.addOnPropertyChangedCallback(callback);
password.addOnPropertyChangedCallback(callback);
checkbox.addOnPropertyChangedCallback(callback);
}
@Bindable
public boolean getLoginEnabled() {
if (checkbox.get()
&& !TextUtils.isEmpty(username.get())
&& !TextUtils.isEmpty(password.get())) {
return true;
}
return false;
}
public void onLoginButtonClicked() {
boolean login = mUserRepository.login(new User(username.get(), password.get()));
if (login) {
if (mNavigator != null) {
mNavigator.gotoMain();
}
}
}
ここでデモプロジェクトを作成しました。
参考資料
https://developer.android.com/topic/libraries/data-binding/
https://medium.com/mindorks/android-architecture-patterns-mv-c-p-vm-4594574eeaa1/
https://herbertograca.com/2017/07/28/architectural-styles-vs-architectural-patterns-vs-design-patterns/