【Android】はじめてのDataBinding
前回の記事:【Android】分かった気になれる!アーキテクチャ・MVVM概説 では
アーキテクチャの概要をコードを記述せずに概念のみ説明してみました。
今後は各ライブラリを実際にコードを書いて導入してみることで
アーキテクチャと各種ライブラリを理解し、身につけていきたいと思っています。
このアーキテクチャシリーズ実践編第一回目として
今回は、Googleが提供するJetPackに含まれるDataBindingライブラリを
実践しながら簡単に説明していきたいと思います。
だらだらと書くのも楽しくないので、
今回は私が愛して止まないアメリカンフットボールのルール・魅力を
間に挟みながらDataBindingを説明していきたいと思います。
気になったワードがあれば、ぜひ調べてみてくださいね
はじめに
本記事では、DataBindingとLiveDataを用いて添付のGIF画像のような挙動を実装しようと思います。
この画面の特徴は以下の通りです。
- Buttonタップ時
- EditTextに入力されたテキストがTextViewに表示される
- EditTextに何も入力されていないとき
- Buttonは押せない
- テキストは「Ready」
- EditTextにテキストが入力されているとき
- Buttonは押せる
- テキストは「Set!!!」
本記事では、Buttonに関するDataBindingに注目していきます。
実装したいこと
これに似た画面には以下のようなものが挙げられます。
- ユーザーIDとパスワードが入力されている場合のみボタンを押せるようなログイン画面
- 利用規約にチェックされている場合のみ次に進めるような画面
簡単な環境
AndroidXを導入したプロジェクトで実装を行いました。
これは、プロジェクトを作成するときにUse androidx.* artifacts
にチェックを入れるか
Refactor > Migrate to AndroidX...
することで対応できると思います。
AndroidXについてはこちらを参照:
AndroidX の概要 : Google公式
今回の実装ではそれほど影響はないと思われますが
ハマってしまうかもしれないので。
(おおよそデフォルトになっていると思いますが、念のため)
元のコード
今回はDataBindingの使い方を分かりやすくするために
ごく簡単なレイアウトを用いて説明したいと思います。
MainActivity
は、FrameLayoutを載せただけのシンプルなレイアウトです。
そのFrameLayoutにFootballFragment
と言うFragmentを表示することにします。
(
ちなみに、アメフトはAmerican Footballの略です。
Footballといえば、
多くの国ではSoccerのことを指しますが
アメリカではAmerican Footballのことを指すのが一般的です。
日本開催で大いに盛り上がったラグビーも実は
正式名称Rugby Footballなので、Footballの一種です。
)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction()
.replace(R.id.frameLayout, FootballFragment())
.commit()
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<FrameLayout
android:id="@+id/frameLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
FootballFragment
は以下のコードのように
LinearLayoutにTextView・EditText・Buttonを配置したシンプルなレイアウトです。
ところどころ見た目を改善する処理は記述していますが。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="32dp">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ここに出力されます" />
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:hint="アメフトのルールや魅力は?"
android:textAlignment="center" />
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Ready" />
</LinearLayout>
textをstringで定義していないなど細かいことは置いといて
DataBindingだけに集中してもらえればと思います。
DataBindingの導入
さて、いよいよDataBindingを導入します。
以下の項目の順に実装していきます。
- appレベルのbuild.gradle
- fragment_football.xml
build.gradle(Module:app)に関して
appレベルのbuild.gradle
にて、
以下のコードを記述しDataBindingを有効にします。
android {
// 略
dataBinding {
enabled true
}
}
さらに、Kotlinで開発しているのであれば下図で示されるようなサジェストが出るので
トップレベルにapply plugin: 'kotlin-kapt'
を追加した上で
Sync Now
で同期し対応しておきます。
<img width="684" alt="スクリーンショット 2019-11-04 18.51.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296819/2f36eb3c-0bae-8b36-9610-0a0c58cf6b62.png">
これで、DataBindingの導入は終わりです。簡単ですね。
アメフトで言えば、キックオフリターンが終了しリターンチームが攻撃を開始する頃でしょうか。
さて次は、レイアウトファイルをDataBindingで使える形に変換していきます。
レイアウトファイルに関して
DataBindingを有効化した後
レイアウトファイルを、DataBindingを使用できる状態にします。
元のレイアウトファイル
<LinearLayout xmlns:android="http://schemas.androidd.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<!--略-->
</LinearLayout>
レイアウトファイルにおいて
親のView(ここではLinearLayout)にカーソルを当て
option + Enter
を押すと下図のようなダイアログが表示されます。
<img width="524" alt="convert_to_databinding.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296819/9bd611be-8196-422f-ad8a-0c3dfe6b43e5.png">
このConvert to data binding layout
を選択すると
レイアウトがDataBinding用に変換されます。
DataBinding用に変換したレイアウトファイル
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<!--略-->
</LinearLayout>
</layout>
元のレイアウトファイルと変換後のレイアウトファイルの比較をまとめると以下のようになります。
元のレイアウトファイル | 変換後のレイアウトファイル | |
---|---|---|
親View | LinearLayout | layout |
dataタグ | なし | layoutとLinearLayoutの間 |
これでDataBindingの基本的な導入は完了しました。
本当に完了でしょうか?
いいえ、まだ残っています。アメフトで言えば1Q残り2分ですね。
最後にビルドを実行し、DataBinding用を自動生成して完了です。
これでひとまず、1Qは終了です。
CMです。
(アメフトの試合は 1Q: 15分で4Qまで、計60分で行われます。
が、ゲーム中時間は止まるため実際は3,4時間かかります。長丁場なんですね。)
レイアウトファイルとコードの結びつけ
次は実際に、レイアウトファイルとコードを紐づけていきます。
まず、レイアウトファイルに関して。
レイアウトファイルに関して
先ほどのレイアウトファイルにおいて
dataタグ内にDataBindingしたい変数を定義していきます。
name: 変数名
type: 型
となっており、typeにはStringやIntなどはもちろん
独自で作成したクラスも入れることができます。
その場合は階層からクラス名を指定する必要があります。:
e.g. type="com.hoge.QB"
, type="com.hoge.Audible"
, ...
<data>
<variable
name="buttonText"
type="String" />
</data>
このように定義された変数は、以下の役割を担うことができるようになります。
- 外部ファイルから対象の値を取得
- 外部ファイルに対象の値を渡す
- レイアウトファイルから対象の値を取得
- レイアウトファイルに対象の値を渡す
つまり、別ファイルとこのレイアウトファイルの両方からアクセスできるようになります。
通常は、外部ファイルから取得した値をレイアウトファイルに単一方向に渡すことが多いです。
レイアウトファイルから外部ファイルに値を渡す場合のDataBindingは双方向DataBindingと呼ばれ
処理が異なったり、追加で処理が必要になったりします。
ちなみに今回は、双方向DataBindingは行いません。
次に、定義した変数を代入します。
@{name}
のように記述するだけでDataBindingを実装できます。
例えば、android:text="@{buttonText}"
と言った具合です。
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{buttonText}"
tools:text="Ready" />
これで、button
のandroid:text
とbuttonText
が結びつきました。
つまり、button
のandroid:text
の値の設定を
buttonText
を通すことで別ファイルのコード上でも設定できるようになりました。
ここで、こんな疑問を持った方はいるのではないでしょうか?
そんなことしなくても、適当なKotlinファイル内でbutton.text = "Ready, Set Hut!!!"
のように記述すれば値をセットできるのではないか、と。
その通りです。
こんなことしなくても別ファイルからコード上で値をセットすることは可能です。
しかし、こうすることで格段と値の管理が簡単になる場合があるのです。
それはユーザー操作などにより変更された値を
即座にView
に反映させたい場合などです。
変更された値の反映をコード上で実装しようとすると
記述量が増えコードが煩雑になってしまう可能性があります。
それが、後述するLiveDataとともにDataBindingを利用することで
より簡潔にスマートにコードを書けるようになるのです。
その実装は後ほど、後半戦で実装します。
別ファイルのFragmentクラスにおいて
次に、DataBindingを適用したレイアウトファイルをFragmentクラスに結びつけます。
onCreateViewでレイアウトファイルと結びつけます。
DataBindingを実装しない場合
class FootballFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_football, container, false)
}
}
DataBindingを実装しない場合、通常であれば上記のように実装すると思われます。
inflater
でレイアウトファイルを指定しinflate
することで
レイアウトファイルとFragmentクラスのコードを結びつけています。
一方、DataBindingを用いた場合の実装例は下記の通りです。
DataBindingを実装する場合
class FootballFragment : Fragment() {
private lateinit var binding: FragmentFootballBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentFootballBinding.inflate(inflater, container, false)
return binding.root
}
}
上記で示されるFragmentFootballBinding
クラスは
ViewDataBinding
クラスを継承したクラスで、
実は、レイアウトファイルでDataBindingを実装しビルドした際に作られていました。
それをここではbinding
として定義し、
FragmentFootballBinding
のinflate
メソッドを用いてインスタンス化しています。
そして、onCreateViewの戻り値にbinding.root
を指定することで
FootballFragment
のレイアウトを設定することができます。
詳しくは、こちらを参照:
Generated binding classes : Google公式ガイド
これで、ひとまずレイアウトを設定することができました。
アメフトで言えば、前半残り2分:2 minutes warning
あたりでしょうか。
DataBindingしてみよう!
ここで、binding
を使ってテキストをセットしてみましょう!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonText = "Crouch, Bind, Set!!" // これはラグビー
}
このようにbinding.buttonText
とすることで
レイアウトファイルで定義したString
クラスのbuttonText
を呼び出し、値をセットできます。
それにより、レイアウトファイルのbutton
におけるandroid:text
に
buttonText
の値Crouch, Bind, Set!!
がセットされます。
(ただ、この程度の実装では、前述の通りbutton.text
を呼び出すだけでいいと思います)
以下の実行結果では、ButtonにCrouch, Bind, Set!
のテキストが表示されていることが確認できます。
(Buttonのテキストがデフォルトで大文字なので大文字にはなっていますが)
実行結果
LiveDataを実装
後半戦キックオフ。
次にお待ちかね(かどうかは分かりませんが)、LiveDataを実装します。
以下の順で実装していきます。
- ViewModelの作成
- 各種LiveDataの作成
- レイアウトファイルにViewModelを導入
- LiveDataの更新メソッドを作成
- FragmentクラスにViewModelを導入
ViewModelとは? LiveDataとは? MVVMとは? と言う方はこれらを参照してみてください
- 【Android】分かった気になれる!アーキテクチャ・MVVM概説 : 過去の記事
- Android Architecture Components : Google公式ガイド
- ViewModel : Google公式ガイド
- LiveData : Google公式ガイド
- MVVM : Google公式ガイド
ViewModelの作成・LiveDataの作成
早速、FootballViewModel
を作成します。
(今回は、ViewModel()
を継承しないLiveDataを保持するだけのViewModelを作成します。
ViewModel()
を継承する場合はViewModel生成の際に別途処理が必要になります。)
その後、変更を検知したいデータに対して、MutableLiveData、およびLiveDataを作成します。
LiveDataは、値がセットされたときに値がセットされたことを検知できるクラスです。
実際に値をセットされるのはMutableLiveDataで、
LiveDataに直接値をセットされることはありません。
MutableLiveDataは、その名の通り値の変更が可能なLiveDataで、
LiveDataの値を取得するとMutableLiveDataの値が返されるよう実装します。
また、ここでは下記のコードのようにスコープ関数を用いて初期値を設定しています。
下記コードはFootballViewModel
の一部を抜粋したものです
class FootballViewModel {
// 略
private val _buttonText: MutableLiveData<String> =
MutableLiveData<String>().also { mutableLiveData ->
mutableLiveData.value = "Ready"
}
val buttonText: LiveData<String>
get() = _buttonText
}
レイアウトファイルにViewModelを導入
レイアウトファイルのdataタグで定義した変数buttonText
を削除し、vm
(ViewModel)を変数に設定します。
また、button
にて設定していた@{buttonText}
は@{vm.buttonText}
に変更し、
ViewModelを経由してbuttonText
の値を取得するよう変更します。
(少し疲れてきました。頭を整理するため、タイムアウトを取ります。。)
変更前
<data>
<variable
name="buttonText"
type="String" />
</data>
<LinearLayout>
<!--略-->
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{buttonText}"
tools:text="Ready" />
</LinearLayout>
変更後
<data>
<variable
name="vm"
type="io.github.itakahiro.databindingfootball.FootballViewModel" />
</data>
<LinearLayout>
<!--略-->
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{vm.buttonText}"
tools:text="Ready" />
</LinearLayout>
LiveDataの更新メソッドを作成
次に、LiveDataの値を更新するメソッドを作成します。
fun updateButton(isBlank: Boolean) {
_isEnabled.value = !isBlank
if (!isBlank) {
_buttonText.value = "Set!!!"
} else {
_buttonText.value = "Ready"
}
}
このメソッドでは、buttonの状態を以下の2項目に関して更新しています。
-
buttonText
: ボタンのテキスト -
isEnabled
: ボタンを押下できるかどうか(Button.isEnabled
)
buttonText
に関して。
_buttonText
の値(間接的にはbuttonText
の値でもある)を
EditTextにテキストが入力されているかどうか(メソッドの引数:isBlank
)によって変更しています。
isEnabled
に関して。
記載しておりませんでしたが
_isEnabled
, isEnabled
をViewModel内で定義しています。
これはButtonがenableかどうかと言う情報を持つ
MutableLive、およびDataLiveDataです。
(
LiveDataのsetterには
setValue(Kotlinでは、.value)とpostValueが用意されています。
setValueはメインスレッド(UIスレッド)のみでしかセットされず、
もし、別スレッドでLiveDataの値がセットされたとしても
検知できないと言うことらしいです。
参照:LiveData : Google公式リファレンス
)
FragmentクラスにViewModelを導入
さて、3Qも終わりいよいよ4Qです。
時間運びがより一層大事になってきますね。
FootballFragment
にFootballViewModel
を導入します。
下記コードは、FootballFragment
のコードです。
class FootballFragment : Fragment() {
private lateinit var binding: FragmentFootballBinding
private val viewModel = FootballViewModel()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentFootballBinding.inflate(inflater, container, false)
binding.vm = viewModel
binding.lifecycleOwner = this
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.isEnabled.observe(viewLifecycleOwner, Observer { isEnabled ->
binding.button.isEnabled = isEnabled
})
binding.editText.addTextChangedListener { text ->
viewModel.updateButton(text.isNullOrBlank())
}
}
まず、プロパティにviewModel
を定義します
今回作成したViewModelはViewModel()
を継承していないため
FootballViewModel()
でインスタンス化できます。
(ViewModel()を継承する場合の実装はこちらを参照:
Implement a ViewModel: Google公式ガイド)
private val viewModel = FootballViewModel()
次に、onCreateView()
にてbinding
の設定を追加します。
追加したのは下記の部分です。
binding.vm = viewModel
binding.lifecycleOwner = this
まず1行目について。
binding
: FragmentFootballBinding のvm
、
つまりレイアウトファイルにてdataタグ内に設定したvm
に
インスタンス化したViewModelを代入してあげます。
次に2行目について。
これを追加してlifecycleOwner
を設定しておかないと
LiveDataが値の更新を検知することはできません。
コメントアウトして実行してみると挙動が分かると思います。
-- 後半4Q, 2 minutes warning
最後に、onViewCreated
にて、
Observer
とEditTextのリスナーを設定します。
まず、Observer
について。
以下に添付のコードのように、viewModel
内のLivedataを指定することで
LiveDataの値が更新された際に検知することができます。
これは、同じ値がセットされたとしても検知されます。
今回の場合、viewModel
で定義した
isEnabled
というLiveDataの値が更新された時に呼び出され、
Button.isEnabled
の値に代入されます。
つまり、データの更新とViewの更新が同期されます
また、このObserver
の処理はDataBindingでできますが
実装方法を記載するために実装しています。
viewModel.isEnabled.observe(viewLifecycleOwner, Observer { isEnabled ->
binding.button.isEnabled = isEnabled
})
次に、EditTextのリスナーを設定します。
以下の添付のリスナーは、editText
のテキストが変更されたとき
つまりテキストを入力・削除したときに呼ばれます。
このときのeditText
にテキストが入力されているかどうかを取得し
viewModel
のメソッドに引数として渡します。
前述のこのメソッド内でボタンに関するデータの更新を行います。
binding.editText.addTextChangedListener { text ->
viewModel.updateButton(text.isNullOrBlank())
}
これで試合終了です。
最後に、全体のコードを添付して終わりにしたいと思います。
記事内では記載していなかった部分もあります。
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="vm"
type="io.github.itakahiro.databindingfootball.FootballViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="32dp">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{vm.submittedText}"
tools:text="ここに出力されます" />
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:hint="アメフトのルールや魅力は?" />
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="@{vm.isEnabled}"
android:text="@{vm.buttonText}"
tools:text="Ready" />
</LinearLayout>
</layout>
class FootballFragment : Fragment() {
private lateinit var binding: FragmentFootballBinding
private val viewModel = FootballViewModel()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentFootballBinding.inflate(inflater, container, false)
// このようにしても良い
// binding = DataBindingUtil.inflate(
// inflater, R.layout.fragment_football, container, false
// )
binding.vm = viewModel
binding.lifecycleOwner = this
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.editText.addTextChangedListener { text ->
viewModel.updateButton(text.isNullOrBlank())
}
binding.button.setOnClickListener {
val text = binding.editText.text.toString()
viewModel.submitText(text)
}
// この部分はDataBindingで実装.
// 代わりに、レイアウトファイルのandroid:enabled="@{vm.isEnabled}"の行で実装できる!
// viewModel.isEnabled.observe(viewLifecycleOwner, Observer { isEnabled ->
// binding.button.isEnabled = isEnabled
// })
}
}
class FootballViewModel {
private val _submittedText = MutableLiveData<String>().also { mutableLiveData ->
mutableLiveData.value = "ここに、出力されます"
}
val submittedText: LiveData<String>
get() = _submittedText
private val _isEnabled: MutableLiveData<Boolean> =
MutableLiveData<Boolean>().also { mutableLiveData ->
mutableLiveData.value = false
}
val isEnabled: LiveData<Boolean>
get() = _isEnabled
private val _buttonText: MutableLiveData<String> =
MutableLiveData<String>().also { mutableLiveData ->
mutableLiveData.value = "Ready"
}
val buttonText: LiveData<String>
get() = _buttonText
fun updateButton(isBlank: Boolean) {
_isEnabled.value = !isBlank
if (!isBlank) {
_buttonText.value = "Set!!!"
} else {
_buttonText.value = "Ready"
}
}
fun submitText(text: String) {
_submittedText.value = text
}
}
まとめ
DataBindingに触れる機会があったので
今回記事にしてまとめてみました。
ご意見ご感想あればコメントいただけると幸いです。
参考にした資料
- Android Data Binding codelab
- LiveData : Google公式リファレンス
- Generated binding classes : Google公式ガイド
- AndroidX の概要 : Google公式
Discussion