DataBindingってよく聞くけどそもそもなに?
DataBindingの使い方がいまいちわかってないな
こんな疑問を解決する記事になります。
簡単なサンプルアプリを用意していますので、実際に自分の手で動かして動きをつかんでみてください。
本記事ではサンプルアプリを作りながら大きく以下の3点を説明します。
- データバインディングとはどんなものか
- データバインディングのメリット
- データバインディングの使い方(サンプルアプリを作りながら)
データバインディングを使ったことがない方や、いまいち理解できていないといった方は是非記事をご覧ください。
データバインディングとは
データバインディング(DataBinding)とは、レイアウト内のコンポーネントに対してデータソースを定義することでバインドしてくれるライブラリです。
これだけだと何を言っているのかわからないと思うので例を上げます。
例えば、LiveDataの使い方について書いた記事 でカウント数を表示させるときに、Fragmentで以下のコードを書きました。
val countMessageView = view.findViewById(R.id.countMessageView)
viewModel.countMessage.observe(viewLifecycleOwner, Observer { countMessage ->
countMessageView.text = countMessage
})
findViewById で特定のViewを取得してから、LiveDataのcountMessageプロパティを監視し、通知がきたらその内容でViewを更新するといったコードになります。
データバインドを使用することでこれが不要になります。
一部抜粋ですが、データバインディングを使用することで、レイアウトファイルで以下のようにするとtextの内容を更新できるようになります。
<androidx.appcompat.widget.AppCompatTextView android:text="@{viewmodel.countMessage}" />
メリット
メリットを2つ上げます。
- findViewByIdが不要
- ActivityやFragmentの見通しが良くなる
findViewByIdが不要
データバインディングを使用することで findViewById が不要になります。
先程の説明では、
「例えば、ボタンにクリックイベントを追加したいとき使うでしょう?」
と思われると思いますが、不要です。
データバインディングを使うことで、Bindingクラスが自動生成されます。
このBindingクラスを使用することで、findViewById をせずコンポーネントにアクセスできるようになります。
後述するFragmentの処理を作成の箇所を見るとわかります。
※また、クリックイベントもレイアウトファイルでバインドも可能です。
ActivityやFragmentの見通しが良くなる
これは findViewById を使わなくなることによる副次的効果になります。
findViewById がなくなることで、ボイラープレートコードを減らすことができます。
また、findViewById した後はメンバー変数として参照しておいたりすると思います。
これも無くすことができるので、ActivityやFragmentのソースコードの見通しがよくなります。
サンプルアプリを作る
実装を進めるときには、依存がない方から実装をした方がエラーにならずに進めやすいです。
そのため、Model → ViewModel → Viewの順番のが進めやすいです。
なので、基本的にはこの順番で説明を進めます。
開発環境
サンプルアプリ作成時の開発環境は以下です。
- macOS Catalina 10.15.3
- Android Studio 3.5.3
- Kotlin 1.3.50
また、今回作成したサンプルアプリのソースコードは githubのDataBindingSample に置いてあります。
ぜひ動かしてみてください。
作成するデータバインディングのサンプルアプリ
今回作成するサンプルアプリでは、データバインディングを使ってユーザ情報を表示するアプリを作ります。
画面下部のスピナーでユーザを選択すると、そのユーザ情報が画面上部に表示されるようにします。
画面で表示するユーザ情報は以下です。
- ユーザアイコン
- ユーザ名
- ユーザID
- ユーザのお気に入り状態 ※ユーザによって表示・非表示を切り替える
これらの情報をデータバインディングを使って表示切り替えします。
gradleに定義を追加
データバインディングを使うために以下の定義を追加していきます。
app直下の build.gradle に追加しています。
apply plugin: 'kotlin-kapt'
android {
dataBinding {
enabled = true
}
}
kotlinで開発している環境では apply plugin: ‘kotlin-kapt’ が必要になります。
ModelとViewModelを追加
今回はMVVMの形に近づけて実装します。
そして、リポジトリ(Repository)パターンを使っていきます。
リポジトリパターンを使うことで、リポジトリを使用する側からは、どこからデータを取得するのか(サーバやローカルデータベースなど)を意識することなく使用することができます。
そのため、リポジトリではサーバからデータを取得したり、データベースからデータを取得したり、よしなにハンドリングする必要があります。
これにより、仕様変更等でデータの取得元を変更しなければならなくなったとき、リポジトリ内の処理を変更するだけだけで、ViewModelは取得方法を変更する必要がなくなるのです。
ということで、リポジトリとユーザ情報を持つデータソースを追加します。
※今回はデータバインディングがメインのため説明は省きます。(コードはこちらです。)
続いてViewModelを追加します。
class UserViewModel : ViewModel() {
private val repository: IUserRepository = UserRepository.getInstance()
private val userId = MutableLiveData()
val user: LiveData = Transformations.switchMap(userId) { userId ->
repository.getUser(userId)
}
fun onItemSelected(item: String) {
userId.value = item
}
}
ViewModelの処理はこれだけです。
公開していのは user プロパティと onItemSelected メソッドだけです。
user プロパティは画面に表示する現在のユーザ情報を持ちます。
onItemSelected はスピナーで選択されたアクションを受け、LiveDataの userId にデータをセットします。
これに反応して、リポジトリから指定された userId のユーザ情報を取得してTransformations.switchMap を用いて変換しています。
「なぜ Transformations.switchMap を使っているのか?」
「onItemSelected でLiveDataを戻り値にすれば良いのではないか?」と思われるかもしれません。
fun onItemSelected(item: String) : LiveData {
return repository.getUser(userId)
}
これでも動かすことはできます。
しかし、これだとActivityやFragmentで以下のようにしなければならなくなります。
- ユーザ情報を持つLiveDataを監視
- スピナーでユーザが選択されたのでLiveDataの監視を解除
- onItemSelectedを呼び出す
- 再度取得できたLiveDataを監視する
しかし、Transformations.switchMap を使うことで、ActivityやFragmentでは onCreate や onViewCreated で一度LiveDataを監視したら、それを監視し続けるだけでよいのです。
- ユーザ情報を持つLiveDataを監視
Transformations.switchMap の内部の処理を見ていくとわかるのですが、外から監視されるのはLiveDataを継承したMediatorLiveDataになっています。
このMediatorLiveDataがデータソースとなるLiveDataを切り替えてくれているのです。
そのため、repository.getUser(userId) をして違うLiveDataが返されても次々にデータソース切り替えているので問題がないのです。
レイアウトの作成
ここからがデータバインディングの本格的な部分になります。
まずは、<layout>タグをルートのタグにします。
そして、タグの中に以下のようにバインドさせるためのデータホルダーを定義します。
<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>
<variable name="viewmodel"
type="com.tf.android.databindingsample.viewmodel.UserViewModel">
</variable>
</data>
<androidx.constraintlayout.widget.constraintlayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.appcompatimageview
android:id="@+id/iconImageView"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginstart="150dp"
android:layout_margintop="30dp"
android:layout_marginend="150dp"
app:layout_constraintend_toendof="parent"
app:layout_constraintstart_tostartof="parent"
app:layout_constrainttop_totopof="parent" />
...
</androidx.constraintlayout.widget.constraintlayout>
</layout>
あとはタグの中に通常通りコンポーネントを配置してレイアウトを作成していくだけです。
まずは一旦ここまでをやりましょう。
Fragmentの処理を作成
一度ここでビルドをしておきます。
ビルドをすることで、レイアウトxmlを元にBindingクラスが自動生成されます。
このとき、xmlで自動生成されるクラス名前を指定していない場合は、xmlのファイル名を元に生成されます。
今回、fragment_user.xmlでデータバインディングするように定義したため、FragmentUserBindingというクラスが自動生成されます。
以下のようにクラスが自動生成されます。
- fragment_user.xml → FragmentUserBinding
- activity_main.xml → ActivityMainBinding
FragmentUserBindingを使ってレイアウトの適用と、コンポーネントへのアクセス処理を作ります。
class UserFragment : Fragment() {
private lateinit var binding: FragmentUserBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentUserBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val viewModel = ViewModelProviders.of(this)[UserViewModel::class.java]
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
val adapter = ArrayAdapter(
requireContext(),
R.layout.common_spinner,
resources.getStringArray(R.array.user_list)
)
binding.selectUserSpinner.apply {
setAdapter(adapter)
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
val spinner = parent as AppCompatSpinner
val str = spinner.selectedItem.toString()
viewModel.onItemSelected(str)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// Do nothing
}
}
}
}
}
このFragmentのコードで大事な部分が以下です。
- FragmentUserBinding.inflate(inflater, container, false)
- binding.lifecycleOwner = viewLifecycleOwner
- binding.viewModel = viewModel
- binding.selectUserSpinner
FragmentUserBinding.inflate(inflater, container, false)
これで、Bindingクラスを取得しています。
DataBindingUtilを使った取得方法も可能です。
DataBindingUtil.inflate(inflater, R.layout.fragment_user, container, false)
binding.lifecycleOwner = viewLifecycleOwner
忘れやすいのですがこれも非常に大事です。
これを忘れると、LiveDataのデータをバインドする際にうまく通知されません。
binding.viewModel = viewModel
レイアウトで定義したviewModelに実際のviewModelの参照を渡します。
これで、レイアウトで定義したデータモデルと実際のviewModelインスタンスをつないでいます。
binding.selectUserSpinner
findViewById をせずにレイアウトで定義したコンポーネントにアクセスできるようになります。
このプロパティは、レイアウトで定義した android:id を元に作成されます。
バインディング
コンポーネントとバインドさせていきます。
@{} という形式で記述することでバインドできます。
ユーザ名を表示させる場合は以下だけで済みます。
<androidx.appcompat.widget.AppCompatTextView android:text="@{viewModel.user.name}" />
アイコンの表示とビューの表示非表示については簡単にできません。
そのため、独自にBindingAdapterを作ります。
※他にも方法がありますが、色々な記述方法についてはまた別途解説しようと思います。
これにより、自分の好きなように処理を作れます。
アイコンの表示のために、画像を切替える処理とビューの表示非表示を切り替えるための処理を作ります。
object CommonBindingAdapter {
@JvmStatic
@BindingAdapter("app:goneUnless")
fun goneUnless(view: View, visible: Boolean) {
view.visibility = if (visible) View.VISIBLE else View.GONE
}
@JvmStatic
@BindingAdapter("app:iconId")
fun setIconById(view: AppCompatImageView, iconId: Int) {
val resourceId = when (iconId) {
1 -> R.drawable.avatar_01
2 -> R.drawable.avatar_02
3 -> R.drawable.avatar_03
4 -> R.drawable.avatar_04
else -> null
}
resourceId?.let {
view.setImageResource(it)
} ?: view.setImageDrawable(null)
}
}
xml側での書き方はこちらです。
<androidx.appcompat.widget.AppCompatImageView app:iconId="@{viewModel.user.iconId}" />
<androidx.appcompat.widget.AppCompatImageView app:goneUnless="@{viewModel.user.favorite}" />
完成版デモ
スピナーでユーザを切り替えるとそのユーザの情報が表示されるようになっていのがわかると思います。
動作としては期待通り動いていますね。
データバインディング重要ポイントまとめ
サンプルアプリを作りながら使い方を紹介してきましたが、ここでデータバインディングの重要ポイントを整理します。
- gradleで有効化
- レイアウトファイル(xml)のルートタグを<layout>にする
- 使用するデータの定義は<data>タグ内に<variable>タグを使用
- BindingクラスのLifecycleOwnerを設定
- <variable>で定義したデータモデルに実際のデータを設定
gradleで有効化
app直下の build.gradle に追加します。
apply plugin: 'kotlin-kapt'
android {
dataBinding {
enabled = true
}
}
レイアウトファイル(xml)のルートタグを<layout>にする
<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">
...
</layout>
使用するデータの定義は<data>タグ内に<variable>タグを使用
<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>
<variable name="viewmodel"
type="com.tf.android.databindingsample.viewmodel.UserViewModel">
</variable>
</data>
....
</layout>
BindingクラスのLifecycleOwnerを設定
binding.lifecycleOwner = viewLifecycleOwner
<variable>で定義したデータモデルに実際のデータを設定
binding.viewModel = viewModel
最後に
データバインディングはいかがでしたか。
MVVMアーキテクチャで開発していないプロジェクト内でも簡単に導入ができますし、使った方が良いです。
findViewById書かなくて良くて、メンバ変数も減らすことができるので本当に良いです。
今後実装してみて、「あれ?うまくいかない」と思ったときにはまたこの記事をぜひ読んでみてください。
「重要ポイントまとめ」のところで多くの問題は解決できると思います。
この記事であなたの疑問や問題が解決できたら幸いです。