検証環境
この記事の内容は、以下の環境で検証しました。
- Java:open jdk 1.8.0_152
- Kotlin 1.2.10
- Android Studio 3.0.2
ゴール
これまで作成したきたアプリは、ユーザからのアクションがありきになっていました。
例えば、ボタンを押したときのイベントで画面遷移や何かしらの処理を行っていました。
今回は、EditTextで入力するとリアルタイムで処理をするイベントを使わないアプリを作っていきます。
実現方法の全体像
入力された値や答えを保持するModel(POJO)をどうやってリアルタイムで処理したものか・・・。と考えていきます。
まずは、Modelを常に監視して、計算するクラスが必要になります。監視する人をObserverと呼びます。ObserverはLiveData関連のクラス群の1つです。
もちろんModelをObserverに対応する形にする必要あります。
更に、EditTextに入力された値を、Modelに代入する人も必要になってきます。画面とModelを同期させる人をデータバインドと呼びます。データバインドはDataBind関連のクラス群の1つです。
結果的に下記のような構成になります。
各機能を実現するクラスは下記の通りです。
今後の実装の説明では、図の番号の順番で説明します。
実装
各機能の実装を見ていきます。
事前準備
Gradle
Live DataやDataBindを利用するために、設定を行っていきます。
Live Dataの設定
dependenciesに下記を追記します。
dependencies {
// ViewModel and LiveData
implementation "android.arch.lifecycle:extensions:1.1.0"
}
DataBindの設定
dependenciesに下記を追記します。
android {
dataBinding{
enabled = true
}
}
dependencies {
kapt "com.android.databinding:compiler:3.0.1"
}
kapt {
generateStubs = true
}
Model
コードの全体像
先に説明したように、Observerを設定する必要があるクラスは下記のように記述します。
class CalcModel : ViewModel() {
var first = MutableLiveData<String>()
var second = MutableLiveData<String>()
var result = "0"
init {
first.value = "0"
second.value = "0"
}
}
コードの詳細
ViewModelクラスを継承します。
class CalcModel : ViewModel()
Observerを設定するフィールドはMutableLiveDataクラスにする必要があります。
var first = MutableLiveData<String>()
var second = MutableLiveData<String>()
Observer
コードの全体像
package jp.co.casareal.mvvmsample
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import jp.co.casareal.mvvmsample.databinding.ActivityMainBinding
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
lateinit var calcModel: CalcModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activityMainBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
calcModel = ViewModelProviders.of(this).get(CalcModel::class.java)
activityMainBinding.model = calcModel
}
override fun onResume() {
super.onResume()
val observer = Observer<String>() {
if (firstEdit.text.isNotBlank() && secondEdit.text.isNotBlank())
resultText.text = (calcModel.first.value!!.toInt() + calcModel.second.value!!.toInt()).toString()
}
calcModel.first.observe(this, observer)
calcModel.second.observe(this, observer)
}
}
コードの詳細
ViewModelを継承したクラスのオブジェクトを取得します。(本サンプルではonCreateメソッド内に実装しています。)
オブジェクト取得にはViewModelProvidersクラスのof関数でFragmentActivityを継承したクラスのオブジェクトを渡します。戻り値がViewModelProvidersクラスのオブジェクトです。
get関数を呼び出してViewModelクラスのオブジェクトを取得します。
calcModel = ViewModelProviders.of(this).get(CalcModel::class.java)
Observerインターフェイスの実装オブジェクトを生成します。
本サンプルでは、左辺と右辺を整数に変換して、加算した結果を答えのTextViewに設定しています。
val observer = Observer<String>() {
if (firstEdit.text.isNotBlank() && secondEdit.text.isNotBlank())
resultText.text = (calcModel.first.value!!.toInt() + calcModel.second.value!!.toInt()).toString()
}
ViewModelで定義したObserverを設定できるフィールドにObserverを設定します。
calcModel.first.observe(this, observer)
calcModel.second.observe(this, observer)
データバインド
コードの全体像
<?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="model"
type="jp.co.casareal.mvvmsample.CalcModel"></variable>
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="jp.co.casareal.mvvmsample.MainActivity">
<TextView
android:id="@+id/resultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="16dp"
android:text="@{model.result}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginStart="32dp"
android:layout_marginTop="8dp"
android:text="+"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/firstEdit"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/firstEdit"
android:layout_width="71dp"
android:layout_height="54dp"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:ems="10"
android:text="@={model.first.value}"
android:inputType="number"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/secondEdit"
android:layout_width="74dp"
android:layout_height="47dp"
android:layout_marginBottom="8dp"
android:layout_marginStart="36dp"
android:layout_marginTop="8dp"
android:ems="10"
android:text="@={model.second.value}"
android:inputType="number"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/textView"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/equalText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="="
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/resultText"
app:layout_constraintHorizontal_bias="0.436"
app:layout_constraintStart_toEndOf="@+id/secondEdit"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
</layout>
package jp.co.casareal.mvvmsample
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import jp.co.casareal.mvvmsample.databinding.ActivityMainBinding
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
lateinit var calcModel: CalcModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activityMainBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
calcModel = ViewModelProviders.of(this).get(CalcModel::class.java)
activityMainBinding.model = calcModel
}
override fun onResume() {
super.onResume()
val observer = Observer<String>() {
if (firstEdit.text.isNotBlank() && secondEdit.text.isNotBlank())
resultText.text = (calcModel.first.value!!.toInt() + calcModel.second.value!!.toInt()).toString()
}
calcModel.first.observe(this, observer)
calcModel.second.observe(this, observer)
}
}
コードの詳細
レイアウトリソースファイルの全体構成は下記のようになっています。
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="xmlないでの変数名みたいなもの"
type="バインディングするクラスの完全クラス名"></variable>
</data>
<ビューグループ(LinearLayoutとかConstraintLayoutなど>
〜〜EditTextとかTextViewとかとか〜〜
</ビューグループ>
</layout>
バインドする際の変数名とその完全クラス名をdataタグないで定義します。
<data>
<variable
name="model"
type="jp.co.casareal.mvvmsample.CalcModel"></variable>
</data>
バインディングするデータを設定するには、下記のように記述します。
<TextView
android:id="@+id/resultText"
android:text="@{model.result}"
/>
<EditText
android:id="@+id/firstEdit"
android:text="@={model.first.value}"
/>
バインドの記述方法は2種類存在します。
@{dataのname属性.フィールド}
は単一方向のバインディングです。「ViewModel⇛画面」
@={dataのname属性.フィールド}
は双方向のバインディングです。「ViewModel⇔画面」
ViewModelでフィールドがMutableLiveDataクラスの場合は、下記のように記述し、値を取得します。
@={dataのname属性.フィールド.value}
ViewModelとバインドするには、Kotlin側で設定が必要です。バインドするために、「ActivityMainBinding」を取得しています。「ActivityMainBinding」は自動生成されるクラスです。
クラスの命名は、「レイアウトリソースファイルをキャメルケース+Binding」です。
通常であれば存在するsetContentViewメソッドは削除します。その代わり、DataBindingUtilクラスのsetContentViewを呼び出します。
ジェネリクスでは、Bindingクラス名を指定し、第1引数にはActivity、第2引数にはレイアウトリソースファイルを指定します。
val activityMainBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
ActivityMainBindingのオブジェクトが取得できたら、レイアウトリソースファイルのdataタグのname属性で指定した名前のフィールドにViewModelクラスのオブジェクトを設定します。
activityMainBinding.model = calcModel
まとめ
バインディングの方法はLive Data+DataBind以外にも存在しますが、Live Dataを利用することによりより簡潔に記述できるようになっているそうです。
いつか、比較できるような記事を書けたらと思います。