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


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

# 作るもの

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2740385/50cadc84-9811-113d-be80-2a3509dfd104.png)

こんな感じで、EditTextに入力した文字の文字数をカウントするだけのシンプルなアプリを
バインディング機能で実装します

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

# 参考

https://qiita.com/zakimiiiii/items/2d0de51bc37f8e3789e0

相違点
・Flagmentクラスを使わない

こいつ何のために使うんでしょう
なんかあったら教えてください

# プロジェクト作成

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

パッケージ名はなんでもいいんですけど、自分はこうしてます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2740385/5e7517f3-b953-ee43-4d3d-b4cc20180201.png)

ViewModelもFactoryも別パッケージにしたほうがいいと思うけど
分かりやすさ優先であえて分けてません

# gradleの設定

追加するのちょっとなので、ここでは全ソース載せません
最後のあとがきあたりに一応全ソース載せようと思います

build.gradle(app)に以下を追記

Androidセクションに下記を追加

~~~ build.gradle(app)
buildFeatures {
         dataBinding = true
    }
~~~

これを記述しないとバインディング機能が使えない
機能しないとか以前に純粋にバインディング関連の機能使おうとするとビルドエラーでます

調べた限り、もう一つぐらい方法があるが、こっちのほうがシンプル（だと思った）なのでこっちを採用

dependenciesセクションに下記を追加


~~~ build.gradle(app)
    var lifecycle_version = "2.4.1"
    // ViewModel
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
~~~

androidxのViewModelクラスを使うための儀式みたいなもの

AndroidStudioで「Sync Now」をクリックして更新
gradleはこれで用済み

# ViewModel作成

名前もなんでもいいんですが、MainActivityViewModelで作成

ソースは下記

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

class MainActivityViewModel : ViewModel() {

    var editTextString: MutableLiveData<String> = MutableLiveData()

}
~~~

androidxのViewModelクラスを継承して、文字列を保存するだけの変数定義
こいつは単純に説明すると、起動中のアプリがバックグラウンドとかに入っても
値を保持してくれる奴と覚えておけばいい

# Factoryクラス作成

これも名前なんでもいいんですが、MainActivityFactoryで作成

~~~ MainActivityFactory.kt
package com.bindingtest.bindingtest
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
    }
}
~~~

こいつの必要性は謎だが、こうしておかないと素直にActivity側でViewModelを作れなさそうなので、素直に作っておく。なんか重要な意味があるのだろう。きっと。


# activity_main.xml

こいつは作らなくてもデフォルトであるが、書き換える必要があるので書き換える
下記をコピペして、適度必要な個所を少し書き換えれば大丈夫なはず。たぶん

~~~ activity_main.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.bindingtest.MainActivityViewModel" />
    </data>

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

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">


            <TextView
                android:id="@+id/textView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{Integer.toString(vm.editTextString.length())}" />

            <EditText
                android:id="@+id/editTextText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:ems="10"
                android:inputType="textPersonName"
                android:text="@{vm.editTextString}" />


        </LinearLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>
~~~

バインディングを実装するためにはLayoutの中に内包する
ViewModelと結びつけるためのDataがこの中でしか使えない為。理由は謎

下記は説明

~~~ activity_main.xml
      <data>
        <variable
            name="vm"
            type="com.bindingtest.bindingtest.MainActivityViewModel" />
    </data>
~~~

これは、このXML内でViewModelクラスを使いますよという宣言みたいなもの
nameの部分は、このViewModelクラスをxml内で何と呼ぶかという名前付け

ただ、これだけは、「使いますよ」という宣言だけで、
こいつの実体自体はここまででは、どこにも存在しない

その実態を「MainActivity.kt」で作成する

# MainActivity.kt

まずは、そのままコピペしてちょこっと弄れば使える（かもしれない）全体のソースを記述する

~~~ MainActivity.kt

package com.bindingtest.bindingtest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.TextWatcher
import android.text.Editable
import android.widget.EditText
import androidx.lifecycle.ViewModelProvider
import com.bindingtest.bindingtest.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

        val editText =  findViewById<EditText>(R.id.editTextText) as EditText
        editText.addTextChangedListener( object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
            }

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                //viewmodel.textString = s
                viewmodel.editTextString.value = s.toString()
            }

            override fun afterTextChanged(s: Editable?) {
                //v
                //text.text = s
            }
        })
    }

}

~~~

「ActivityMainBinding」というクラスは、自動的に生成されるクラス
自分で作る必要はない

下記で個々の説明を行う

importしてる奴らは、とりあえずこれがないとビルド通らないので脳死で記述OK

~~~ MainActivity.kt
　　lateinit var factory : MainActivityViewModelFactory
    lateinit var viewmodel : MainActivityViewModel
    lateinit var binding : ActivityMainBinding
~~~

factoryは必要かわからないが、viewmodelとbindingはActivityMainの全体で使う可能性があるため
クラス変数として保持しておく（今回はOnCreate内部だけで完結してるため必要ないが）

~~~ MainActivity.kt
        // bingding情報に結び付けられたviewをセットする
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
~~~

inflate処理の内容までは追ってないが、こうすることで
bindingされたビューやＵＩ部品にアクセスできる


~~~ MainActivity.kt
        factory = MainActivityViewModelFactory()
        viewmodel = ViewModelProvider(this,factory).get(MainActivityViewModel::class.java)
~~~

ViewModelクラスを作成する
これも詳しく処理を追ってないが、おそらく
ViewModelが生成されてなければ作る。既に存在する場合は作られてるViewModelを持ってくる処理だと思う。自信が無い

~~~ MainActivity.kt
        binding.vm = viewmodel
        binding.lifecycleOwner = this
~~~

作った（あるいはすでに作られた）ViewModelをbinding(View）側に関連付ける処理
binding.lifecycleOwner の詳細は不明だが、こいつに自身のActivityの参照を設定しないと
EditTextで入力した情報がリアルタイムにTextViewに伝わらない
１行だが重要な処理なのだろう。きっと


~~~ MainActivity.kt
        val editText =  findViewById<EditText>(R.id.editTextText) as EditText
        editText.addTextChangedListener( object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
            }

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                //viewmodel.textString = s
                viewmodel.editTextString.value = s.toString()
            }

            override fun afterTextChanged(s: Editable?) {
                //v
                //text.text = s
            }
        })
~~~

editTextの入力情報をリアルタイムにViewModel側に伝える処理

見てわかるが、findViewByIdでEditTextを引っ張ってきてaddTextChangedListnerで登録してるので
nullエラーで落ちる危険性があるし、たぶん旧時代の処理だと思われる
余計な関数をオーバーライドしてるので、XMLの記述でスマートにできないかと調べたが見つからず

viewmodelが更新される事でactivity_main.xmlでTextViewと結びつけた

~~~ activity_main.xml
            <TextView
                android:id="@+id/textView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{Integer.toString(vm.editTextString.length())}" />
~~~

こいつにリアルタイムにEditTextに入力された文字数の変化が反映されるはず

できなかったら、何かが間違ってるか、この情報が古くなったのが理由だと思う

これで一通りの説明は終了

# おまけ

### build.gradle(app) 全文

~~~ build.gradle(app)
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    compileSdk 32

    defaultConfig {
        applicationId "com.bindingtest.bindingtest"
        minSdk 21
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }

    buildFeatures {
         dataBinding = true
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.2'
    implementation 'com.google.android.material:material:1.6.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

    var lifecycle_version = "2.4.1"
    // ViewModel　
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")


}
~~~

### 「BaseObservale」クラスではなく「ViewModel」クラスを使う理由

「BaseObservable vs ViewModel」とかいう興味深いサジェストがあったので
ググってみると、StackOverflowに下記投稿がある

https://stackoverflow.com/questions/50270542/what-is-the-difference-between-viewmodel-that-extends-baseobservable-and-android

・BaseObserverクラスを継承したViewModelでは、画面の回転とかでデータが飛ぶので復元しないといけないが、Androidx側で用意されたViewModelクラスを継承すれば、そういう状況になってもデータが保持されるっぽい

バインディングにも支障がないので、なにか特別な理由がなければ、ViewModel継承したクラスでデータ保持したほうがいいと思われる