Android
 Computer >> コンピューター >  >> システム >> Android

Androidのデータバインディング入門:Data Binding Libraryでレイアウトとデータを結びつける方法

データバインディングとは、アプリが扱う「データ」と、画面上の視覚的なUI要素を結びつける(バインドする)ための手法です。この仕組みを使うと、UI側の値が更新されるたびに、裏側で保持しているデータも自動的に更新されます。

決して目新しい概念ではなく、AngularJS、React、Vueなど、多くのフロントエンドフレームワークがすでにこの仕組みを設計に取り入れています。

しかし本記事で注目するのはフロントエンドフレームワークではなく、モバイル開発です。GoogleはAndroid向けにData Binding Libraryを提供しており、これはAndroid Jetpackの一部として位置づけられています。

Jetpackライブラリ群をご存じない方もいるかもしれません。それは、Googleが従来のサポートライブラリの開発終了を発表し、代わりにAndroidXライブラリ(サポートライブラリの後継バージョン)への移行を進めているためです。

アダプターを使ったデータバインディングの解説記事は数多く存在しますが、本記事ではそこには踏み込みません。代わりに、書くべきコード量を最小限に抑えて開発時間を節約できる、シンプルで最小構成のデータバインディングの実装方法をご紹介します。

なぜデータバインディングを使うのか?

まだメリットがピンと来ていない方のために、具体的な例で説明しましょう。カスタムボタンが3つ並んだメニュー画面があり、それぞれのボタンが独立したレイアウトを持っているケースを想定します。

Androidのデータバインディング入門:Data Binding Libraryでレイアウトとデータを結びつける方法
今回作成するレイアウト

これを実現する一つの方法は、XMLレイアウトを4つ用意することです。メインレイアウト1つと、ボタンごとのレイアウト3つです。

各ボタンはアプリ内の異なる画面へユーザーを誘導するため、表示するテキストや画像もそれぞれ異なるものが必要になるからです。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_gravity="center_horizontal"
    android:layout_height="wrap_content"
    android:layout_width="wrap_content">


    <ImageView
        android:id="@+id/imageView"
        android:layout_height="100dp"
        android:layout_width="100dp"
        android:src="@drawable/image_name"
        android:adjustViewBounds="true"
        android:scaleType="centerInside"
        />

    <TextView
        android:id="@+id/textView"
        android:gravity="center_horizontal"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:text="Image Text"
        android:textSize="16sp" />

</LinearLayout>
ボタンのレイアウト

たった3つのレイアウトなので、まだ大したコードの重複ではありません。しかし冷静に考えてみると、これらのコードを管理するのはかなりの時間の無駄です。商品画像などを多数表示するような、より複雑なレイアウトを持つアプリを想像してみてください。退屈なコードの複製が大量に発生してしまうでしょう。

データバインディングを使えば、すべてのボタンで共有できるXMLレイアウトをたった1つ作るだけで済みます。

どこから始める?

まず、プロジェクトでデータバインディングを有効化します。アプリのbuild.gradleファイルにdataBinding要素を追加しましょう。

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.tomerpacific.example"
        minSdkVersion 15
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        dataBinding {              //<-------
          enabled = true
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}
build.gradle

プロジェクトを同期したら、次にレイアウトへバインドするためのデータクラスを作成します。

package com.tomerpacific.example

import android.graphics.drawable.Drawable

data class ButtonData(val buttonText: String, val buttonImageSrc : Drawable)
ButtonData.kt

ButtonDataクラスには2つのフィールドがあります。

  • buttonText — 画像の下に表示されるテキストです。
  • buttonImageSrc — ボタンの画像を担当します。

さらに多くのデータを持たせたい場合は、データクラスにフィールドを追加するだけです。

実際のバインディング

次に、レイアウト内で変数を宣言して使えるようにします。この変数は先ほど作成したデータクラスに紐づきます。そのために必要な作業は2つです。

  • ルートレイアウト要素を<layout>タグで囲む。
  • 変数(buttonData)の宣言を含む<data>タグを追加する。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="https://schemas.android.com/apk/res/android">  // <---- 1
                                                                              
    <data>
        <variable name="buttonData" type="com.tomerpacific.example.ButtonData"/> // <---- 2
    </data>
    
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:app="https://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">


        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Example"
            android:textSize="30dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/linearLayout3"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView2">

            <ImageButton
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:adjustViewBounds="true"
                android:scaleType="centerInside"
                android:src="@drawable/android">

            </ImageButton>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:text="Image Text"
                android:textSize="16sp" />

        </LinearLayout>

        <LinearLayout
            android:id="@+id/linearLayout3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView2"
            app:layout_constraintVertical_bias="0.504">

            <ImageButton
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:adjustViewBounds="true"
                android:scaleType="centerInside"
                android:src="@drawable/android_p_logo">

            </ImageButton>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:text="Image Text"
                android:textSize="16sp" />

        </LinearLayout>

        <LinearLayout
            android:id="@+id/linearLayout2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.200"
            app:layout_constraintStart_toEndOf="@+id/linearLayout3"
            app:layout_constraintTop_toBottomOf="@+id/textView2"
            app:layout_constraintVertical_bias="0.504">

            <ImageButton
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:adjustViewBounds="true"
                android:scaleType="centerInside"
                android:src="@drawable/android_studio_icon">

            </ImageButton>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:text="Image Text"
                android:textSize="16sp" />

        </LinearLayout>


    </androidx.constraintlayout.widget.ConstraintLayout>
    
</layout>
activity_main.xml

ルートのlayoutタグへスキーマ定義が移動したため、以前のメインレイアウトからは削除できる点にも注目してください。また、追加した変数はデータクラスに直接紐づいています。

続いて、MainActivityファイルにバインディングを処理するコードを追加します。

package com.tomerpacific.example

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import com.tomerpacific.example.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

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

        binding.buttonData = ButtonData("First", resources.getDrawable(R.drawable.android))
    }
}
MainActivity.kt

レイアウト内に変数を作成すると、そのレイアウト専用のバインディングクラスが自動的に生成されます。今回はレイアウト名がactivity_mainなので、生成されるクラス名はActivityMainBindingになります。

命名規則は常に「レイアウト名 + Binding」です。

レイアウト内でbuttonDataという変数を宣言したので、それがバインディングオブジェクトに追加され、ButtonDataクラスの新しいインスタンスを代入できます。

ここまでできたら、いよいよバインドしたデータをレイアウト内で使えるようになります。

<LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/linearLayout3"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView2">

            <ImageButton
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:adjustViewBounds="true"
                android:scaleType="centerInside"
                android:src="@{buttonData.buttonImageSrc}">  // <----

            </ImageButton>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:text="@{buttonData.buttonText}"   // <----
                android:textSize="16sp" />
バインドした変数buttonDataの使用例

実行結果はこちらです。

Androidのデータバインディング入門:Data Binding Libraryでレイアウトとデータを結びつける方法
なかなかスムーズですね

ちょっと待って…ボタンが3つある場合は?

ボタンは3つあるのに、今のデータクラスは1つのボタン分しか対応していません。どうすればよいのでしょうか?

package com.tomerpacific.example

data class ButtonsData(val buttonsData : List<ButtonData>) {

    fun get(index: Int) : ButtonData {
        return buttonsData.get(index)
    }
}
ButtonsData.kt

レイアウト内で使用したときに正しく認識されるよう、getメソッドをオーバーライドする必要があります。

次に、activity_main.xml内の参照を変更します。

 <data>
        <variable name="buttonsData" type="com.tomerpacific.example.ButtonsData"/>
 </data>
変数名とクラスをButtonsDataに合わせて変更

そして、新しいデータクラスに対するバインディングを作成します。

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

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

        val firstButton : ButtonData = ButtonData("First", resources.getDrawable(R.drawable.android))

        val secondButton : ButtonData = ButtonData("Second", resources.getDrawable(R.drawable.android_p_logo))

        val thirdButton : ButtonData = ButtonData("Third", resources.getDrawable(R.drawable.android_studio_icon))

        val buttonsData : ButtonsData = ButtonsData(listOf(firstButton, secondButton, thirdButton))

        binding.buttonsData = buttonsData
    }
MainActivity.kt

ButtonDataクラスのインスタンスを3つ作成し、それらを格納したButtonsDataオブジェクトを生成して、バインディングオブジェクトにセットします。

最後に、レイアウト内でデータクラスを正しく活用できるようになりました。

 <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/linearLayout3"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView2">

            <ImageButton
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:adjustViewBounds="true"
                android:scaleType="centerInside"
                android:src="@{buttonsData.get(0).buttonImageSrc}">   // <-------

            </ImageButton>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:text="@{buttonsData.get(0).buttonText}"       // <--------
                android:textSize="16sp" />

        </LinearLayout>
要素の一つの使用例
Androidのデータバインディング入門:Data Binding Libraryでレイアウトとデータを結びつける方法
完璧に動作します

まとめ

本記事は、筆者が以下のアプリを開発した経験をもとに執筆しました。

Laundry Symbols(Google Play掲載アプリ)
洗濯表示の意味がわからなくて困ったことはありませんか?このアプリなら、見慣れないあの記号たちの意味を、使いやすいインターフェースと画像付きでわかりやすく解説してくれます。

Androidのデータバインディング入門:Data Binding Libraryでレイアウトとデータを結びつける方法 Androidのデータバインディング入門:Data Binding Libraryでレイアウトとデータを結びつける方法
Google Playストアから入手できます

ソースコード全体は以下のGitHubリポジトリで公開されています。

TomerPacific/LaundrySymbols
さまざまな洗濯表示とその説明を表示する、Kotlinで書かれたアプリケーションです。

Androidのデータバインディング入門:Data Binding Libraryでレイアウトとデータを結びつける方法 Androidのデータバインディング入門:Data Binding Libraryでレイアウトとデータを結びつける方法
完全なソースコードはこちらから確認できます
  1. Androidのデータ使用量を超過させない方法|「Check Data Usage」アプリの使い方を徹底解説

    インターネットはスマートフォンの生命線ですが、残念ながら無料ではありません。通信事業者(ISP)ごとにさまざまな料金プランが用意されており、国によっても内容は大きく異なります。世界中に無料Wi-Fiが行き渡る日が来るまでの間、私たちはモバイルデータの使用量を賢く管理し、上限超過を防ぐ必要があります。そこで役立つのが、「Check Data Usage」のようなデータ使用量管理アプリです。Systweak社が開発したこのアプリを使えば、モバイルデータとWi-Fi両方の使用量を監視できるだけでなく、帯域速度もリアルタイムで計測できます。 Check Data Usageとは?Androidのデータ

  2. Androidでモバイルデータが使えないときの対処法を徹底解説

    初めてスマートフォンを手にしたとき、モバイルデータは生活必需品になります。実際に通信ができなくなって初めて、Googleマップなしでは目的地にたどり着けず、ニュースアプリなしでは読むものがなく、メッセージアプリなしでは人と連絡が取れないことに気づくものです。この記事では、Androidのモバイルデータの問題を効率的に解決し、再びインターネットに接続できるようにするための実用的な方法を紹介します。 Androidでモバイルデータの問題を解決する方法 APN設定をリセットする すべての携帯電話会社は、アクセスポイント名(APN)を使ってスマートフォンをモバイルインターネットに接続しています。APN