追記 (2020-07-21)
(先月頭だかのAndroidStudio4.0アップデートで起きた話なので、割と今さら感あるが)
データバインディングを使用したいときに必要な設定の記述がAndroid Gradle Plugin 4.0.0
では次のものに変わっていた。ちなみにGradle
のバージョンは6.1.1
だった。
android {
buildFeatures {
dataBinding true
}
}
以前の記述法 でも今のところ動きはするがビルド時に警告が出る。
DSL element 'android.dataBinding.enabled' is obsolete and has been replaced with 'android.buildFeatures.dataBinding'.
追記 (2020-03-31)
lifecycleに関する依存先のバージョンを2.2.0
にアップデート。
それに伴い、ViewModelProvider
を使用したViewModelのインスタンス生成方法を修正。
ViewModelProviders.of(owner)
→ ViewModelProvider(owner)
追記 (2020-03-10)
BindingAdapterに関するいくつかのこと - すいはんぶろぐ.io
関連する記事としてバインディングアダプタの使い方を書いた。
最近ずっとSatenaのActivity/Fragmentのコード側を作り直す作業をしていて、ViewModel + LiveDataは割と活用してきて"前よりは"いい感じになってきているのだが、
DataBindingに関しては以前UWPアプリ作るとき(中途半端に)触れて以来、MVVMというかデータバインディングやったらやったでそれはそれで色々と面倒なことがあるイメージがあったので積極的に取り入れてなかったのだけど、楽できそうな部分ではやっていこうかと唐突に思った。
今さら感強い上にまだ詳しく使い込んでいないので、
とりあえず導入的なものと、
主に忘れそうな部分についていくつかメモを書く。
導入
以下、Android Studio 4.0
、Android Gradle Plugin 4.0.0
、Gradle 6.1.1
時点でのお話になります。
build.gradle (app)
ViewModelやらDataBindingを使用するのに必要な設定・依存関係を追加する。
android {
buildFeatures {
dataBinding true
}
}
dependencies {
...
// ViewModel and LiveData
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
testImplementation "androidx.arch.core:core-testing:2.1.0"
...
}
activity_hoge.xml
以前のレイアウトの内容を<layout>
で囲い、<data> ~ </data>
にバインドに必要な情報を記述する。
ここでは、コード側で用意してバインドするViewModel
のオブジェクトをレイアウトファイル内ではvm
として扱う。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<!-- bindするデータ -->
<data>
<variable
name="vm"
type="com.suihan74.hoge.HogeViewModel" />
</data>
<!-- 以下表示部分 -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/hoge_layout"
android:background="?attr/panelBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content">
...
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
HogeActivity.kt
ActivityHogeBindingはレイアウトまで書いてビルドすると自動的に生成される。ViewDataBinding
を継承した"アッパーキャメルなレイアウトファイル名 + Binding"
という名前のクラスができるので、これをDataBindingUtil.setContentView<T>(...)
の型引数に与えて呼ぶとバインドが開始される。
class HogeActivity : AppCompatActivity() {
private val viewModel: HogeViewModel by lazy {
// ViewModelの生成
// ViewModel用のFactoryを用意しておけばViewModelを生成する際コンストラクタに値を渡すことができる
val factory = HogeViewModel.Factory(
...
)
ViewModelProvider(this, factory)[HogeViewModel::class.java]
}
private lateinit var binding: ActivityHogeBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// データバインド
binding = DataBindingUtil.setContentView<ActivityHogeBinding>(
this,
R.layout.activity_hoge
).apply {
lifecycleOwner = this@BookmarkPostActivity
vm = viewModel
}
}
...
}
バインドするデータにLiveData
を使用する場合、作成したbinding
に適切なlifecycleOwner
を渡す必要がある。これを行わないとLiveData
の値変更がレイアウトにうまく通知されない。
ViewModel
を使うことでsavedInstanceState
はせいぜい初回起動か復元後かを判別する程度の用途しか無くなりましたとさ。良さ。
データの変更をViewに反映する
たとえば次のようなViewModelを用意するとする。LiveData<T>
はLiveData<T>.value
が変更された際に生きている監視者に変更を通知するもの。LiveData<T>
を継承して好きなように作ることもできるが、ここでは面倒なのでvalue
を外から変更できるMutableLiveData<T>
を使うことにする。
HogeViewModel.kt
class HogeViewModel(
private val repository : HogeRepository
) : ViewModel() {
val text by lazy {
MutableLiveData<String>("")
}
...
// NewInstanceFactoryを用意すればViewModelのプライマリコンストラクタでプロパティが初期化できる
class Factory(
private val repository : HogeRepository
) : ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>) =
HogeViewModel(repository) as T
}
}
ViewModel
ごとにFactory
を用意しなくてよくするための拡張を書いた▼
コンストラクタ引数有りのViewModelを簡単に作成する - すいはんぶろぐ.io
これを先ほどのレイアウトファイルにTextView
を追加して、データバインディングで表示するようにするには次のようにTextView
を記述する。
<TextView
android:text="@{vm.text}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
@{...}
でViewModel→Viewの単方向データバインディング。
これでviewModel.text
が変更されたときにTextView
の表示も変更されるようになる。楽。
Viewの変更をデータに反映する
EditText
の入力テキストを先ほどのHogeViewModel.text
に反映する例。
<EditText
android:text="@={vm.text}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
@={...}
でViewModel⇔Viewの双方向データバインディング。
他にもチェックボックスやトグルボタンの状態管理など。
真偽値をバインドしてVisibilityを変更する例
楽そうだと思ったのはこれ。
...
<data>
<import type="android.view.View"/>
...
</data>
...
<TextView
android:text="@{vm.text}"
android:visibility="@{vm.display ? View.VISIBLE : View.GONE}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
バインディング式にView.VISIBLEなどを記述する場合、<data>~</data>
にてView
をインポートする必要がある。
どうでもいいけど、最近kotlinばかり書いていて三項演算子懐かしいってなった。
レイアウトとバインディング式 | Android Developers
もっと凝ったことをする場合は、バインディングアダプターを用意する方法をとれば良さそう。
バインディングアダプター | Android Developers
双方向バインディングでConverterを介して値をやりとりする
BindingAdapter
を使うやり方についてはこちらに書いた。そっちでよさそう。
BindingAdapterに関するいくつかのこと - すいはんぶろぐ.io
たとえば適当な列挙型HogeEnum
があるとして、これをSpinner
に表示して選択させたいときなどに以下のようなシングルトンを用意しておいて、レイアウトから呼ぶことができる。
object HogeEnumConverter {
@JvmStatic
@InverseMethod("toHogeEnum")
fun toInt(item: HogeEnum?) = item?.ordinal ?: 0
@JvmStatic
fun toHogeEnum(ordinal: Int) = HogeEnum.values()[ordinal]
}
<layout ...>
<data>
<import type="com.suihan74.hoge.HogeEnumConverter"/>
<variable
name="vm"
type="com.suihan74.hoge.HogeViewModel" />
</data>
...
<Spinner
android:id="@+id/spinner"
android:entries="@array/hogeEnums"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:selectedItemPosition="@={HogeEnumConverter.toInt(vm.selectedHogeEnum)}" />
...
</layout>