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

式言語を使用すると、ビューからディスパッチされたイベントを処理する式を記述できます。データ バインディング ライブラリは、レイアウト内のビューとデータ オブジェクトをバインドするために必要なクラスを自動的に生成します。

データ バインディングのレイアウト ファイルは少し変わっていて、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 変数で、このレイアウト内で使用する可能性があるプロパティを記述します。

<variable name="user" type="com.example.User" />
    

レイアウト内の式は、「@{}」構文を使用して、属性プロパティの形式で記述します。ここでは、TextView テキストは user 変数の firstName プロパティに設定されます。

<TextView android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="@{user.firstName}" />
    

データ オブジェクト

ここで、従来のオブジェクトで User エンティティを記述するとします。

Kotlin

    data class User(val firstName: String, val lastName: String)

    

Java


    public class User {
      public final String firstName;
      public final String lastName;
      public User(String firstName, String lastName) {
          this.firstName = firstName;
          this.lastName = lastName;
      }
    }

    

この型のオブジェクトには、変更されることがないデータが格納されます。アプリでは、読み取られたデータがその後変更されないことがよくあります。また、次の例に示すように、Java のアクセサ メソッドを使用するなどの一連の規則に従うオブジェクトを使用することもできます。

Kotlin

    // Not applicable in Kotlin.
    data class User(val firstName: String, val lastName: String)

    

Java

    public class User {
      private final String firstName;
      private final String lastName;
      public User(String firstName, String lastName) {
          this.firstName = firstName;
          this.lastName = lastName;
      }
      public String getFirstName() {
          return this.firstName;
      }
      public String getLastName() {
          return this.lastName;
      }
    }

    

データ バインディングの観点からすると、これら 2 つのクラスは同等です。android:text 属性に使用されている式 @{user.firstName} は、前者のクラスの firstName フィールドと後者のクラスの getFirstName() メソッドにアクセスします。また、このメソッドが存在する場合は firstName() にも解決されます。

データのバインド

バインディング クラスはレイアウト ファイルごとに生成されます。デフォルトのクラス名は、レイアウト ファイルの名前がパスカルケースに変換され、Binding サフィックスが付加されたものになります。上のレイアウト ファイルの名前は activity_main.xml であるため、生成されるクラスの名前は ActivityMainBinding になります。このクラスは、レイアウト プロパティ(user 変数など)からレイアウトのビューへのバインディングをすべて保持します。また、バインディング式で使用する値の割り当て方法を認識しています。バインディングの作成は、次の例に示すように、レイアウトをインフレートしている間に行うことをおすすめします。

Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding: ActivityMainBinding = DataBindingUtil.setContentView(
                this, R.layout.activity_main)

        binding.user = User("Test", "User")
    }

    

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

    

実行時に、アプリの UI に Test ユーザーが表示されます。また、次の例に示すように、LayoutInflater を使用してビューを取得できます。

Kotlin

    val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())

    

Java

    ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());

    

FragmentListViewRecyclerView アダプター内でデータ バインディング アイテムを使用している場合は、次のコード例に示すように、バインディング クラスまたは DataBindingUtil クラスの inflate() メソッドを使用することをおすすめします。

Kotlin

    val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
    // or
    val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

    

Java

    ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
    // or
    ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

    

式言語

一般的な機能

式言語はマネージコードの式によく似ています。式言語では次の演算子とキーワードを使用できます。

  • 数学 + - / * %
  • 文字列の連結 +
  • 論理的 && ||
  • バイナリ & | ^
  • 単項 + - ! ~
  • シフト >> >>> <<
  • 比較 == > < >= <=<&lt; としてエスケープする必要があることに注意)
  • instanceof
  • グループ化 ()
  • リテラル - 文字、文字列、数値、null
  • キャスト
  • メソッド呼び出し
  • フィールド アクセス
  • 配列アクセス []
  • 3 項演算子 ?:

例:

android:text="@{String.valueOf(index + 1)}"
    android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
    android:transitionName='@{"image_" + id}'
    

使用できない演算

次の演算は、マネージコードで使用できる式の構文に含まれていません。

  • this
  • super
  • new
  • 明示的な汎用呼び出し

null 合体演算子

null 合体演算子(??)は、左のオペランドが null でない場合は左のオペランドを、左のオペランドが null の場合は右のオペランドを選択します。

android:text="@{user.displayName ?? user.lastName}"
    

これは、機能的に次と同等です。

android:text="@{user.displayName != null ? user.displayName : user.lastName}"
    

プロパティ参照

次の形式(フィールド、ゲッター、ObservableField オブジェクトについても同様)を使用することにより、式でクラス内のプロパティを参照できます。

android:text="@{user.lastName}"
    

null ポインタ例外の回避

生成されたデータ バインディング コードは null 値を自動的にチェックして、null ポインタ例外を回避します。たとえば式 @{user.name}では、user が null の場合、user.name にデフォルト値の null が割り当てられます。user.age(age は int 型)を参照する場合、データ バインディングではデフォルト値の 0 が使用されます。

ビューの参照

次の構文を使用することで、ID により式でレイアウト内の他のビューを参照できます。

android:text="@{exampleText.text}"
    

次の例では、TextView ビューは、同じレイアウトの EditText ビューを参照しています。

<EditText
        android:id="@+id/example_text"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"/>
    <TextView
        android:id="@+id/example_output"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{exampleText.text}"/>
    

コレクション

[] 演算子を使用すると、一般的なコレクション(配列、リスト、疎なリスト、マップなど)に簡単にアクセスできます。

<data>
        <import type="android.util.SparseArray"/>
        <import type="java.util.Map"/>
        <import type="java.util.List"/>
        <variable name="list" type="List&lt;String>"/>
        <variable name="sparse" type="SparseArray&lt;String>"/>
        <variable name="map" type="Map&lt;String, String>"/>
        <variable name="index" type="int"/>
        <variable name="key" type="String"/>
    </data>
    …
    android:text="@{list[index]}"
    …
    android:text="@{sparse[index]}"
    …
    android:text="@{map[key]}"
    

また、object.key の表記を使用すると、マップの値を参照できます。たとえば、上の例の @{map[key]}@{map.key} に置き換えることができます。

文字列リテラル

属性値は一重引用符で囲むことができます。このため、次の例に示すように、式で二重引用符を使用できます。

android:text='@{map["firstName"]}'
    

また、属性値を二重引用符で囲むこともできます。その場合、文字列リテラルをバッククォート ` で囲む必要があります。

android:text="@{map[`firstName`]}"
    

リソース

次の構文により、式でアプリのリソースを参照できます。

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
    

パラメータを指定することにより、書式設定文字列と複数形を評価できます。

android:text="@{@string/nameFormat(firstName, lastName)}"
    android:text="@{@plurals/banana(bananaCount)}"
    

プロパティ参照ビュー参照をリソース パラメータとして渡すことができます。

android:text="@{@string/example_resource(user.lastName, exampleText.text)}"
    

複数形で複数のパラメータを受け取る場合は、すべてのパラメータを渡す必要があります。


      Have an orange
      Have %d oranges

    android:text="@{@plurals/orange(orangeCount, orangeCount)}"
    

リソースによっては、次の表に示すように、明示的な型の評価が必要になります。

通常の参照 式参照
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
color int @color @color
ColorStateList @color @colorStateList

イベント処理

データ バインディングを使用すると、ビューからディスパッチされたイベントを処理する式(onClick() メソッドなど)を記述できます。イベントの属性名はリスナー メソッドの名前によって決まります(いくつか例外があります)。たとえば、View.OnClickListener には onClick() メソッドがあるため、このイベントの属性は android:onClick になります。

クリック イベント用に特殊なイベント ハンドラがいくつか用意されています。これらのハンドラでは、競合を回避するために android:onClick 以外の属性が必要になります。こうした競合を回避するには、以下の属性を使用します。

クラス リスナーのセッター 属性
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

イベントを処理するために以下のメカニズムを使用できます。

  • メソッド参照: 式では、リスナー メソッドのシグネチャに準拠するメソッドを参照できます。式がメソッド参照として評価されると、データ バインディングによってメソッド参照と所有者オブジェクトがリスナーにラップされ、対象のビューにそのリスナーが設定されます。式が null と評価されると、データ バインディングでリスナーが作成されず、代わりに null リスナーが設定されます。
  • リスナー バインディング: イベントの発生時に評価されるラムダ式です。データ バインディングでは必ずリスナーを作成してビューに設定します。イベントがディスパッチされると、そのリスナーがラムダ式を評価します。

メソッド参照

android:onClick をアクティビティのメソッドに割り当てられるのと同様に、イベントをハンドラ メソッドに直接バインドできます。View onClick 属性と比べた場合の大きなメリットは、式がコンパイル時に処理される点です。そのため、メソッドが存在しない場合やメソッドのシグネチャが不正な場合は、コンパイル時にエラーになります。

メソッド参照とリスナー バインディングの主な違いは、リスナーの実際の実装が、イベントがトリガーされたときではなくデータがバインドされたときに作成される点です。イベントの発生時に式を評価したい場合は、リスナー バインディングを使用する必要があります。

イベントをそのハンドラに割り当てるには、通常のバインディング式を使用し、値に呼び出すメソッドの名前を指定します。例として、次のレイアウト データ オブジェクトについて考えてみましょう。

Kotlin

    class MyHandlers {
        fun onClickFriend(view: View) { ... }
    }

    

Java

    public class MyHandlers {
        public void onClickFriend(View view) { ... }
    }

    

バインディング式では、次のようにビューのクリック リスナーを onClickFriend() メソッドに割り当てることができます。

<?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data>
           <variable name="handlers" type="com.example.MyHandlers"/>
           <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}"
               android:onClick="@{handlers::onClickFriend}"/>
       </LinearLayout>
    </layout>
    

リスナー バインディング

リスナー バインディングは、イベントの発生時に実行されるバインディング式です。メソッド参照に似ていますが、リスナー バインディングでは任意のデータ バインディング式を実行できます。この機能は、Android Gradle プラグイン バージョン 2.0 以降で使用できます。

メソッド参照では、メソッドのパラメータがイベント リスナーのパラメータと一致する必要があります。リスナー バインディングでは、戻り値だけがリスナーの期待される戻り値(void の場合を除く)と一致する必要があります。例として、onSaveClick() メソッドを持つ次のプレゼンター クラスについて考えてみましょう。

Kotlin

    class Presenter {
        fun onSaveClick(task: Task){}
    }

    

Java

    public class Presenter {
        public void onSaveClick(Task task){}
    }

    

さらに、次のようにクリック イベントを onSaveClick() メソッドにバインドできます。

<?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <variable name="task" type="com.android.example.Task" />
            <variable name="presenter" type="com.android.example.Presenter" />
        </data>
        <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
            <Button android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:onClick="@{() -> presenter.onSaveClick(task)}" />
        </LinearLayout>
    </layout>
    

式でコールバックを使用する場合、データ バインディングによって必要なリスナーが自動的に作成され、イベント用に登録されます。ビューでイベントが発生すると、データ バインディングで所定の式が評価されます。通常のバインディング式と同様に、これらのリスナーの式が評価されている間に null が返され、データ バインディングのスレッドの安全性が確保されます。

上記の例では、onClick(View) に渡される view パラメータを定義しませんでした。リスナー バインディングにはリスナーのパラメータに関する 2 つの選択肢があります(メソッドへのパラメータをすべて無視するか、すべてのパラメータに名前を付けることができます)。パラメータに名前を付けると、式で使用できるようになります。たとえば、上の式は次のように記述できます。

android:onClick="@{(view) -> presenter.onSaveClick(task)}"
    

あるいは、式でパラメータを使用する場合、次のように記述できます。

Kotlin

    class Presenter {
        fun onSaveClick(view: View, task: Task){}
    }

    

Java

    public class Presenter {
        public void onSaveClick(View view, Task task){}
    }

    
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
    

ラムダ式では複数のパラメータを指定できます。

Kotlin

    class Presenter {
        fun onCompletedChanged(task: Task, completed: Boolean){}
    }

    

Java

    public class Presenter {
        public void onCompletedChanged(Task task, boolean completed){}
    }

    
<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
          android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
    

リッスンしているイベントが void 以外の型の値を返す場合、式でも同じ型の値を返す必要があります。たとえば、長いクリック イベントをリッスンする場合、式はブール値を返す必要があります。

Kotlin

    class Presenter {
        fun onLongClick(view: View, task: Task): Boolean { }
    }

    

Java

    public class Presenter {
        public boolean onLongClick(View view, Task task) { }
    }

    
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
    

null オブジェクトが原因で式を評価できない場合、データ バインディングはその型のデフォルト値を返します。たとえば、参照型の場合は nullint の場合は 0boolean の場合は false を返します。

述語(3 項など)を指定して式を使用する必要がある場合は、void を記号として使用できます。

android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
    

複雑なリスナーを使用しない

リスナーの式は非常に強力です。リスナーの式を使用することで、コードの読みやすさを大幅に改善できます。その一方で、リスナーに複雑な式を含めると、レイアウトが読みにくくなり、管理が困難になります。リスナーに含める式は、UI からコールバック メソッドに使用可能なデータを渡す程度のシンプルなものにする必要があります。ビジネス ロジックは、リスナーの式から呼び出したコールバック メソッド内に実装する必要があります。

インポート、変数、インクルード

データ バインディング ライブラリには、インポート、変数、インクルードなどの機能が用意されています。インポートを使用すると、レイアウト ファイル内のクラスを簡単に参照できます。 変数を使用すると、バインディング式で使用可能なプロパティを記述できます。インクルードを使用すると、複雑なレイアウトをアプリ全体で再利用できます。

インポート

インポートを使用すると、マネージコードと同様に、レイアウト ファイル内のクラスを簡単に参照できます。ゼロ個以上の import 要素を data 要素内で使用できます。次のコード例では、View クラスをレイアウト ファイルにインポートしています。

<data>
        <import type="android.view.View"/>
    </data>
    

View クラスをインポートすると、バインディング式から参照できるようになります。次の例は、View クラスの VISIBLE 定数と GONE 定数を参照する方法を示しています。

<TextView
       android:text="@{user.lastName}"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
    

型のエイリアス

クラス名が競合している場合、いずれかのクラスの名前をエイリアスに変更できます。次の例では、com.example.real.estate パッケージの View クラスの名前を Vista に変更しています。

<import type="android.view.View"/>
    <import type="com.example.real.estate.View"
            alias="Vista"/>
    

Vista を使用して com.example.real.estate.View を参照できます。また、View を使用すると、レイアウト ファイル内の android.view.View を参照できます。

他のクラスをインポートする

インポートされた型は、変数や式で型参照として使用できます。次の例で、UserList は、変数の型として使用されています。

<data>
        <import type="com.example.User"/>
        <import type="java.util.List"/>
        <variable name="user" type="User"/>
        <variable name="userList" type="List&lt;User>"/>
    </data>
    

また、インポートされた型を使用して式の一部をキャストすることもできます。次の例では、connection プロパティを User 型にキャストています。

<TextView
       android:text="@{((User)(user.connection)).lastName}"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
    

インポートされた型は、静的フィールドや静的メソッドを式で参照する際にも使用できます。次のコードは、MyStringUtils クラスをインポートし、その capitalize メソッドを参照しています。

<data>
        <import type="com.example.MyStringUtils"/>
        <variable name="user" type="com.example.User"/>
    </data>
    …
    <TextView
       android:text="@{MyStringUtils.capitalize(user.lastName)}"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
    

マネージコードと同様に、java.lang.* は自動的にインポートされます。

変数

data 要素内で複数の variable 要素を使用できます。各 variable 要素でレイアウトに設定可能なプロパティを記述して、レイアウト ファイル内のバインディング式で使用できます。次の例では、userimagenote の各変数を宣言しています。

<data>
        <import type="android.graphics.drawable.Drawable"/>
        <variable name="user" type="com.example.User"/>
        <variable name="image" type="Drawable"/>
        <variable name="note" type="String"/>
    </data>
    

変数の型はコンパイル時に検査されます。そのため、変数に Observable を実装する場合や、変数が監視可能なコレクションの場合は、それを型に反映する必要があります。変数が Observable インターフェースを実装していない基本クラスまたはインターフェースの場合、変数は監視されません。

各種構成(横向きや縦向きなど)に対して別々のレイアウト ファイルが存在する場合、変数が結合されます。これらのレイアウト ファイル間で変数の定義が競合してはなりません。

生成されたバインディング クラスには、記述された各変数用のセッターとゲッターがあります。セッターが呼び出されるまで、各変数にはマネージコードのデフォルト値が設定されます。たとえば、参照型の場合は nullint の場合は 0boolean の場合は false が設定されます。

context という名前の特別な変数が生成され、必要に応じてバインディング式で使用されます。context の値には、ルートビューの getContext() メソッドで取得した Context オブジェクトが指定されます。context 変数は、その名前での明示的な変数宣言によって上書きされます。

インクルード

アプリの名前空間と属性の変数名を使用することで、含まれるレイアウトからインクルードしたレイアウトのバインディングに変数を渡すことができます。次の例は、name.xml および contact.xml レイアウト ファイルからインクルードした user 変数を示しています。

<?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>
    

データ バインディングでは、include を merge 要素の直接の子として使用することはできません。たとえば、次のレイアウトはサポートされません

<?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>
       <merge><!-- Doesn't work -->
           <include layout="@layout/name"
               bind:user="@{user}"/>
           <include layout="@layout/contact"
               bind:user="@{user}"/>
       </merge>
    </layout>
    

参考情報

データ バインディングの詳細については、以下の参考情報をご確認ください。

サンプル

コードラボ

ブログ投稿