オッス!おらタイン!
ということで、今回はAndroidの開発者向けに便利なアーキテクチャのお話をすっかんな。
さっそくごらんください。
(オラわくわくしてきたぞ!)

アーキテクチャパターン

特定のシステムを実装する為に、どのようなクラスがあるか?お互いにどのように相互作用するか?を説明する為の一連のルールであります。

一般なアーキテクチャのパターンは下記の通りです。

  • Domain Driven Design
  • Three-tier
  • Micro-kernel
  • Model-View-Controller
  • Model-View-ViewModel

特にAndroidに人気のあるアーキテクチャパターンはMVC, MVP, MVVMです。今回、この記事ではData Bindingを使用して、MVVMを実装する方法を紹介したいと思います。

Data Bindingを使用するMVVM

MVVMの各コンポーネントの役割

  • Model: アプリ内に使用するデータ
  • View: ユーザーにデータを表示する
  • ViewModel: ビジネスロジックを含み、ビューに表示するデータを提供する

Data BindingライブラリーはModelViewに宣言しているObservableフィルードをTextViewやImageViewなどUI要素をバインドして、ViewとViewModelの間に双方向で実行出来ます。

Data Bindingライブラリー

Android 4.0(APIレベル14)以上を実行する端末ではData Bindingライブラリーを使用出来ます。

はじめに

環境を準備

Data Bindingライブラリーを使用するにはdataBindingをappモジュールのbuild.gradleファイルに次のように追加します。

android {
    ...
    dataBinding {
        enabled = true
    }
}

ビルードプロセスをスピードアップし、ややこしいエラーを避ける為に、gradle.propertiesファイルに次のように追加してください。

android.databinding.enableV2=true

レイアウトとバインディング式

レイアウトファイルはlayoutタグで始まり、data要素とルートviewが続きます。次に例を出します。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="https://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);
// または
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

イベント処理

ライブラリーはonClickイベントなどビューから送られたイベントを処理する式を記述することができます。イベントを処理するには、次の二つの方法があります。

メソッド参照

構文はこれまで実装している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="https://schemas.android.com/apk/res/android"
        xmlns:bind="https://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にする2つの方法があります。

  • ビルドインObservableクラスを使用する
  • Observableインタフェースを実装する

ビルドインObservableクラスを使用する

次のようにライブラリーがビルドインObservableクラスを提供しています。

  • ObservableBoolean
  • ObservableByte
  • ObservableChar
  • ObservableShort
  • ObservableInt
  • ObservableLong
  • ObservableFloat
  • ObservableDouble
  • ObservableParcelable
  • ObservableArrayMap

例:

<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メソッドを呼び出すか、またはデフォルードのsetterメッソッドを上書くロッジックをすることができます。

置き換えるsetterメソッドを指定する

setterメソッドは属性名と一致しない場合もあるのでその時、置き換えるsetterメソッドを指定する必要があります。その時、BindingMethodsアノテーションを使います。 例えば、android:tint属性におけるsetTintのsetterメソッドはないが、setImageTintListsetterメソッドがあります。

@BindingMethods(value = [
    BindingMethod(
        type = android.widget.ImageView::class,
        attribute = "android:tint",
        method = "setImageTintList")])

注意: 置き換えるsetterメソッドはライブラリーがほぼ設定してもらっていますので、必要ではない限り、やらない方法がいいです。

カスタムロジック

たとえば、インターネットから画像をロードしてビューに表示するなど、希望に合わせてバインディングロジックをカスタマイズすることができます。

<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);
}

注意:コンフリクトが発生した場合、定義したバインディングアダプターはデフォルトのバインディングアダプタを上書きします。

双方向データバインディング

データリソースの変更がある際にビューを更新します。一方で、ビューの状態の変更がある際にデータリソースに反映します。

双方向データバインディングにする為に、@の後に=が必要です。例を出します。

<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オブジェクトにOnPropertyChangedCallbackリスナーを登録することで、Observableオブジェクトのデータが変更する際に、カスタマロジックをはめることができます。次の例のように、ユーザー名、パスワード、およびチェックボックスをすべて入力される場合、ログインボタンを有効になります。

public final ObservableField username = new ObservableField();
public final ObservableField password = new ObservableField();
public final ObservableBoolean checkbox = new ObservableBoolean();

public UserModelView(LoginNavigator navigator) {
    this.mNavigator = navigator;
    this.mUserRepository = new UsersRepository();

    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

投稿者プロフィール

クオン タイン