日々是好日

プログラミングについてのあれこれ、ムダ知識など

MVVM 完全に理解した - 7

前回 Android Architecture BluePrints を読んで挫折した(早っ)ので、基本に立ち返り最もシンプルな構成で Data Binding を体験してみます。

作るのは、MainActivity 1枚だけのミニアプリです。 純粋にリファレンスに沿って実装しました。

機能と外観

機能は、 EditText に入力した文字列をすぐ下の TextView に表示するだけのものです。
次の画像では hint を表示しているので違う文が表示されていますが、入力すると同じ文字列が表示されます。

f:id:kcpoipoi:20181210210359p:plain:w300
Main画面

手順

MainViewModel.ktの作成

たぶん最初に行うのが、ViewModel クラスの作成だと思います。
TextViewのtext要素にバインドさせるため、inputTextというフィールドを定義します。

class MainViewModel : ViewModel() {
    val inputText = MutableLiveData<String>()

    fun setInputText(s: String) {
        inputText.value = s
    }
}

これだけ。
inputText.valueでバインドされている要素に更新通知がなされます。*1
inputTextはできればprivateにしたいところですが、後述のとおり外部からobserveメソッドでセットするのでなんとも言えないところ。BluePrints でもpublicになってるしそういうものなのかな?

activity_main.xmlの作成

MainActivity のレイアウトファイルを作成します。
ほとんどの場合 UI を Activity 上に直接載せることは少ないかと思いますが、リファレンスに沿うと Data Binding の練習はこんな形のスタートになると思います。

<layout xmlns:android=...>
    <data>
        <variable name="viewmodel" type="work.kcs_labo.mvvmpractice.MainViewModel" />
    </data>

    <android.support.constraint.ConstraintLayout ...>

        <EditText
                android:hint="文字を入力してくだしあ"
                android:id="@+id/input" .../>

        <TextView
                android:hint="ここに反映されます"
                android:text="@{viewmodel.inputText}"
                android:id="@+id/output" .../>

    </android.support.constraint.ConstraintLayout>
</layout>

Data Binding のためのレイアウトファイルでは、layout要素をルートにし、data以下を記述します。
android:text="@{viewmodel.inputText}"というところでバインドを行っています。

ここまで終わったら、念のためリビルドしActivityMainBindingクラスが自動生成されていることを確認します。

MainActivity.ktの作成

最後にMainActivityを作成します。onCreateに全部詰めた形であまり見てくれはよくないですが、最も簡単な構成になると思います。

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

        //Viewの生成(inflate)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        //または次も可
        //val binding = ActivityMainBinding.inflate(layoutInflater)

        //MainViewModelの生成
        val viewmodel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        viewmodel.inputText.observe(this, Observer { input ->
            if (input != null) {
                //UI更新
                binding.output.text = input
            }
        })

        //BindingインスタンスにViewModelを追加
        binding.viewmodel = viewmodel

        //TextWatcherで入力監視
        input.addTextChangedListener(object : TextWatcher { ...
            override fun onTextChanged(s: CharSequence?, ...) {
                //ここでBinding#viewmodelを参照
                if (s != null) {
                    binding.viewmodel?.setInputText(s.toString())
                }
            }
        })
    }
}

少し長いので、要素ごとに見てみます。


View の生成

        //Viewの生成(inflate)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        //または次も可
        //val binding = ActivityMainBinding.inflate(layoutInflater)

もともとあったsetContentViewDataBindingUtil.setContentView<T>に置き換えます。
簡単に言えば、Data Binding Layout File を使う場合、DataBindingUtil.setContentView<T>を用いてViewを生成します。

ViewModel の生成

次に ViewModel を生成します。
ViewModel はActivity#onCreateで生成するのが鉄則です。

        //MainViewModelの生成
        val viewmodel = ViewModelProviders.of(this).get(MainViewModel::class.java)

ViewModel のインスタンス化は、ViewModelProvidersの静的メソッドにより生成します。

値更新時の動作の実装

次に、inputText (MutableLiveData型)setValueメソッドが呼ばれた(=値が更新された)ときの動作を定義します。

        viewmodel.inputText.observe(this, Observer { input ->
            if (input != null) {
                //UI更新
                binding.output.text = input
            }
        })

binding.outputは TextView です。text要素を入力された値(input)に更新します。
これにより、「何らかの動作」でinputText.setValueがコールされたとき、Observerの匿名メソッドが読まれてoutputが更新されます。
「何らかの動作」部分は、TextWatcher にて実装します。(後述)

Binding に ViewModel をセット

次に、View と ViewModelを紐づけます。
これによりbinding.viewmodel?.[メソッド]といった形で、ViewModel のメソッドがコールできるようになります。View への処理を ViewModel に委譲することができます。*2

        //BindingインスタンスにViewModelを追加
        binding.viewmodel = viewmodel

「何らかの動作」の実装

今回は EditText を入力として使っているので、EditText に TextWatcherを追加して随時イベントが発生するようにしてみます。*3

        //TextWatcherで入力監視
        input.addTextChangedListener(object : TextWatcher { ...
            override fun onTextChanged(s: CharSequence?, ...) {
                //ここでActivityMainBinding#viewmodelを参照
                if (s != null) {
                    binding.viewmodel?.setInputText(s.toString())
                }
            }
        })

binding.viewmodel?.setInputTextにより次のメソッドが呼ばれます。(再掲)

class MainViewModel : ViewModel() {
    val inputText = MutableLiveData<String>()

    fun setInputText(s: String) {
        inputText.value = s
    }
}

これで入力に応じて随時同じ文字列が出力されるようになりました。長い。

制御の流れ

文字列を入力する

TextWatcher のbinding.viewmodel?.setInputText(s.toString())が呼ばれる

inputText.value = sが読まれる

更新通知

Observerの匿名メソッドが読まれる

binding.output.text = inputでUI更新

流れは上記のようになりますが、、、うーん分かりづらい。


次回は少し発展させて Activity に Fragment 載せた形で実装してみようと思いますが、この文章力で書き続けていいものか不安すぎる(´・ω・`)

*1:Java的に書けば inputText.setValue

*2:今回は行っていませんが、BluePrints ではフィールド変数に binding: ActivityMainBinding として置いておき、他のメソッドから参照できるようにしているみたいです。

*3:TextWacher#onTextChanged により、文字入力のたびに更新が行われます。