ViewModel内でデータベースへアクセス
このMainViewModelにprepareCocktailNote()メソッドを追加するとします。このメソッドは、図1の画面上のリストをタップした時に呼び出されるメソッドであり、このメソッド内でデータベースにアクセスして、タップされたカクテルのデータを呼び出し、そのデータをフィールド/プロパティに格納するメソッドとします。そして、その際にRoomを利用します。すると、Javaではリスト3、Kotlinではリスト4のようなコードになります。
public void prepareCocktailNote() { //フィールドのカクテルメモデータを初期化。 _cocktailNote = ""; //DAOオブジェクトを取得。 CocktailmemoDAO cocktailmemoDAO = _db.createCocktailmemoDAO(); //(1) //主キー検索を実行。 ListenableFuture<Cocktailmemo> future = cocktailmemoDAO.findByPK(_cocktailId); try { //検索結果を取得。 Cocktailmemo cocktailmemo = future.get(); //検索結果がnullじゃなかったら… if(cocktailmemo != null) { //データベースのカクテルメモデータをフィールドに格納。 _cocktailNote = cocktailmemo.note; } } catch(ExecutionException ex) { : } catch(InterruptedException ex) { : } }
suspend fun prepareCocktailNote(): Job { // (1) //DAOオブジェクトを取得。 val cocktailmemoDAO = _db.createCocktailmemoDAO() // (2) //コルーチンスコープの準備。 val job = viewModelScope.launch { // (3) //主キー検索を実行。 val cocktailmemo = cocktailmemoDAO.findByPK(cocktailId) //データベースのカクテルメモデータをフィールドに格納。 cocktailNote = cocktailmemo?.note ?: "" } //コルーチンスコープの戻り値をリターン。 return job // (4) }
リスト3のJavaコードに関しては、特に問題はないと思います。コメントを頼りにすると理解してもらえるコードだと思います。一方、リスト4のKotlinコードに関して、少し補足しておきます。
前回紹介したように、KotlinのDAOメソッドは、コルーチンを利用したsuspendメソッドとなっています。このメソッドを利用する場合は、コルーチンスコープが必要になります。前回のリスト10ではこのコルーチンスコープとしてlifecycleScopeのコードを掲載していますが、ViewModel内でコルーチンスコープを用意する場合は、専用のviewModelScopeがあるので、これを利用します。それが、(3)です。
ただし、prepareCocktailNote()メソッド内でコルーチンスコープを利用した時点で、そのメソッド自身もsuspendメソッドになるので、(1)のようにsuspendキーワードをメソッド宣言に付与します。さらに、このviewModelScopeのlaunchの戻り値であるJobオブジェクトをこのメソッドの戻り値としてリターンしておきます。それが(4)です。こうすることで、このメソッドを利用するMainActivityでは、リスト5のコードを記述することで、prepareCocktailNote()メソッド内の処理終了を待つことができ、その結果、データベースから取得した値を画面表示に利用できるようになります。
lifecycleScope.launch { val job = _mainViewModel.prepareCocktailNote() job.join() : etNote.setText(_mainViewModel.cocktailNote) }
[NOTE]アクティビティのコルーチンスコープは避けるべき
ここで紹介したように、ViewModelのメソッド内でコルーチンスコープを利用するためにsuspendメソッドとして、アクティビティ内でさらなるコルーチンスコープを用意するコーディングパターンは、あまりよくありません。そもそも、たとえlifecycleScopeというコルーチンスコープが用意されていても、アクティビティでのコルーチンの実行は避けるべきです。なぜなら、アクティビティではライフサイクルへの依存が発生し、コルーチンの中断、開始の適切な処理が難しいからです。そのため、コルーチンの実行はViewModelに任せます。ただし、ViewModelでコルーチンの実行を完結させようとすると、Flowという仕組みを利用する必要があります。こちらに関しては、次回紹介します。
アプリケーションコンテキストが必要な_db
ここまでの内容で、すでに問題が含まれています。それは、リスト3の(1)やリスト4の(2)の_dbをどのように用意するかということです。この_dbは、ViewModelクラスのフィールドで保持したRoom Database(AppDatabase)オブジェクトを表します。
そして、前回のリスト9やリスト10で紹介したように、このRoom Databaseのインスタンスの取得には、アプリケーションコンテキストが必要です。これは、前回のリスト7やリスト8にあるシングルトンパターンでも同じであり、getDatabase()の引数として同じくアプリケーションコンテキストを渡す必要があります。そして、このアプリケーションコンテキストを、ViewModel内では用意できないという問題があります。
Applicationが利用できるAndroidViewModel
この問題を解決するために、AndroidではApplicationオブジェクトを利用できるViewModelとして、AndroidViewModelクラスが用意されています。そのため、ViewModelクラスを、AndroidViewModelクラスを継承したものとし、Javaコードとしては、リスト6のようなものになります。
public class MainViewModel extends AndroidViewModel { // (1) private AppDatabase _db; // (2) private int _cocktailId = -1; : public MainViewModel(Application application) { // (3) super(application); // (4) _db = AppDatabase.getDatabase(application); // (5) } public void prepareCocktailNote() { : } //以下アクセサメソッド : }
まず、リスト6の(1)のようにAndroidViewModelを継承したクラスとします。そして、このAndroidViewModelを継承した途端、Applicationオブジェクトを引数とするコンストラクタを定義し、内部で親クラスのコンストラクタを呼び出す必要があります。それが、(3)と(4)です。併せて、この引数のApplicationオブジェクトを利用して、AppDatabaseオブジェクトを取得し、(2)のフィールドに格納するコードをコンストラクタに記述します。それが(5)です。このコードパターンを利用することで、ViewModelクラス内でもRoom Database(AppDatabase)オブジェクトが利用できるようになります。
このコードパターンは、Kotlinでも同じであり、リスト7のコードとなります。リスト6のJavaコードをそのままKotlinコードに置き換えた内容ですので、特に問題ないでしょう。
class MainViewModel(application: Application) : AndroidViewModel(application) { private val _db: AppDatabase var cocktailId = -1 : init { _db = AppDatabase.getDatabase(application) } suspend fun prepareCocktailNote(): Job { : } }
[NOTE]AndroidViewModelの利用も避けるべき??
Android開発者向けの公式ドキュメントには、アプリのアーキテクチャを考える上での推奨事項が記載されたページがあります。そのうちのViewModelに関するセクションでは、実は、このAndroidViewModelの利用はあまり推奨されていません。これはそもそも、ViewModelが、コンテキストからは自由であるべきという考え方に基づいているからです。Applicationオブジェクトを利用する段階で、どうしてもコンテキストに依存してしまいます。
ただし、これを解決するためには、HiltのようなDIライブラリを別途利用する必要が出てきます。今回は、そこまでの内容を紹介できないことをご了承ください。