30歳からの開発日記

30歳でエンジニア転職した元営業の備忘録。

【Android】双方向データバインディング(Two-way data binding)の簡単なサンプルを作る

はじめに

今回は双方向データバインディングを理解するために簡単なアプリを作成してみました。

双方向データバインディングによって、ユーザーの入力値をビューに反映するようなロジックがとても簡単に実装できるのでぜひ覚えておきたい技術の1つです。

今回作るもの

入力欄に名前を入力してボタンを押すと、TextView に「Hello, XXX!」と表示される簡単なアプリを作ってみます。

TextView に入力したテキストと、プログラム内部で保持しているデータがリンクするように実装することで、入力テキストを画面に反映させる処理が簡単に記述できるのが実感できるかと思います。

ソースファイルはこちらで公開してます。

github.com

データバインディングとは

まず、データバインディングについて簡単に説明しておきたいと思います。

データバインディングとは、ビューとデータを結びつけて、どちらかの値が変更されたときもう片方の値も自動的に変更される仕組みのことです。結びつきが一方通行だと One-way、お互いに結びついていると Two-way(双方向)のデータバインディングとなります。

通常、Android 開発で画面を更新したい場合は以下のように findViewById() を使ってビューオブジェクトを取得し、値をセットします。

val textView: TextView = findViewById(R.id.text_view)
textView.text = "Hello World"

データバインディングを利用すれば、画面に表示する値をレイアウトファイル内で直接割り当てることができます。

<TextView
        ...
        android:text="@{viewModel.text}" />

ここでは TextViewviewModel.text を紐づけています。viewModel.text の値が変更されれば、表示されているテキストも自動的に変更されるため、逐一 findViewById() でビューを取得する必要がありません。findViewById()は重たい処理でもあるのでメモリにも優しい実装になります。

データバインディングはビューロジックとビジネスロジックを分離してくれます。個人的には、ビジネスロジックの開発においてビューロジックを意識する必要がなくなる(逆もしかり)ので、あれこれ考えず開発に集中できるのが嬉しいポイントです。データバインディングを使って可読性が高く、保守しやすいアプリ開発を目指しましょう!

最終的なディレクトリ構造

参考までに最終的なディレクトリ構造を載せておきます。

最終的なディレクトリ構造

実装手順

1. 新しいプロジェクトを作成

まずは Empty Activity で新しいプロジェクトを作成します。

2. Gradle を編集

build.gradle (Module.app) を編集してデータバインディングライブラリを利用できるようにします。

また、ViewModelLiveData のライブラリも追加します。

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' // 追加

android {
    ...

    // dataBinding を有効化
    dataBinding {
        enabled = true
    }

}

dependencies {
    ...
    
    // ViewModel と LiveData のライブラリを追加
    def lifecycle_version = "2.2.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}

ネットの記事を読んでいると lifecycle-extensions を追加している記事をよく見かけますが、Lifecycle バージョン 2.2.0 からはサポートが終了しているので注意してください。developers にも以下のとおり記載されています。

lifecycle-extensionsAPI はサポートが終了しました。代わりに、必要とする特定の Lifecycle アーティファクトの依存関係を追加してください。

詳しくは developers の Lifecycle のページをご確認ください。

3. ViewModel、User モデルクラスを作成

次に ViewModel と User モデルクラスを作成します。

UserViewModel という名前で新しくファイルを作成し、以下の内容を実装します。

/**
 * User モデル。
 */
class User(var name: String)

/**
 * User に関するデータを扱う ViewModel。
 */
class UserViewModel : ViewModel() {

    // クラス内では値の変更が可能なミュータブルな LiveData を扱う。
    private val _user = MutableLiveData(User("John Doe"))
    private val _greeting = MutableLiveData("Hello, ${_user.value?.name}!")

    // 自由に値が変更できない形式で外部に公開。
    val user: LiveData<User> = _user
    val greeting: LiveData<String> = _greeting

    // ボタンがクリックされたら表示する文章を更新。
    fun onClick() {
        _greeting.value = "Hello, ${user.value?.name}!"
    }

}

ViewModel クラスを継承したUserViewModel クラスと、name というメンバ変数を持った User モデルクラスを作成しました。User クラスは1行しかないので今回は1つのファイルにまとめてますが、本来は分けたほうがいいかと思います。

UserViewModel クラスでは LiveData を使ってデータを保存しています。LivaData は Activity や Fragment などのライフサイクルに合わせて監視可能なデータホルダークラスです。

4. レイアウトファイルを編集

先ほど定義した UserViewModel をレイアウトファイル内で扱うためにレイアウトファイルを編集していきます。

<?xml version="1.0" encoding="utf-8"?>
<!-- データバインディングを行う場合、ルートが layout タグである必要がある。 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- レイアウトファイル内で使う変数を data タグの中で定義する。 -->
    <data>
        <!-- 変数 viewModel を定義する。 -->
        <variable
            name="viewModel"
            type="com.example.hellodatabinding.UserViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <!--
         android:text の値が @={...} であることに注意。
         ユーザーの入力値を ViewModel に反映させたい場合はイコールが必要。
         -->
        <EditText
            android:id="@+id/edit_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Please input your name!"
            android:inputType="text"
            android:text="@={viewModel.user.name}" />

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end"
            android:onClick="@{() -> viewModel.onClick()}"
            android:text="BUTTON" />

        <TextView
            android:id="@+id/text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="20dp"
            android:text="@{viewModel.greeting}"
            android:textSize="26sp" />

    </LinearLayout>
</layout>

5. Activity を編集

最後に MainActivity を編集して、3で定義した UserViewModel とレイアウトファイル内で定義した変数 viewModel を関連づける処理を追加します。また、MainActivity を lifecycleOwner として設定します。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val userViewModel =
            ViewModelProvider(this).get(UserViewModel::class.java)

        val binding: ActivityMainBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.also {
            // レイアウトファイルで定義した viewModel にバインド。
            it.viewModel = userViewModel
            it.lifecycleOwner = this
        }

        /**
         * 双方向データバインディングでは以下のようなリスナーは必要ない。
         */

//        binding.editText.addTextChangedListener { text ->
//            userViewModel.setName(text.toString())
//        }
    }
}

これで完成です。

冒頭に載せた GIF のように、名前を入力してボタンを押せば画面のテキストが変わるのが確認できるかと思います。