Android(Kotlin)LiveData でデータバインディング

2020/9/14

はじめに

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>