Tatsuro のテックブログ

アプリ開発の技術情報を発信します。

【Android × Kotlin】Data Binding でレイアウトと StateFlow をバインドする

今回は、Data Binding でレイアウトと StateFlow をバインドする方法を解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

  • Android Studio Electric Eel | 2022.1.1
  • JDK 11.0.15
  • Android Gradle Plugin 7.4.0
  • Kotlin 1.8.0
  • Gradle 7.6.0
  • org.jetbrains.kotlinx:kotlinx-coroutines-android 1.6.4
  • androidx.lifecycle 2.5.1

概要

画面の表示が可変であり、その状態を ViewModel の StateFlow で保持する場合、事前に画面のレイアウトとその StateFlow をバインドすることで、その StateFlow で保持する状態が自動的に画面に反映されるようになります。

今回は、Button をクリックした回数を表示するアプリの作成を通して、Data Binding でレイアウトと StateFlow をバインドする方法を解説します。

ライブラリのインポート

Data Binding を有効にする場合、アプリの build.gradle ファイルの android ブロックに以下の設定を追加します。

android {
    buildFeatures {
        dataBinding true
    }
}

また、今回の解説では ViewModel と StateFlow も使用するため、以下の依存関係を追加します。

dependencies {
    // Kotlin Coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
    // AndroidX Lifecycle
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
}

以上で、ライブラリのインポートは完了です。

ViewModel の実装

最初に ViewModel を実装します。この ViewModel では、Button のクリック回数を StateFlow で保持し、クリック回数をカウントアップするメソッドを設けます。

class MainViewModel : ViewModel() {

    /** Button のクリック回数 */
    private val _count = MutableStateFlow("0")
    val count = _count.asStateFlow()

    /** クリック回数をカウントアップする。 */
    fun countUp() {
        val iCount = count.value.toInt()
        _count.value = (iCount + 1).toString()
    }
}

まず、Button のクリック回数は文字列型で保持します。これは、Data Binding でレイアウトとバインドする際、表示するデータは文字列型でなければいけないからです。

/** Button のクリック回数 */
private val _count = MutableStateFlow("0")
val count = _count.asStateFlow()

そして、クリック回数をカウントアップするメソッドは以下のように実装します。

/** クリック回数をカウントアップする。 */
fun countUp() {
    val iCount = count.value.toInt()
    _count.value = (iCount + 1).toString()
}

以上で、ViewModel の実装は完了です。

レイアウトの実装

次に画面のレイアウトファイルを以下のように実装します。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="viewModel"
            type="com.tatsuro.app.databindingbasic.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Count up"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.count, default=0}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/button" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

まず、レイアウトのルートタグを layout にします。こうすることで、このレイアウトは Data Binding が有効になります。

<?xml version="1.0" encoding="utf-8"?>
<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"
    tools:context=".MainActivity">

    <!--  略  -->

</layout>

次に、layout タグの中に data タグを追加します。この data タグには変数を定義します。name 属性には変数名を設定します。type 属性にはこの変数の型を設定します。今回、ViewModel で保持する状態を画面に表示するため、さきほど実装した ViewModel を type 属性に設定します。

<data>

    <variable
        name="viewModel"
        type="com.tatsuro.app.databindingbasic.MainViewModel" />
</data>

そして、Button のクリック回数を表示する TextView は以下のように実装します。

さきほどの data タグに定義した変数を参照する場合、@{} 構文を使用します。この @{} 構文には、参照する変数と、Layout Editor の Preview に表示するデフォルト値を設定します。(このデフォルト値は省略できます。)

今回は viewModel の count プロパティを TextView に表示するため、android:text="@{viewModel.count, default=0}" と実装します。

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{viewModel.count, default=0}"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/button" />

以上で、レイアウトの作成は完了です。

Activity や Fragment の実装

最後に Activity や Fragment を実装します。Activity や Fragment では、Data Binding のインスタンスを取得し、そのインスタンスに必要な設定を行います。

なお、Activity と Fragment ではそれぞれ実装に異なる箇所があるので、それぞれ解説します。

Acticity の実装

Activity の場合、以下のように実装します。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val viewModel =
            ViewModelProvider(this)[MainViewModel::class.java]

        DataBindingUtil.setContentView<MainActivityBinding>(this, R.layout.main_activity)
            .let {
                it.lifecycleOwner = this
                it.viewModel = viewModel
                it.button.setOnClickListener {
                    viewModel.countUp()
                }
            }
    }
}

まず、Activity にレイアウト ID を設定する際、Activity#setContentView ではなくて DataBindingUtil.setContentView を使用します。DataBindingUtil.setContentView を使用して Data Binding のインスタンスを取得します。

そして、Data Binding のインスタンスには、LifecycleOwner とレイアウトで定義した変数、リスナーメソッドを設定します。

LifecycleOwner には Activity のインスタンスを設定します。この LifecycleOwner は StateFlow や LiveData で保持する状態を自動的に画面に反映するために必要な設定です。

レイアウトで定義した変数については、今回レイアウトで ViewModel を定義しているので、ViewModel のインスタンスを設定します。

最後に、Button のクリックリスナーに ViewModel のメソッドを設定します。

Fragment の実装

Fragment の場合、以下のように実装します。

class MainFragment : Fragment(R.layout.main_fragment) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val viewModel =
            ViewModelProvider(this)[MainViewModel::class.java]

        MainFragmentBinding.bind(view)
            .let {
                it.lifecycleOwner = viewLifecycleOwner
                it.viewModel = viewModel
                it.button.setOnClickListener {
                    viewModel.countUp()
                }
            }
    }
}

Fragment で異なる箇所は、Data Binding インスタンスの取得方法と LifecycleOwner です。

まず、Data Binding インスタンスの取得は DataBindingUtil.setContentView ではなくて Data Binding クラスの bind メソッドを使用します。

Activity のときに使用した DataBindingUtil.setContentView は Activity にレイアウト ID を設定するメソッドであるため、Fragment では使用できません。そのため、Fragment の場合には Data Binding クラスの bind メソッドを使用して、Data Binding インスタンスを取得します。

なお、この Data Binding クラスはレイアウトファイルごとに自動生成されます。そのクラス名はレイアウトファイルのファイル名のパスカルケースに、末尾に Binding が付与されたものになります。例えば、レイアウトファイル名が main_fragment の場合、その Data Binding クラスのクラス名は MainFragmentBinding になります。よって、Data Binding インスタンスを取得する際は、MainFragmentBinding.bind(view) になります。

そして、LifecycleOwner には Fragment のインスタンスではなくて viewLifecycleOwner を設定します。Fragment には Fragment 自身と Fragment の上にある View の 2 つのライフサイクルが存在します。StateFlow で保持する状態を反映する先が Fragment の View になるため、LifecycleOwner には viewLifecycleOwner を設定します。

参考

データ バインディング ライブラリ

関連記事

ViewModel の使用方法については、以下で解説しています。

tatsurotech.hatenablog.com

StateFlow の使用方法については、以下で解説しています。

tatsurotech.hatenablog.com