Roomの使い方公式チュートリアルをやったメモ

# Google公式のチュートリアル
[Room とフローの概要](https://developer.android.com/codelabs/basic-android-kotlin-training-intro-room-flow?hl=ja&continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-kotlin-unit-5-pathway-1%3Fhl%3Dja%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-training-intro-room-flow#0)
2画面で構成され、最初の画面に内部DBから取得したバスの時刻表（バス停名・時刻）をRecycelerViewで表示。タップするとそのバス停だけの時刻表だけを表示する。

追記：
[Room を使用してデータを永続化する](https://developer.android.com/codelabs/basic-android-kotlin-training-persisting-data-room?hl=ja&continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-kotlin-unit-5-pathway-2%3Fhl%3Dja%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-training-persisting-data-room#0)
こっちのチュートリアルの方が詳しかった。

# 流れ
- Entity用意
- Dao用意（interface）。引数をFlow型にしておくとDBのデータ更新に応じてViewModel内データおよびUIも更新される
- Databaseクラス用意（RoomDatabaseを継承。利用するEntityの指定やDaoに他のクラスからアクセスできるようなメソッド定義）
- Applicationクラスを継承したクラスを用意（Databaseのインスタンスをこのクラスで行う）。AndroidManifest.xmlの<application>タグのname属性をこのクラス名で更新する。アプリ起動時にDatabaseインスタンスが生成される？
- ViewModelのメソッドでDaoのメソッドを扱えるようにする。ViewModelをDaoを引数としてインスタンス化できるように`ViewModelProvider.Factory`なるものを定義する。Fragment内でViewModelの参照を取得する際は、このFactoryクラスを使う。

# Entity
Dataクラスを用意。

```Kotlin:Schedule.kt
/*
* バスのスケジュールのEntity(Roomが手キーやカラム名を識別するアノテーションを記載)
* */
@Entity
data class Schedule(
    @PrimaryKey val id: Int,

    @NonNull
    @ColumnInfo(name = "stop_name")
    val stopName: String,

    @NonNull
    @ColumnInfo(name = "arrival_time")
    val arrivalTime: Int
)
```
# Dao
selectやinsert などSQLに応じたアノテーションを記載。メソッドの引数はFlowを指定。DB内のリストが変更になったら自動でUIも更新される。アプリ完成後、試しに[View]->[Tool Window]->[App Inspection]->[DataBase]でINSERT文発行するとリストが更新される。MyBatisっぽい。interfaceだけど、Roomがコンパイル時にこのクラスの実装を生成する。
```Kotlin:ScheduleDao.kt
@Dao
interface ScheduleDao {
    // バス停と時刻表をすべて取得(フローを使用して、DBの更新をすぐUIに反映)
    @Query("SELECT * FROM schedule ORDER BY arrival_time ASC")
    fun getAll(): Flow<List<Schedule>>

    // バス停名で時刻表を取得(フローを使用して、DBの更新をすぐUIに反映)
    @Query("SELECT * FROM schedule WHERE stop_name = :stopName ORDER BY arrival_time ASC")
    fun getByStopName(stopName: String): Flow<List<Schedule>>
}
```
# Databaseクラス
RoomDatabaseクラスを継承したDataBaseクラスを用意。使用するエンティティを Room に伝え、DAO にアクセスできるようにし、データベースの作成時のセットアップを行う。今回は、asettesフォルダ配下のbus_schedule.dbを読み込んで初期化している。Entityが増えたら、@Database内のentitiesに追加。versionはカラムが増えたりEntityが増えたりしたら上げていく。
```Kotlin:AppDatabase.kt
/*
* モデル、DAO、データベースのセットアップを行うクラス
* */
@Database(entities = arrayOf(Schedule::class), version = 1)
abstract class AppDatabase : RoomDatabase() {

    /*
    * Daoを返す（他のクラスがDaoにアクセスできるように）
    * */
    abstract fun scheduleDao(): ScheduleDao

    /*
    * 既存のAppDatabaseインスタンスを返すメソッド、または初回データベースを作成
    * */
    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context,
                    AppDatabase::class.java,
                    "app_database"
                )
                    .createFromAsset("database/bus_schedule.db")
                    .build()
                INSTANCE = instance

                instance
            }
        }
    }
}
```
# Applicationクラス
Applicationクラスを継承したクラスを用意し、databaseのインスタンスをこのクラスで行う。このdatabaseをDatabaseクラスのgetDatabaseメソッドで初期化。ViewModelのコンストラクタに、このdatabaseを渡して、Daoのメソッドを利用する。AndroidManifest.xmlのapplicationタグのname属性を下記にしておく。よくわかないがアプリ起動時にデータベースオブジェクトが使えるようになる？？
```Kotlin:BusScheduleApplication.kt
class BusScheduleApplication : Application() {
    val database: AppDatabase by lazy { AppDatabase.getDatabase(this) }
}
```
# ViewModel
コンストラクタ引数があるViewModelは直接インスタンス化できないらしい。そのため、ViewModelProvider.Factoryなるものでインスタンス化を行う。引数DaoのメソッドがViewModelのメソッドとして扱えるようになっている。フラグメントではViewModelのメソッド経由でDaoを利用する。
```Kotlin:BusScheduleViewModel.kt
class BusScheduleViewModel(private val scheduleDao: ScheduleDao) : ViewModel() {

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

    fun scheduleForStropName(name: String): Flow<List<Schedule>> = scheduleDao.getByStopName(name)
}

/*
* ビューモデルをインスタンス化するファクトリークラス
* */
class BusScheduleViewModelFactory(private val scheduleDao: ScheduleDao) :
    ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(BusScheduleViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return BusScheduleViewModel(scheduleDao) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}
```
# FragmentでのViewModelの参照とUIへのデータ表示
下記のようにViewModelへの参照を設定。Applicationクラスを継承したクラス内でのdatabaseフィールドによりviewModel経由で、Daoのメソッドを利用しUIにデータを反映する。DBのデータ取得はコルーチンで行う。UIスレッドで行うとアプリが落ちる。
```Kotlin:FullScheduleFragment.kt
// ビューモデルへの参照を取得
private val viewModel: BusScheduleViewModel by activityViewModels {
    BusScheduleViewModelFactory((activity?.application as BusScheduleApplication).database.scheduleDao())
}

// 中略

// viewModelの値を表示
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    recyclerView = binding.recyclerView
    recyclerView.layoutManager = LinearLayoutManager(requireContext())

    // リストタップしたときの処理（タップしたバス停名の時間を表示するフラグメントに遷移）
    val busStopAdapter = BusStopAdapter {
        val action =
            FullScheduleFragmentDirections.actionFullScheduleFragmentToStopScheduleFragment(
                stopName = it.stopName
            )
        view.findNavController().navigate(action)
    }
    recyclerView.adapter = busStopAdapter

    // submitList()でDBアクセスする。UIをロックしないようにコルーチンで行う。
    lifecycle.coroutineScope.launch {
        viewModel.fullSchedule().collect {
            busStopAdapter.submitList(it)
        }
    }
}
```