前置き
2022年09月19日現在の情報です
出来るだけ最新のやり方を調べてますが、古いやり方が混入してるかも
作るもの
こんな感じで、EditTextに入力した文字の文字数をカウントするだけのシンプルなアプリを
バインディング機能で実装します
環境
OS:Windows10
IDE:AndroidStudio
実機:Galaxy A20
言語:Kotlin
参考
相違点
・Flagmentクラスを使わない
こいつ何のために使うんでしょう
なんかあったら教えてください
プロジェクト作成
AndroidStudioでEmpty Anctivityが作れる事前提で話します
ViewModelもFactoryも別パッケージにしたほうがいいと思うけど
分かりやすさ優先であえて分けてません
gradleの設定
追加するのちょっとなので、ここでは全ソース載せません
最後のあとがきあたりに一応全ソース載せようと思います
build.gradle(app)に以下を追記
Androidセクションに下記を追加
buildFeatures {
dataBinding = true
}
これを記述しないとバインディング機能が使えない
機能しないとか以前に純粋にバインディング関連の機能使おうとするとビルドエラーでます
調べた限り、もう一つぐらい方法があるが、こっちのほうがシンプル(だと思った)なのでこっちを採用
dependenciesセクションに下記を追加
var lifecycle_version = "2.4.1"
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
androidxのViewModelクラスを使うための儀式みたいなもの
AndroidStudioで「Sync Now」をクリックして更新
gradleはこれで用済み
ViewModel作成
名前もなんでもいいんですが、MainActivityViewModelで作成
ソースは下記
package com.bindingtest.bindingtest
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel
class MainActivityViewModel : ViewModel() {
var editTextString: MutableLiveData<String> = MutableLiveData()
}
androidxのViewModelクラスを継承して、文字列を保存するだけの変数定義
こいつは単純に説明すると、起動中のアプリがバックグラウンドとかに入っても
値を保持してくれる奴と覚えておけばいい
Factoryクラス作成
これも名前なんでもいいんですが、MainActivityFactoryで作成
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
こいつは作らなくてもデフォルトであるが、書き換える必要があるので書き換える
下記をコピペして、適度必要な個所を少し書き換えれば大丈夫なはず。たぶん
<?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がこの中でしか使えない為。理由は謎
下記は説明
<data>
<variable
name="vm"
type="com.bindingtest.bindingtest.MainActivityViewModel" />
</data>
これは、このXML内でViewModelクラスを使いますよという宣言みたいなもの
nameの部分は、このViewModelクラスをxml内で何と呼ぶかという名前付け
ただ、これだけは、「使いますよ」という宣言だけで、
こいつの実体自体はここまででは、どこにも存在しない
その実態を「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
lateinit var factory : MainActivityViewModelFactory
lateinit var viewmodel : MainActivityViewModel
lateinit var binding : ActivityMainBinding
factoryは必要かわからないが、viewmodelとbindingはActivityMainの全体で使う可能性があるため
クラス変数として保持しておく(今回はOnCreate内部だけで完結してるため必要ないが)
// bingding情報に結び付けられたviewをセットする
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
inflate処理の内容までは追ってないが、こうすることで
bindingされたビューやUI部品にアクセスできる
factory = MainActivityViewModelFactory()
viewmodel = ViewModelProvider(this,factory).get(MainActivityViewModel::class.java)
ViewModelクラスを作成する
これも詳しく処理を追ってないが、おそらく
ViewModelが生成されてなければ作る。既に存在する場合は作られてるViewModelを持ってくる処理だと思う。自信が無い
binding.vm = viewmodel
binding.lifecycleOwner = this
作った(あるいはすでに作られた)ViewModelをbinding(View)側に関連付ける処理
binding.lifecycleOwner の詳細は不明だが、こいつに自身のActivityの参照を設定しないと
EditTextで入力した情報がリアルタイムにTextViewに伝わらない
1行だが重要な処理なのだろう。きっと
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と結びつけた
<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) 全文
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に下記投稿がある
・BaseObserverクラスを継承したViewModelでは、画面の回転とかでデータが飛ぶので復元しないといけないが、Androidx側で用意されたViewModelクラスを継承すれば、そういう状況になってもデータが保持されるっぽい
バインディングにも支障がないので、なにか特別な理由がなければ、ViewModel継承したクラスでデータ保持したほうがいいと思われる