Google公式のチュートリアル
Room とフローの概要
2画面で構成され、最初の画面に内部DBから取得したバスの時刻表(バス停名・時刻)をRecycelerViewで表示。タップするとそのバス停だけの時刻表だけを表示する。
追記:
Room を使用してデータを永続化する
こっちのチュートリアルの方が詳しかった。
流れ
- Entity用意
- Dao用意(interface)。引数をFlow型にしておくとDBのデータ更新に応じてViewModel内データおよびUIも更新される
- Databaseクラス用意(RoomDatabaseを継承。利用するEntityの指定やDaoに他のクラスからアクセスできるようなメソッド定義)
- Applicationクラスを継承したクラスを用意(Databaseのインスタンスをこのクラスで行う)。AndroidManifest.xmlのタグのname属性をこのクラス名で更新する。アプリ起動時にDatabaseインスタンスが生成される?
- ViewModelのメソッドでDaoのメソッドを扱えるようにする。ViewModelをDaoを引数としてインスタンス化できるように
ViewModelProvider.Factory
なるものを定義する。Fragment内でViewModelの参照を取得する際は、このFactoryクラスを使う。
Entity
Dataクラスを用意。
/*
* バスのスケジュールの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がコンパイル時にこのクラスの実装を生成する。
@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が増えたりしたら上げていく。
/*
* モデル、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属性を下記にしておく。よくわかないがアプリ起動時にデータベースオブジェクトが使えるようになる??
class BusScheduleApplication : Application() {
val database: AppDatabase by lazy { AppDatabase.getDatabase(this) }
}
ViewModel
コンストラクタ引数があるViewModelは直接インスタンス化できないらしい。そのため、ViewModelProvider.Factoryなるものでインスタンス化を行う。引数DaoのメソッドがViewModelのメソッドとして扱えるようになっている。フラグメントではViewModelのメソッド経由でDaoを利用する。
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スレッドで行うとアプリが落ちる。
// ビューモデルへの参照を取得
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)
}
}
}