MVVM 完全に理解した - 8
前回は Activity 1枚だけの超簡単構成だったので、今回は Activity 1枚と Fragment 1個に少しだけ発展させてみます。
機能と外観
前回とまったく同じです←
入力した文字列と同じものを出力するだけ。
画像は使いまわし
変更点
- 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 の実装について勘違いをしていました。詳細は次回