3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Google CodeLabs Android Data Binding を日本語で概要解説

Posted at

Google Code Labsの日本語で概要解説の第3弾です。

過去記事はこちら

今回は、Data bindingをやります。ちゃんと勉強したことが無く他人のコードの見よう見まねでしかやったことがなかったので、改めて筋道立てて学んでみます。
元のCodeLabsはこちらの、Android Data Binding codelabです。

しつこいですが、日本語は大いに意訳、要約です(でも面倒になるとGoogleさんに力を借りた直訳風になりますwあと、口語になったり文語になったり)。必ず原文と照らし合わせながら参照してください。
日本語訳以外に、個人的に嵌まったところ(CodeLab内で触れていなくて罠になっているところ)も覚え書きしていきます。
斜体部分が、個人的な感想、メモ、意見、独り言です。(でも、最近気付きましたが、OS、ブラウザによっては斜体って見えないんですね・・・)

対象者

  • Java,Kotlinを読める
  • Android Architecture Components(以下AAC)のViewModel 、LiveDataについて何となく知っている
  • Androidのレイアウトxmlファイルを読める

CodeLab説明

1. Introduction(紹介)

Data Binding Library(Data Bindingライブラリ)

Data Binding Libraryは、レイアウト内のUIコンポーネントとデータ元とを結びつけるのを、プログラム的にではなく宣言的に書けるようにします。このコードラボでは、それを使うためのセットアップ方法と、レイアウトファイルの書き方、obsarvableなオブジェクトと結びつけたり、Binding Adapterをカスタマイズする方法などを学びます。

What you'll build(あなたがビルドするのは)

次のようなアプリをData Bindingを使ったものに変換していきます。

[画像は割愛します]

アプリは1つの画面を持ち、いくつかの固定データ、observableなデータを表示します。つまりデータが変われば、表示も変わらなければなりません。

データはViewModelによって提供されます。Model-View-ViewModel *(※MVVM)*は、Data Bindingととても相性が良いです。ダイアグラムを参照して下さい。

[画像は割愛します]

もし、AACのViewModelクラスにまだ馴染みが無い場合は、(公式ドキュメント)[https://developer.android.com/topic/libraries/architecture/viewmodel]を読んでください。概略を言うと、ViewModelはActivityはFragment
といったViewにUIの状態を提供するクラスです。ViewModelは画面回転でも生き残り、アプリの他のレイヤーに対してあたかもインターフェースかのように動作します。

What you'll need(推奨環境)

Android Studio 3.4以上

下記のプロジェクトをクローンし、実行してください。

$ git clone https://github.com/googlecodelabs/android-databinding

あるいは、サンプルプロジェクトをzipでダウンロドしてください。

  1. zipを解凍する
  2. 3.4以上のAndroidStudioでプロジェクトを開く。

[アプリを実行する手段については割愛します]

Likeボタンを押すとカウンターの数字がインクリメント(加算)され、Progress Barも更新されます。SimpleViewModelを使っています。中身を確認して下さい。

(Windowsの場合)Ctrl+Nでクラスを探すことが出来ます。Ctrl+Shift+Nではファイル名で検索することが出来ます。
Macの場合は、Command+Oでクラス検索、Command+Shift+Oでファイル検索が出来ます。

このショートカット情報はありがたい

SimpleViewModelは以下のデータを持っています。

  • ファーストネームとラストネーム
  • イイネの数
  • 人気度

また、onLike()で、イイネの数を加算します。

SimpleViewModelには興味をそそるような関数はありませんが、それは問題ではありません。一方で、PlainOldActivityにはたくさんの問題があります。

  • findViewByIdをたくさん使っている点。この関数の処理が遅いだけでなく、コンパイル時チェックが無いため安全ではありません。findViewByIdに渡したIDが不正な場合、アプリを実行したときにクラッシュします。
  • onCreateで初期値をセットしいる点。自動的にセットされるデフォルト値がある方が良いです。
  • android:onClickをレイアウトファイルで指定していますが、これも安全ではない。onLikeメソッドがActivityクラスに無かったり、renameしていたりした場合、アプリ実行時にクラッシュします。
  • コードが長い点。ActivityとFragmentはあっという間に肥大化していくので、なるべくたくさんのコードを外に出したいものです。また、AcitivityやFragmentの中のコードは、テストやメンテナンスがしづらいのも難点です。

Data Bindingライブラリを使うと、これらの問題を解決できます。ロジック部分をActivityから分離し、もっとテストがしやすく再利用しやすい場所に出すことが出来るのです。

3. Enable Data Binding and convert the layout(Data Bindingを有効にしてレイアウトを変更する)

このプロジェクトは、既にdata bindingが有効にされています。あなたのモジュールで使うときに最初にすることは、data bindingライブラリを使うように設定することです。

build.gradle
android {
...
    dataBinding {
       enabled true
    }
}

さあ、レイアウトをData Bindingなレイアウトに変えていきましょう。


通常のレイアウトをData Bindingなレイアウトに変えるに必要なステップは、次の通りです。

  1. レイアウトをタグで囲む
  2. レイアウト変数を追加する(任意)
  3. レイアウト式を追加する(任意)

plain_activity.xmlを開いてください。このレイアウトは、ごく標準的な、Constraint Layoutをルートに持つレイアウトです。

Data Binding向けのレイアウトに変えるには、まず、ルート要素を<layout>タグで囲む必要があります。同時に、namespace定義も(xmlns:で始まる属性)新しいルートに移動しなければなりません。

お手軽なことに、AndroidStudioで自動的にやってくれる方法があります。

※Const Layoutの開始タグのところで、クリックしてホバリングしていると、黄色い(オレンジかも?)電球アイコンが出るのでそれをクリックするか、Macならalt + Enterキーでこのメニューが出ます。つーかこれ、知らなかった!

レイアウトはこのようになります。

plain_activity.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto"
       xmlns:tools="http://schemas.android.com/tools">
   <data>

   </data>
   <androidx.constraintlayout.widget.ConstraintLayout
           android:layout_width="match_parent"
           android:layout_height="match_parent">

       <TextView
...

<data>タグを見て下さい。そこにレイアウト変数を入力します。

レイアウト変数は、レイアウト式で使われます。レイアウト式は、要素属性の値として使われ、@{expression}のフォーマットで指定されます。以下はサンプルです。

// Some examples of complex layout expressions
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'

レイアウト式言語はとてもパワフルですが、基本的な記述にだけにすべきです。でないと、レイアウトファイルがとても複雑になり可読性が下がってしまいます。メンテできる人も限られちゃうよ。


// Bind the name property of the viewmodel to the text attribute
android:text="@{viewmodel.name}"
// Bind the nameVisible property of the viewmodel to the visibility attribute
android:visibility="@{viewmodel.nameVisible}"
// Call the onLike() method on the viewmodel when the View is clicked.
android:onClick="@{() -> viewmodel.onLike()}"

完全なドキュメントはこちらを参照して下さい。

いくつかのデータをバインドしていきましょう!

4. Create your first layout expression(最初のレイアウト式を作る)

まず最初は、静的データのバインディングから始めましょう。

  • <data>タグ内に、2つのレイアウト変数を文字列で定義する
    <data>
        <variable name="name" type="String"/>
        <variable name="lastName" type="String"/>
    </data>
  • plain_nameというidのTextViewを見つけ、android:text属性ををレイアウト式を使って追加する
        <TextView
                android:id="@+id/plain_name"
                android:text="@{name}" 
        ... />

レイアウト式は、{}内に@で始めます。

nameは文字列なので、Data BindingはTextViewにどのように値をセットすれば良いかは知っています。他のレイアウト式の種類や属性については、後々学習しましょう。

  • plain_lastNameにも同様にセットする
        <TextView
                android:id="@+id/plain_lastname"
                android:text="@{lastName}"
        ... />

ここまでの結果は、plain_activity_solution_2.xml.で見ることが出来ます。

次にData Bindingを正しく反映するよう、Activityを変更します。

5. Change inflation and remove UI calls from activity(レイアウト反映コードを変更し、activityからUI呼び出しを削除する)

レイアウトの準備が出来たましたが、activityのコードも変更する必要があります。PlainOldActivityを開いて下さい。

Data Binding レイアウトにしたので、レイアウトの作成方法も少し変わります。

onCreateの、次のコードを、

PlainOldActivity.kt
setContentView(R.layout.plain_activity)

次のように変更します。

PlainOldActivity.kt
val binding : PlainActivityBinding =
    DataBindingUtil.setContentView(this, R.layout.plain_activity)

なぜbinding変数を作ったのでしょう? <data>ブロックで作ったレイアウト変数をセットする手段が必要だからです。それこそが、bindingオブジェクト役目です。Bindingクラスは、ライブラリによって自動的に生成されています。もし興味があるならば、PlainActivitySolutionBindingクラスを開き、中身を確認して下さい。簡単なコードです。

※PlainActivityBindingがなかなかimport出来ない場合は、Clean & Rebuild, Make Module等して、generatedJavaにBindingクラスが出来ているか確認して下さい。なお、Bindingクラスの命名は、 レイアウトファイル から行われます。

  • あとは、値をセットするだけ
PlainOldActivity.kt
    binding.name = "Your name"
    binding.lastName = "Your last name"

以上です。ライブラリを使って、データをバインドしました。

古いコードを削除出来ます。

  • updateName()メソッドを削除。Data Bindingは既にレイアウトのIDとセットする値を知っている
  • onCreateでのupdateName()呼び出しを削除

ここまでの実装結果は、PlainOldActivitySolution2にあります。

アプリを実行してみましょう。名前が"Ada"から変わったはずです。

※Adaってどこから来たw SampleViewModelでの初期化が、"Grace"ですので、多分そこが途中で変わったのかな(汗) とにかく、変わってます。

6. Dealing with user events(ユーザーイベントを扱う)

さしあたって、データをユーザーに見せる方法は分かりました。しかし、Data Bindingはユーザーイベントをも扱うことができ、レイアウト変数にアクションを発動させることが出来ます。

その前に、少しレイアウトを綺麗にしましょう。

  • 最初に、2つのレイアウト変数をViewModelのものに変更する。ほとんどの場合、これにより表示用のコードと状態が1箇所にまとめられる。
    <data>
        <variable
                name="viewmodel"
                type="com.example.android.databinding.basicsample.data.SimpleViewModel"/>
    </data>

変数に直接アクセスする代わりに、viewmodelのプロパティを使います。

  • 両方のTextViewのレイアウト式を変更する
        <TextView
                android:id="@+id/plain_name"
                android:text="@{viewmodel.name}"
... />
        <TextView
                android:id="@+id/plain_lastname"
                android:text="@{viewmodel.lastName}"
... />

"Like"ボタンへの反応もします。

  • like_buttonボタンを探し、次のコードを
android:onClick="onLike"

次のように置き換える。

android:onClick="@{() -> viewmodel.onLike()}"

最初のコードは、ActivityかFragmentに実装されたonLike()を呼び出すという、安全では無いものでした。(※Fragmentのは自動で呼べなかったような気がするが・・・経験上、Activityにないと必ずクラッシュしていたかと思う・・・) 正しいシグネチャのメソッドがどこにも定義されていないと、アプリはクラッシュしました。

新しい方法はより安全です。コンパイル時に*(関数があるか)*チェックされ、ラムダによってonLike()が実行時に呼び出されます。


補足内容
AndroidStudioで"Make Project"をすればData Bindingのエラーも見られるよ。


ここまでの実装結果は、plain_activity_solution_3.xmlで見ることが出来ます。

activityのコードから、もはや不要となったコードを削除しましょう。

  1. 下記のコードを、

    PlainOldActivity.kt
        binding.name = "Your name"
        binding.lastName = "Your last name"
    

    次のように変更する。

    PlainOldActivity.kt
        binding.viewmodel = viewModel
    
  2. onLikeメソッドを削除する。処理のバイパスは既になされている

ここまでの実装結果は、PlainOldActivitySolution3.ktに見ることが出来ます。

アプリを実行すると、ボタンをタップしても何も起こらないでしょう。updateLikes()が呼ばれなくなったからです。その実装をしていきましょう。

※ViewModelは画面回転で生き残ります。Activityは再生成されます。ということは、onCreateが呼ばれます。onCreate内では、updateLikes()を呼んでいるので、そこでLikesカウントやPopularity画像が変わっているのは確認できます。

7. Observing data(データを監視する)

前回、静的なデータのバインドを行いました。view modelクラスを見れば、namelastNameはただの文字列型だと分かるでしょう。それは変わることはないので、それで良いのです。一方、likesはユーザー操作により変更されます。

var likes =  0

この値が変更されたときに、UIを明示的に更新するのではなく、observable(観察可能)にします。


Data Bindingでは、observableなデータが変更されると、UIも自動的に更新されます。


データをobservableにするには、いくつかの方法があります。observableクラスや、observable fieldsもありますが、 ここではよりLiveDataの方が好ましいでしょう。詳細はこちらを見て下さい。

ObservableFieldsがより単純なので使用します。

※え!? どう見ても、下でLiveData使ってるけど!??! しかもその前にLiveDataが好ましい言うてるやん!? ということで、これはLiveDataの間違いかと思います。LiveDataが出てくる前の記述の修正漏れ?

次のコードを置き換えます。

SimpleViewModel.kt
    val name = "Grace"
    val lastName = "Hopper"
    var likes = 0
        private set // This is to prevent external modification of the variable.

このようにします。

SimpleViewModel.kt
    private val _name = MutableLiveData("Ada")
    private val _lastName = MutableLiveData("Lovelace")
    private val _likes =  MutableLiveData(0)

    val name: LiveData<String> = _name
    val lastName: LiveData<String> = _lastName
    val likes: LiveData<Int> = _likes

また、次の箇所も直します。

SimpleViewModel.kt
    fun onLike() {
        likes++
    }

    /**
     * Returns popularity in buckets: [Popularity.NORMAL],
     * [Popularity.POPULAR] or [Popularity.STAR]
     */
    val popularity: Popularity
        get() {
            return when {
                likes > 9 -> Popularity.STAR
                likes > 4 -> Popularity.POPULAR
                else -> Popularity.NORMAL
            }
        }

以下のようにします。

SimpleViewModel.kt
    // popularity is exposed as LiveData using a Transformation instead of a @Bindable property.
    val popularity: LiveData<Popularity> = Transformations.map(_likes) {
        when {
            it > 9 -> Popularity.STAR
            it > 4 -> Popularity.POPULAR
            else -> Popularity.NORMAL
        }
    }

    fun onLike() {
        _likes.value = (_likes.value ?: 0) + 1
    }

ご覧の通り、LivDataはsetValue()で値がセットされる必要があります*(※Kotlinではsetterはプロパティアクセスに書き換えられるので、_likes.value = XXX_likes.setValue(XXX)と同義)*。そして他のLiveDataに依存したLiveDataをTransformationsを使って定義することが出ます。この仕組みが、ライブラリが値が変わったときにUIを更新出来るようにしています。

LiveDataはライフサイクルを意識したobservableなオブジェクトなので、ライフサイクルオーナーを指定しなければなりません。これはbinding`オブジェクトに対して行えます。

PlainOldActivityクラスを開き(現時点で、このクラスはPlainOldActivitySolution3と同等になっているべきです)、ライフサイクルオーナーをbindingに設定します。

PlainOldActivity.kt
binding.lifecycleOwner = this

プロジェクトをリビルドすると、activityがコンパイルエラーになるでしょう。activityからlikesに直接アクセスしてしまっていますが、これはもう不要です。

PlainOldActivity.kt
    private fun updateLikes() {
        findViewById<TextView>(R.id.likes).text = viewModel.likes.toString()
        findViewById<ProgressBar>(R.id.progressBar).progress =
            (viewModel.likes * 100 / 5).coerceAtMost(100)
...

PlainOldActivityのprivate関数はすべて不要になったので、宣言と呼び出し箇所を削除します。activityクラスのコードが可能な限りシンプルになりました。

PlainOldActivity.kt
class PlainOldActivity : AppCompatActivity() {

    // Obtain ViewModel from ViewModelProviders
    private val viewModel by lazy { ViewModelProviders.of(this).get(SimpleViewModel::class.java) }

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

        val binding : PlainActivityBinding =
            DataBindingUtil.setContentView(this, R.layout.plain_activity)

        binding.lifecycleOwner = this // CodeLabsのコードはここが抜けているので必要です

        binding.viewmodel = viewModel
    }
}

※えーと、binding.lifecycleOwner = thisが抜けてますよね。必要です。

ここまでの実装を終えると、SolutionActivityと同じになっているはずです。

activityクラスからコードを外部へ移動していくことは、通常、メンテナンス性と可読性という意味で非常に効果的です。

TextViewをバインドして、イイネの数を、監視している数値を表示しましょう。plain_activity.xmlを次のようにしましょう。

plain_activity.xml
        <TextView
                android:id="@+id/likes"
                android:text="@{Integer.toString(viewmodel.likes)}"
...

アプリを実行して、ボタンをタップすると、期待通りに、カウントが増えていきます。

========================================================
重要補足コメント
そのままこの工程をやってくると、ここでビルドエラーになります。
SimpleViewModelの各変数をLiveDataにしたので、SimpleViewModelを参照していた、以下のクラスでコンパイルエラーになるのです。

  • PlainOldActivitySolution2
  • PlainOldActivitySolution3

上記のクラスからもすべてのprivate関数と呼び出し箇所を削除するか、SimpleViewModelをコピーしてSimpleNewViewModelとかにして、PlainOldActivityはそれを使うようにするとかすれば通るようになります。

ビルドしてないやろ、このプロジェクト作成者(汗)

========================================================

ここまでやってきたことを総括します。

  1. Nameとlast nameをただの文字列としてView Modelから表示
  2. ボタンのonClick属性を、ラムダ表記でView Modelとバウンド
  3. イイネの数をview modelのInt型のデータを監視し、TextViewと結びつけることで、変更があると自動的に表示が更新されるようにした

android:onClickandroid:textといった属性を使ってきました。それ以外の属性や、自作することにについて見ていきましょう。

8. Using Binding Adapters to create custom attributes(BindingAdapterを使ってカスタム属性を作る)

文字列データ(あるいはそのLiveData)をandroid:text属性ににバインドすると何が起こるかは明白ですが、では、それはどうやって行われているのでしょう?

Data Bindingライブラリでは、ほとんどのUI呼び出しは、Binding Adaptersと呼ばれるstaticメソッドによって行われます。

ライブラリは膨大な数のBinding Adaptersを提供しています。ここでチェックできます。android:text属性への例はこうなっています。

    @BindingAdapter("android:text")
    public static void setText(TextView view, CharSequence text) {
        // Some checks removed for clarity

        view.setText(text);
    }

また、android:backgroundであればこうなっています。

    @BindingAdapter("android:background")
    public static void setBackground(View view, Drawable drawable) {
        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
            view.setBackground(drawable);
        } else {
            view.setBackgroundDrawable(drawable);
        }
    }

Data Bindingにマジックはありません。すべてはコンパイル時に解決され、ジェネレーとされたコードで読むことが出来ます。

progress barについて実装していきましょう。以下の動作にしたいものです。

  • イイネが無ければ非表示にする
  • 5イイネで満タンにする
  • 満タンで色を変える

これのためにカスタムBinding Adapterを作成していきます。

utilパッケージのBindingAdapters.ktを開いて下さい。ファイルはパッケージのどこにあっても問題ありません。ライブラリが見つけてくれます。Kotlinにおいては、staticメソッドはファイルのトップレベルに定義するか、クラスの拡張関数として定義することが出ます。

最初の問題を解決するための、Binding Adapterを見て下さい。それはhideIfZeroです。

BindingAdapters.kt
    @BindingAdapter("app:hideIfZero")
    fun hideIfZero(view: View, number: Int) {
        view.visibility = if (number == 0) View.GONE else View.VISIBLE
    }

このアダプターは、以下のことをします。

  • app:hideIfZero属性に当てはめる
  • すべてのViewに適用できる(最初の引数がViewクラスだから。この型を変えることで限定することも可能)
  • レイアウト式が返すべき値として、整数を取る
  • 値がゼロならViewのvisibilityをGONEに設定する。そうで無ければVISIBLEを設定する

plain_activityレイアウトで、progress barを探してhideIfZero属性を追加しましょう。

plain_activity.xml
    <ProgressBar
            android:id="@+id/progressBar"
            app:hideIfZero="@{viewmodel.likes}"
...

アプリを実行すると、Likeボタンを初めて押したときにprogress barが現れるのを見て取ることが出来ます。でも、まだ値や色を変える必要があります。

ここまでの実装の完成形は、plain_activity_solution_4.xmlにあります。

9. Create a Binding Adapter with multiple parameters(複数の引数を取るBinding Adapterを作る)

(Progress Barへの)進捗値のために、最大値とイイネの数を取るBinding Adapterを使いましょう。BindingAdaptersを開き下記のコードを探して下さい。

BindingAdapters.kt
/**
 *  Sets the value of the progress bar so that 5 likes will fill it up.
 *
 *  Showcases Binding Adapters with multiple attributes. Note that this adapter is called
 *  whenever any of the attribute changes.
 */
@BindingAdapter(value = ["app:progressScaled", "android:max"], requireAll = true)
fun setProgress(progressBar: ProgressBar, likes: Int, max: Int) {
    progressBar.progress = (likes * max / 5).coerceAtMost(max)
}

このBinding Adaptersは、すべての属性が揃っていないと、使われません。これはコンパイル時に起こります。メソッドは3つの引数を取っています。アノテーションで定義された属性の数がViewに適用されます*(※ここの英文意味不明なんですが・・・)*

requireAllはbinding adapterが使われたときの動作を決定します。

  • trueにすると、すべての属性がXMLで定義されている必要がある
  • falseにすると、未定義の属性はnullか、booleanの場合はfalse, プリミティブ型の場合は0として扱われる

XMLに次の属性を追加しましょう。

        <ProgressBar
                android:id="@+id/progressBar"
                app:hideIfZero="@{viewmodel.likes}"
                app:progressScaled="@{viewmodel.likes}"
                android:max="@{100}"
...

progressScaled属性を、イイネの数ででバインドしました。maxにはただの数値を渡しています。@{}フォーマットを使わないと、Data BindingはどのBinding Adapterを使えばいいか分からなくなります。

ここまでの実装結果は、plain_activity_solution_5.xmlにあります。

アプリを実行すると、progress barが期待通りに動作しているのを見られます。

10. Practice creating Binding Adapters(Binding Adapters作成の練習)

練習は大事だよ。次の物を作ろう。

  • イイネの数でprogress barの色を変え、対応する属性をバインドするBinding Adapter
  • popularityに応じて異なるアイコンを表示するBinding Adapter
  • ic_person_black_96dpを黒で
  • ic_whatshot_black_96dpをライトピンクで
  • ic_whatshot_black_96dpをゴールドピンクで

答えは、BindingAdapters.ktファイル、SolutionActivity、そしてsolution.xmlレイアウトにあります。

※BindingAdapters.ktに、必要なメソッドは用意されているので、実際はBindAdapterを作る必要がありません。また、この段階でActivityには追加/変更するコードはありません。まあつまり編集する必要があるのはxmlだけです。

11. You're bound to succeed!(あなたの成功は約束された!)

おめでとうございます! codelabを完了したあなたは、Data Binding レイアウト、変数、そして式を使う方法を知ったはずです。observable dataを使い、カスタム属性とBinding AdapterとでXMLレイアウトをもっと重要なものとすることも。

もっと知りたい方は、サンプルドキュメントで学んで下さい。

最後に

感想

やってみると意外に簡単ですね。食わず嫌いはやっぱりダメですね^^;

特にViewModelのLiveDataでの使い方は、今後それらが主流になることを考えると、おおいに参考になりそうです。

あまり複雑な式は書かない方が良さそうですが、積極的に使っていこうと思います。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?