(Android StudioとKotlinを使っています)
Roomを使ってSQLiteのデータを操作するよう書き換えていたところ、
ViewModelとLiveDataも使った方が良さそうだという事がわかりました。
でも、どう連携するかわからないー…と思った時のメモです。
もくじ
基本は公式から。
こちらの、公式サイトを見ながらやり方を確認してみました。
ViewModel Overview | Android Developers
わかりやすく書かれているのですが、英語なので後から確認しやすいように、必要そうなところだけ自分なりにまとめてみました。
ViewModelとは?
<消えちゃうデータを保存しとく!>
ActivityやFragmentは、画面を回転した時などに、UI関連のデータが消えちゃったりする事があります。
でも、ViewModelを使えば、これを消さずに保存し続ける事ができます。便利便利。
使い方としては、
例えば、Activity一つに対して、ViewModelを一つ用意して、
そのViewModelにデータを保存するようにすればいいようです。
<役割を分けるために使う!>
ActivityやFragmentに
全ての処理をずらーーっと書いたりする事もありますが、
そうすると、ActivityやFragmentに負荷がかかりがちになります。
そこで、
ActivityやFragmentでは画面周りの作業だけを行い、
他の処理は他でやる、という風にすると、スッキリしてきます。
そして、処理を分割することで、
分割した処理ごとのテストもしやすくなります。
(ActivityやFragment単位で全部テストしなくちゃいけない、って事が減ります)
そのためにも、
ViewModelにはデータの保持などの処理を任せた方が良いようです。
ViewModelの使い方メモ。 + LiveDataも使う。
それでは、ViewModelを実際に使っていってみますー。
<MyViewModel.ktを準備する>
例えば、アプリでユーザーの一覧を表示する場合。
ユーザーの一覧を取得して・保持する事は、
ViewModelにやらせるようにしたいと思います。
ViewModelは、
例えば下記のような「MyViewModel.kt」というファイルを作って、
こんな風に書いていきます。
(ファイル名の「MyViewModel」の部分は、後に書くclass名に合わせて変えてください)
class MyViewModel : ViewModel() {
//ここに、保存したい情報(ユーザーの一覧)を書く。
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData().also {
loadUsers()
}
}
//保存した情報(ユーザー一覧)を取り出す
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
//ここで、ユーザーを取得するために、非同期操作を実行する
}
このサンプルの流れとしては、
1.ActivityなどからgetUsersという関数を呼ぶ。
2.getUsersは、usersを返してくれる。
usersというのは、ViewModelに保存しておきたい「ユーザー一覧」のデータ。
このusersを返すタイミングで、インスタンスを作り、非同期処理でユーザーのデータも取得してくる。
3.非同期処理の操作内容は、loadUsersという関数に書く。
という感じだと思います。
それでは、このサンプルを、上から少しずつ説明していきますー。
class MyViewModel : ViewModel() {
→この、MyViewModelというclass名は、場合により変えていきます。
例えば、「UserActivity」という名前のActivityがあったとしたら、
ViewModelは「UserViewModel」という名前にしたりするようです。
(参考:android-architecture-components/UserViewModel.kt at master · googlesamples/android-architecture-components)
private val users: MutableLiveData<List<User>> by lazy {
(コピペする時は、< は、 < に 置き換えてください)
→この場合のUserは、データベースのテーブル(@Entityで宣言したもの)型のデータです。
例えば、useid(ユーザーのID)とusename(ユーザーの名前)のようなデータが入っています。
※ここで書いた「User」という名前は、扱うテーブルの名前によって、適宜変えてみてください。
それを、何件かListに入れる形になります。
そのListに入れたデータを、MutableLiveDataとして扱っていきます。
(MutableLiveDataは、可変のLiveDataです)
こんな感じで、
Activityで使いたい「ユーザー一覧」のデータ(今回はusersという名前をつけています)を、
ViewModelに保存しています。
今回、このデータはprivate(他からは呼び出す事ができないデータ)になっています。
そのため、このデータにアクセスする時は、アクセス用の関数を作り、そこからアクセスする形になります。
by lazyは、最初に一度作れば、その後は同じインスタンスを使えるようになる、というもののようです(何度もインスタンスを作るという事がなくなるようです)。
MutableLiveData().also {
loadUsers()
→先ほどのusersは、呼び出す時にインスタンスも作るようにしてありました。
そしてここではそのインスタンスを作るタイミングで、
loadUsersという関数を呼び出して、
その関数の中で非同期処理を行なうようにしているようです。
(ここでいうloadUsersというのは、MyViewModel.ktの後ろの方に書かれている、
private fun loadUsers() の事です)
fun getUsers(): LiveData<List<User>> {
(コピペする時は、< は、 < に 置き換えてください)
→先ほどのデータ(users)は、他からは呼び出す事ができない形にしてあるため、
この「getUsers」という関数を使って呼び出して使います。
private fun loadUsers() {
→これは、MyViewModel.ktの最初の方にある、usersの近くに書かれていた、
loadUsersの内容です。
この中には、例えば、ユーザー一覧を取得するための非同期処理などを書きます。
※loadUsersという名前は、その時の処理内容によって、わかりやすい名前に変えてみてください。
<Activityなどからアクセスする>
ViewModelは準備できたので、早速使っていきます。
例えばこんな風に、Activityからアクセスするようです。
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// システムがアクティビティのonCreate()メソッドを最初に呼び出すときにViewModelを作成します。
// 再作成されたアクティビティは、最初のアクティビティによって作成されたものと同じMyViewModelインスタンスを受け取ります。
val model = ViewModelProviders.of(this)[MyViewModel::class.java]
model.getUsers().observe(this, Observer<List<User>>{ users ->
//UIを更新する
})
}
}
上記のように、ViewModelは、例えばActivityのonCreateで作成します。
val model = ViewModelProviders.of(this)[MyViewModel::class.java]
→ここでViewModelを作成しています。
※「MyViewModel」には、先ほど準備したViewModelのclass名が入ります。
model.getUsers().observe(this, Observer<List<User>>{ users ->
(コピペする時は、< は、 < に 置き換えてください)
→ここで、MyViewModel.kt 内で準備したgetUsersを呼び出しています。
返ってくるのはLiveDataなので、observeを使います。
ところで、ViewModelのライフサイクル。
Activity内で作成したViewModelは、Activityが一旦破棄されたり、再作成されたりしても
同じViewModelインスタンスが呼び出されるので、データを保持しつづけてくれます。
(そのため、画面が回転された時などにもデータが消えずに保持されます)
ただ、ViewModelはいつまでも消えない訳ではなく、
ViewModelに対するActivityが終了した時には、
ViewModelオブジェクトのonCleared()を呼び出してリソースをクリーンアップをしてくれるようです。
(複数のActivityを作成した時にも、
Activityが終了すれば一緒にViewModelもクリーンアップされるため、
ViewModelだけが消えずにメモリ上に大量に残って困る、という事もなさそうです)
ここで言う「Activityが終了する」というのは、
Finishedの状態になる(onDestroy後の状態になる)ということのようです。
(先ほどのページの、The lifecycle of a ViewModelの図参照)
(Activityを使い続けている間は、それに対するViewModelは消えず、
Activityを使い終わったら、それに対するViewModelも一緒に消えてくれるという
イメージだと思います。)
そして、先ほど「画面が回転した時に、UI関連のデータが消えちゃうことがある」と書きましたが、
これは、
デバイスの画面が回転したときなどに、
ActivityのonCreateを何度か呼び出すことがあるという事らしいです。
このonCreateするタイミングで、UI関連のデータが消えちゃったりするみたいです。
でも、そんな時でも、ViewModelならUI関連のデータなども消さずに持っていてくれるという事のようです。
他に、フラグメント間のデータを共有する役目も。
Acticity内の2つ以上のFragmentがデータのやりとりなどをすることはよくありますが、
そんな時にもViewModelは便利に使えます。
例えば、
ユーザーがリストから項目を選択するFragmentと、
選択された項目の内容を表示する別のFragmentがあった場合。
データのやりとりが面倒だったり。
片方のFragmentは作られているのに、もう片方は作られていないため、データのやりとりが難しい時があったり。
そんな時も、ViewModelを使えば、やりとりがしやすくなるようです。
サンプルのコードは、以下のような感じですー
(こちらは特に、一行ずつ内容を調べてメモして行く事をしませんでしたが、ここまでの内容をふまえれば、なんとなく理解できるような気がしました)
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this)[SharedViewModel::class.java]
} ?: throw Exception("Invalid Activity")
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
class DetailFragment : Fragment() {
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this)[SharedViewModel::class.java]
} ?: throw Exception("Invalid Activity")
model.selected.observe(this, Observer<Item> { item ->
// Update the UI
})
}
}
この方法をとると、
・Activityには、Fragment同士のやりとりについて何も書かなくていい。
・Fragment同士のやりとりは、ViewModelだけに書けばいい。
・片方のFragmentが消えても、もう片方のFragmentは通常通りに動く。
というようなメリットが出てくるようですー。
ViewModelを使うと、Loaderも便利に使える。
今までCursorLoaderのようなLoaderを使い、
UI内のデータを データベースと同期させていた場合。
そこを
ViewModel + Room + LiveData に
置き換えることができるようです。
このViewModelを使う形に置き換えると、
データベースの操作と、UIの操作を分けることができるようになるようです。
また、ViewModelを使えば、デバイスの状態が変わった時にデータが不意に消えてしまう事がなくなります。
そして、データベースが変更された時には、
1. RoomからLiveDataに通知が行く
2. LiveDataから更新されたデータを元に、UIが更新される
という流れで、画面表示を変えることができるようになるようです。
(先ほどのページの、Replacing Loaders with ViewModel の図参照)
ViewModelではKotlinコルーチンも使える
ViewModelは、Kotlinコルーチンのサポートを含むということでした。
詳しくは、Use Kotlin coroutines with Android Architecture Componentsを参照してください、という事です。
上記リンクには、ViewModelの事、LiveDataの事、Roomの事も書かれているようだったので、
こちらもザックリとメモしてみました。
→ Kotlinのコルーチンと、ViewModelScope・LifecycleScope・LiveDataなど(アプリリ)
もし興味がありましたら、見てみてください。
更に詳しい情報や、サンプルも。
もっと詳しく知りたい場合は、
先ほどのページの、
Further information にも情報が書かれていたり、
Additional resourcesには
サンプルコードや関連ブログなどがまとめられていたりするので、
そちらも見てみてくださいー。