【2022年】Androidアプリで必要最小の双方向バインディング実装

# 前置き

2022年09月19日現在の情報です
出来るだけ最新のやり方を調べてますが、古いやり方が混入してるかも

# 今回作るもの

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2740385/fbf02a8b-7e83-a880-cd92-e73a4496aeb0.png)

・On、Off（めんどくさいのでtrue＝On　false = Off )の内部情報を表示するTextView
・On、Offの情報を切り替え、自身もOn、Off情報の外部変更によって状態を変えるToggleButton
・On Offの内部情報をフリップフロップする単純なButton

上記仕様を満たすアプリ

# 環境

OS：Windows10
IDE：AndroidStudio
実機：Galaxy A20
言語：Kotlin

# 参考

https://qiita.com/princess114514/items/909826f0553f3c73bab6#baseobservale%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8Fviewmodel%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%92%E4%BD%BF%E3%81%86%E7%90%86%E7%94%B1

Factoryクラスとかのつくり方はコレと同じ
ViewModelのつくり方とかも、これとほぼ同じ
activity_main.xmlとMainActibity.ktが違うぐらい

ファイル構成は同一なので、各クラス等がどう機能してるかは、上記の記事参照

# そもそも「双方向データバインディグ」ってなんぞや

https://qiita.com/tkmd35/items/516283b4e43fd5ff7a7c

上記記事に説明と、自分が作ったわけでもない例があるが
たぶん実際に作ったほうが理解早いと思います

# プロジェクト作成

AndroidStudioでEmpty Anctivityが作れる事前提で話します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2740385/a690e7a2-854a-0456-60a8-471446774a05.png)

パッケージ名はなんでもいいんですけど、自分はこうしてます

com.bindingtest.twowaybinding

# build.gradle(app)

https://qiita.com/princess114514/items/909826f0553f3c73bab6#baseobservale%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8Fviewmodel%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%92%E4%BD%BF%E3%81%86%E7%90%86%E7%94%B1

ここに書いてあるのとまったく同じ

# MainActivityFactory.kt

https://qiita.com/princess114514/items/909826f0553f3c73bab6#baseobservale%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8Fviewmodel%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%92%E4%BD%BF%E3%81%86%E7%90%86%E7%94%B1

ここに書いてあるのとまったく同じ

ActivityMain.ktと同じ階層に作る

~~~ ActivityMainFactory.kt
package com.bindingtest.twowaybinding
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

class MainActivityViewModelFactory : ViewModelProvider.NewInstanceFactory() {

    override fun <T : ViewModel> create( modelClass : Class<T> ) : T
    {
        return MainActivityViewModel() as T
    }
}
~~~

# MainActivityViewModel.kt

~~~ MainActivityViewModel.kt
package com.bindingtest.twowaybinding
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel

class MainActivityViewModel : ViewModel() {

    val toggleButtonSw: MutableLiveData<Boolean> = MutableLiveData<Boolean>()

    init {
        toggleButtonSw.value = false
    }

}
~~~

# main_activity.xml

下記をコピペして、必要なところを都度修正
（※：これをそのままコピペしても理想通りには動きません
　　　理由は下記で説明します）

~~~ main_acitivity.xml
<?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">

    <data>
        <variable
            name="vm"
            type="com.bindingtest.twowaybinding.MainActivityViewModel" />
    </data>

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

        <LinearLayout
            android:layout_width="409dp"
            android:layout_height="354dp"
            android:orientation="vertical"
            tools:layout_editor_absoluteX="1dp"
            tools:layout_editor_absoluteY="1dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{vm.toggleButtonSw.toString()}" />

            <ToggleButton
                android:id="@+id/toggleButton"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="ToggleButton"
                android:checked="@{vm.toggleButtonSw}"/>

            <Button
                android:id="@+id/button"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Button"
                android:onClick="onClick_ToggleButtonFlipButton"/>

        </LinearLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>
~~~

・ViewModelのフラグ情報を可視化するTextView
・ViewModeのフラグ情報を切り替え、自身もViewModelのフラグ情報を参照する
　ToggleButton
・ViewModelのフラグ情報を切り替える普通のボタン

の３つで構成

# MainActivity.kt

~~~ MainActivity.kt
package com.bindingtest.twowaybinding

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import com.bindingtest.twowaybinding.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    lateinit var factory : MainActivityViewModelFactory
    lateinit var viewmodel : MainActivityViewModel
    lateinit var binding : ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //setContentView(R.layout.activity_main)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        factory = MainActivityViewModelFactory()
        viewmodel = ViewModelProvider(this,factory).get(MainActivityViewModel::class.java)
        binding.vm = viewmodel
        binding.lifecycleOwner = this

    }

    fun onClick_ToggleButtonFlipButton( view: View )
    {
        viewmodel.toggleButtonSw.value = !viewmodel.toggleButtonSw.value!!
    }
}
~~~

# さあ実行してみよう…しかし

ビルドは通った！問題なく動いてる…
ように見えて、ToggleButtonとButtonを交互に押してみたりすると
ある問題に行き着くと思います

そう、ToogleButtonの入力が正常にViewModel側に通知されてないのです

ToggleButtonはViewModelの入力を受け取るけど
ToggleButton側からの入力はViewModelに反映しませんよと

これが「単方向バインディング」って奴です

# どうすれば想定通り（双方向）になるか

答えは簡単です

~~~ main_actibity.xml
            <ToggleButton
                android:id="@+id/toggleButton"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="ToggleButton"
                android:checked="@={vm.toggleButtonSw}"/>
~~~

現状のソースとの間違い探しみたいですが、簡単です

android:checked="@={vm.toggleButtonSw}の部分を見ればわかる通り
@と{の間に「=」を入れるだけです

これだけで双方向になります

あとは実際にビルドして、ToggleButtonとButtonを交互に押して
おかしな挙動にならず、TextViewに表示してる文字が「True」「False」を交互に表示するになれば成功です