日々是好日

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

MVVM 完全に理解した - 8

前回は Activity 1枚だけの超簡単構成だったので、今回は Activity 1枚と Fragment 1個に少しだけ発展させてみます。

機能と外観

前回とまったく同じです←
入力した文字列と同じものを出力するだけ。

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

画像は使いまわし

変更点

  • Activity に View を直接載せるのをやめて、Fragment に載せるようにした(fragment_main.xml
  • データバインディングを Fragment に置き換えた

MainViewModel.ktには修正加えていません。

手順

MainViewModel.ktの確認

手は加えていませんが、一応載せておきます。

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

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

activity_main.xmlの修正

前回は Data Binding Layout File としてlayoutタグとdataタグを使っていましたが、今回はバインドする必要がないので次のようにしました。

<FrameLayout
        android:id="@+id/frame"
        ... />

Fragment 用の FrameLayout だけ。普通の レイアウトファイルとして記述します。

fragment_main.xmlの作成

入出力の領域を Fragment で置き換えるので、Fragment 用のレイアウトファイルを作成します。
このファイルはバインディングをする必要があるので、layoutタグ等を使用していきます。

<layout>
    <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>

Activity にあった View をそのまま移植した形ですね。実際コピペした
念のためリビルドしておきましょう。

MainFragment.ktの作成

Fragment の実装をします。例によってonCreateViewに処理をすべて詰め込みました←

    override fun onCreateView(...): View? {
        //inflate
        val binding = 
            DataBindingUtil.inflate<FragmentMainBinding>(
                inflater, R.layout.fragment_main, container,false
            )

        //ActivityからViewModelの取得
        //LifecycleOwnerに紐づいたViewModelが返る
        val viewmodel = (activity as MainActivity).obtainViewModel()

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

        //ViewとViewModelとを紐づけ
        binding.viewmodel = viewmodel

        //TextWatcherの追加(省略)return binding.root
    }
...

前回と異なる部分を見ていきます。


View の inflate

        val binding = 
            DataBindingUtil.inflate<FragmentMainBinding>(
                inflater, R.layout.fragment_main, container,false
            )

        //次も可能
        val view = /*普通にView inflateする*/
        val binding = 
            FragmentMainBinding.bind(view)

        val binding = 
            FragmentMainBinding.inflate(inflater, container, false)

DataBindingUtil.inflateという静的メソッドを使用しています。
Fragment では、View の生成にinflater.inflateを使いますが、この部分をDataBindingUtil.inflateが担当します。

View の取得はどうするのかというと、binding.root(=getRoot)で View を取り出すことができます。

ViewModel の取得

ViewModel はライフサイクルの関係上Activity#onCreateで生成するのが鉄則なので、activity(=getActivity)を MainActivity にキャストしてobtainViewModelというメソッドにより取得しています。
obtainViewModelの実装は後述です。

値更新時の動作の実装

ViewModel は Activity より長寿命なので Activity#onCreateで生成しますが、LiveData#observeで Observer を実装するときは万が一 Fragment が死んでいると(恐らく)ヌルぽが発生するので、LifecycleOwner はthis(Fragment 自身)を指定します。

        viewmodel.inputText.observe(this, Observer { inputValue -> ... }

View を返す

        return binding.root

これで Fragment 側の実装は終了です。

MainActivity.ktの修正

次に MainActivity の実装を見ます。とはいうものの、バインディングをする必要が無くなったのでほぼただの Activity の実装になりました。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //ViewModelの生成
        //特に使うわけじゃないけど、onCreateで生成しなければならない
        obtainViewModel()

        //Fragmentに置換
        val fragment = MainFragment.newInstance()
        val transaction = supportFragmentManager.beginTransaction()
        transaction.replace(R.id.frame, fragment, MAIN_FRAG)
        transaction.commit()
    }

    fun obtainViewModel() = ViewModelProviders.of(this).get(MainViewModel::class.java)

ミソはやはりobtainViewModelメソッドですね。
別に Fragment でViewModelProviders.of(activity).get(~)とやっても差し支えないのですが、どうせonCreateで生成しなければならないのだから親側で実装したほうが安全そうです。
なお、このofメソッドの引数は LifecycleOwner 型となっており、Fragment 側で再度呼び出しても、インスタンスが同じならば紐づけされた同一の ViewModel が返ってくる仕様になっています。


修正点は以上になります。

所感

ただ Fragment を追加しただけなのでそこまで大きな変更は無かったものの、DataBindingUtil のメソッドの種類と Binding オブジェクトの生成方法が何種類もあるので少し戸惑いました。
次は Navigator インターフェースを定義して、テキストのクリア機能と大文字化、小文字化機能を追加してみようと思います。

Navigator の実装について勘違いをしていました。詳細は次回