カテゴリー
Kotlin言語

Android:Kotlin: ViewModel(Room+Flow)でデータベースを利用する

Android Studio(Kotlin)によるアプリ作成で ViewModel を使ってデータベース(SQLite)を利用できるようにします。Room と Flow を活用します


説明用のコードは Android Developper にある「Room によるデータの読み取りと更新」のスターターコードを基本にしました

Android Studio は、バージョン 2020.3.1 (Arctic Fox) 以降を使ってくださいね

ViewModel を使ってデータベースを操作する手順

MVVM(Model-View-ViewModel)っていう考え方に基づいた構造になってるんです

  1. アプリの build.gradle に Room , Lifecycle , Coroutine を使うための設定(依存関係)を追加します
  2. エンティティクラス:エンティティ(テーブル)のクラスを作成します
  3. DAOクラス:データベース操作用のクラス(DAOクラス)を作成します
  4. データベースクラス:データベース生成用のクラス を作成します
  5. Applicationクラス:Application クラスを継承したデータベースに対応したクラスを作成します。AndroidManifest.xml に登録してデータベースのインスタンスを生成します
  6. ViewModelクラス:ViewModel でデータベースを操作するためのメソッドを作成します。引数でDAOクラスのインスタンスを指定します。Flow は LiveData に変換しておきます
  7. データベースを利用するためのコードの記述をします。データベースから取得したデータは LiveData に変換してあるので observe で検知できます

build.gradle の設定

Android Studio でアプリのプロジェクトを開いたら、build.gradle(アプリ)に次の行を追加します

dependencies {
    def lifecycle_version = '2.2.0'
    def room_version = '2.3.0'

    // Lifecycle libraries
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

    // Room libraries
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version"

    ・・・
}

build.gradle を変更したときは、最後に Sync Now をクリックしてくださいね

エンティティ(テーブル)のクラスの作成

まずテーブルの構造を決めて…

  • Item という名前のエンティティクラスを作成します。SQLite のデータベースには Item という名前のテーブルが作成されます
  • @ColumnInfo の name で SQLite データベース上のカラム名を指定します(プロパティ名と一致するときは省略可能:大文字と小文字は区別されません)
@Entity
data class Item(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    @ColumnInfo(name = "name")
    val itemName: String,
    @ColumnInfo(name = "price")
    val itemPrice: Double,
    @ColumnInfo(name = "quantity")
    val quantityInStock: Int
)

データベース操作用のクラス(DAOクラス)の作成

テーブルを操作する DAO クラスを作成

  • Item クラス(エンティティ)を操作するための ItemDao インタフェースを作成しています
  • データベースを操作するクエリーを、Kotlin の関数に結び付けます
  • コルーチン内で使う関数なので、suspend 関数とします
  • レコードを返す関数は、戻り値を Flow<T> としておきます
@Dao
interface ItemDao {

    @Query("SELECT * from item ORDER BY name ASC")
    fun getItems(): Flow<List<Item>>

    @Query("SELECT * from item WHERE id = :id")
    fun getItem(id: Int): Flow<Item>

    // Specify the conflict strategy as IGNORE, when the user tries to add an
    // existing Item into the database Room ignores the conflict.
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(item: Item)

    @Update
    suspend fun update(item: Item)

    @Delete
    suspend fun delete(item: Item)
}

データベース生成用のクラス の作成

データベースを生成するクラスを作りますね

  • データベースのインスタンスを生成するための ItemRoomDatabase クラスを作成します
  • データベースのインスタンスを返す getDatabase() メソッドを含みます
  • ItemDao を返す itemDao() メソッドを含みます
  • アプリで使うデータベースファイルの名前は item_database です
  • fallbackToDestructiveMigration() を指定しておきます。デバイス上に使用するバージョンのデータベースが見つからないときに初期状態のデータベースが作成されます
  • このコードはテンプレ的に使います
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase : RoomDatabase() {

    abstract fun itemDao(): ItemDao

    companion object {
        @Volatile
        private var INSTANCE: ItemRoomDatabase? = null

        fun getDatabase(context: Context): ItemRoomDatabase {
            // if the INSTANCE is not null, then return it,
            // if it is, then create the database
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    ItemRoomDatabase::class.java,
                    "item_database"
                )
                    // Wipes and rebuilds instead of migrating if no Migration object.
                    // Migration is not part of this codelab.
                    .fallbackToDestructiveMigration()
                    .build()
                INSTANCE = instance
                // return instance
                instance
            }
        }
    }
}

Application を継承したクラスの作成

実際にデータベースのインスタンスを生成するコードを記述します

  • データベースのインスタンス用のプロパティは、Application を継承したクラスで設定します
  • InventoryApplication という名前のクラスを Application クラスを継承して作成します
  • データベースのインスタンスになるプロパティ database を設定します。by lazy を使って、初めて呼び出されたときに初期化されるようにします
  • データベースのインスタンスは getDatabase() を使って取得します
  • InventoryApplication は AndroidManifest.xml に登録します

by lazy は val で宣言したものに使うの

lateinit と違って、後から変更できないんですね

うん、でも by lazy のほうが初期化の処理を一緒に書けるのでちょっと見やすいかも

class InventoryApplication : Application() {
// Using by lazy so the database is only created when needed
// rather than when the application starts
val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}

ViewModel クラスの作成

データベースの操作を ViewModel から行えるようにしちゃいます

Flow は LiveData に変換できますね

  • データベースの操作を ViewModel から行うようにします
  • DAOクラスのメソッドを ViewModel のメソッドとして利用できるようにします
  • ViewModel の初期化で、引数としてDAOクラスのインスタンスを渡すようにします
  • Flow は asLiveData を使って LiveData に変換できます
  • DAOクラスの中で suspend を使って宣言されたメソッドは、コルーチン(viewModelScope.launch {・・・})の中で使います
  • 引数のある ViewModel をインスタンス化するには、直接インスタンス化するのではなく ViewModelProvider.Factory を作成して利用します

引数のある ViewModel は、直接インスタンス化できないんです

class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {

    // Cache all items form the database using LiveData.
    val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData()

    ・・・

    /**
     * Launching a new coroutine to update an item in a non-blocking way
     */
    private fun updateItem(item: Item) {
        viewModelScope.launch {
            itemDao.update(item)
        }
    }

    ・・・
}

/**
 * Factory class to instantiate the [ViewModel] instance.
 */
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return InventoryViewModel(itemDao) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

データベースを利用するためのコード

アクティビティやフラグメントで使うときは、まず ViewModel のインスタンスを用意するんですね

うん、そうすれば ViewModel のメソッドでデータベースを参照できるの

すっきりして分かりやすそう

データベースから Flow で取得したデータも、 ViewModel の中で LiveData に変換しておけば、検知は observe で大丈夫

  • フラグメント ItemListFragment.kt からデータベースを利用してみます
  • InventoryViewModel のインスタンス viewModel を InventoryViewModelFactory を使って生成します
  • viewModel の中で asLiveData を使って Flow から LiveData に変換したプロパティの変化は observe で検知します
class ItemListFragment : Fragment() {
    private val viewModel: InventoryViewModel by activityViewModels {
        InventoryViewModelFactory(
            (activity?.application as InventoryApplication).database.itemDao()
        )
    }

    ・・・

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ・・・
        viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
            items.let {
                adapter.submitList(it)
            }
        }
        ・・・
    }
}

  • Flow を LiveData に変換せずにそのまま検知するには collect を使います(collect はコルーチンの中で使う必要があります)
  • Android Developper にある「Room とフローの概要」のスターターコードでは次のようになります

DAOクラス(ScheduleDaoクラス)

@Query("SELECT * FROM schedule ORDER BY arrival_time ASC")
fun getAll(): Flow<List<Schedule>>

ViewModel のメソッド(ScheduleListViewModelクラス)

fun fullSchedule(): Flow<List<Schedule>> = scheduleDao.getAll()

Fragment での検知(FullScheduleFragmentクラス)

lifecycle.coroutineScope.launch {
    viewModel.fullSchedule().collect() {
        busStopAdapter.submitList(it)
    }
}