Android(Kotlin)LiveData でデータバインディング
September 14, 2020
はじめに
Android 開発経験を一定積むと、findViewById
メソッドを使って要素を取得した上で、(監視付きで)表示用値セット、イベントハンドリング等、処理を記述していくことに疲れてくる訳ですが、そんな時に嬉しいデータバインディングという仕組みがあります。
ただ、フラグメントで LiveData を使う前提の場合、ドキュメントにも直接的な情報が見当たらずに試行錯誤したので、備忘録としてまとめます。
実装内容
概要
- データバインディングを利用し、EditText に入力された文字数を別の TextView で表示
- EditText に対しては双方向データバインディングを適用
- 監視には LiveData を利用
- 上記をアクティビティではなく、フラグメントで行う
詳細
app/build.gradle
データバインディングのための差分に加え、
by viewModels()
を使うための差分も含めています。
@@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 29
@@ -22,6 +23,14 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ dataBinding {
+ enabled = true
+ }
}
dependencies {
@@ -29,6 +38,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'androidx.fragment:fragment-ktx:1.2.5'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
app/src/main/java/com/example/myapplication/MainActivity.kt
特に何もしていません。
本題から外れますが、AppCompatActivity
のコンストラクタ引数で layout を指定できることを最近知りました。便利ですね。
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity(R.layout.activity_main) {
}
app/src/main/java/com/example/myapplication/MainFragment.kt
最初ハマってしまったんですが、
binding.lifecycleOwner = viewLifecycleOwner
が抜けていると、後述する ViewModel における Transformations.map
利用の LiveData が機能しないことに注意です。
また、DataBindingUtil.inflate に記述がある通り、事前にレイアウト ID が分かっているなら、自動生成されたバインディングクラスの inflate
メソッドを使う方が良いとのことです。
package com.example.myapplication
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.example.myapplication.databinding.FragmentMainBinding
class MainFragment: Fragment() {
private val viewModel: MainViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentMainBinding.inflate(inflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
return binding.root
}
}
app/src/main/java/com/example/myapplication/MainViewModel.kt
LiveData を使用します。
package com.example.myapplication
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
class MainViewModel : ViewModel() {
val name: MutableLiveData<String> = MutableLiveData<String>("")
val nameLength: LiveData<Int> = Transformations.map(name) {
name -> name.length
}
}
app/src/main/res/layout/activity_main.xml
該当フラグメントが設置されているだけで、特筆すべき点はありません。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".MainActivity">
<fragment
android:id="@+id/fragment"
android:name="com.example.myapplication.MainFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
app/src/main/res/layout/fragment_main.xml
双方向含めたバインディング式を記述しています。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="viewModel" type="com.example.myapplication.MainViewModel"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<EditText
android:id="@+id/editTextName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="text"
android:text="@={viewModel.name}"/>
<TextView
android:id="@+id/textViewNameLength"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{viewModel.nameLength.toString()}"/>
</LinearLayout>
</layout>