【Android】はじめてのRoom
【Android】分かった気になれる!アーキテクチャ・MVVM概説 ではアーキテクチャ・MVVMの概要をコードを記述せずに概念のみ説明してみました。
その後の投稿では、実践編として各ライブラリを実際にコードを記述し記事にしています。
これまでの記事 :
MVVMシリーズ実践編第二回目のテーマは、Roomです。
DBの一つであるSQLiteを容易に扱うことのできるRoomライブラリについて記事を書こうと思います。
下図のMVVMの構造において、Roomが担うのは黄緑の枠線で示された部分です。
参照:https://developer.android.com/jetpack/docs/guide#overview
はじめに
本記事では、Roomを使用しDB(SQLite)でのデータ保存・取得処理を実装します。
今回は、添付の画面キャプチャーのようなアプリを例に説明していきます。
このアプリは、前回記事:【Android】はじめてのDataBindingで作成したアプリにDBへのデータ保存機能を追加したものです。
「SET!!!」ボタンを押下したときに入力した文字列(Play Call)をDBに保存します。
(
Play Callとは
アメリカンフットボールの試合中に伝えられる作戦のことです。
本アプリでは、入力した文字列をPlay Callに見立てて各種オブジェクトを命名しました。
)
データ保存の流れ | 次回アプリ起動時 |
---|---|
Roomの導入
まず、appレベルのbuild.gradle
にて、dependencies
にRoomライブラリを追加しsync
を実行します。
これで、導入は完了し各種Roomが提供するコンポーネントを使用することができるようになりました!
dependencies {
// 略
// room
def room_version = "2.2.0-rc01"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
}
Roomの実装
- Database
- Entity
- DAO(Data Access Object)
Roomライブラリは上記3つのコンポーネントを提供し、それらを用いてDB(SQLite)上でデータを管理します。
クラスやインターフェースなどにこれらのアノテーションをつけることで、Roomが提供する各コンポーネントとして振舞うようになります。
それぞれのコンポーネントの説明を実際のコードを用いて行います。
@Database
Database: SQLiteと直接接続する部分になります。
アプリ内でインスタンスを生成し、DBでのデータ管理を実装します。
このRoomDatabase
クラスは、abstract class
に@Database
アノテーションをつけることで定義されます。
Google公式ガイドによると、@Database
アノテーションがついたクラスは以下の条件を満たす必要があります。
-
RoomDatabase
クラスを継承すること - Databaseに関連づけられている
Entity
のリストをアノテーションに含むこと -
@Dao
アノテーションで定義されたインターフェースDao
の抽象メソッドを含むこと
本アプリでの実装:
@Database(entities = [PlayCallEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun playCallDao(): PlayCallDao
}
@Entity
Entity: DBのテーブルを示します。
data class
に@Entity
アノテーションをつけることで定義されます。
Entity
内の要素につけることのできるアノテーションとして、主に以下のものがあげられます。
-
@PrimaryKey
主キーに設定したい要素につけます。主キーは、1つのEntity
クラスに最低1つ必要です。 -
@ColumnInfo
DB内でのカラム情報をアプリとは別で設定したい場合につけます。
このアノテーションのname
プロパティが主に使用されるのではないかと思います。アプリ内では一般的にキャメルケースで要素を命名しますが、SQLiteではテーブル名やカラム名の大文字・小文字は区別されず、スネークケースで定義されることが多いと思われます。このような理由から、ColumnInfo
アノテーションのname
プロパティを使用してアプリで定義した要素名とは別の名前でSQLiteのカラム名を設定しておくのが良いと考えられます。 -
@Ignore
このアノテーションがついた要素はDBのカラムに追加されません。つまり、そのデータは永続化されません。
本アプリでの実装:
@Entity(tableName = "play_calls")
data class PlayCallEntity(
@PrimaryKey
@ColumnInfo(name = "description")
val description: String
)
@Dao
DAO: DAO(Data Access Object)は、Databaseにアクセスするためのメソッドを格納するオブジェクト、インターフェースです。
interface
に@Dao
アノテーションをつけることで定義されます。
このインターフェース内で、以下のアノテーションをつけることで、データ挿入・削除・取得処理などを実行するメソッドを定義します。
-
挿入:
@Insert
Entity
を引数とするメソッドにつけるだけで、Entity
に基づくデータをDBに挿入できます。 -
削除:
@Delete
削除したいレコードのEntity
を引数とするメソッドにつけるだけで、DB内の対象のEntity
を削除することができます。 -
取得
@Query
アノテーション内にSQL文であるSELECT文を記述することで実装します。
SQL文についてはこちらを参照
基本的なSQL文 : Oracle公式ガイド
本アプリでの実装:
@Dao
interface PlayCallDao {
// データの取得メソッド
@Query("SELECT * FROM play_calls")
fun loadAllPlayCall(): List<PlayCallEntity>
// 挿入メソッド
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun savePlayCall(playCallEntity: PlayCallEntity)
}
使ってみる
用意したRoom
のコンポーネントを使用し、Viewを変更するよう実装します。
Databaseをインスタンス化
まず、定義したDatabaseをインスタンス化します。
今回はひとまずApplication
クラスを作成し、companion object
としてAppDatabase
のインスタンスを定義することでアプリ起動中はAppDatabase
のインスタンスを共有できるようにしました。
その後、onCreate
メソッド内でデータベースをビルドしました。
これで、AppDatabase
をアプリ内で使う準備ができました。
class Application : Application() {
companion object {
lateinit var database: AppDatabase
}
override fun onCreate() {
super.onCreate()
// AppDatabaseをビルドする
database = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"app_database"
).build()
}
}
DAO内のメソッドを呼び出す
今回はViewModel
にて、database
およびPlayCallDao
を呼び出しました。
まず、重要な部分を抽出します。
// PlayCallDaoをインスタンス化
private val dao = Application.database.playCallDao()
// daoのデータ取得処理を呼び出す
dao.loadAllPlayCall()
// daoのデータ保存処理を呼び出す
// playCallはメソッドに渡されたPlayCallクラスの引数
dao.savePlayCall(PlayCallEntity(description = playCall.first().description))
次に、実際の実装です。
DAO内のメソッドはメインスレッドからは呼び出せないため、今回はひとまずAsyncTask
を採用しています。
これをViewから呼び出すことで、最初に添付したキャプチャーのように入力履歴のリストを表示することができます。
(RecyclerView実装の説明は省略)
class FootballViewModel {
private val dao = Application.database.playCallDao()
// 略
// Fragmentから呼ばれる
fun loadPlayCallHistoryList() {
val asyncLoad = AsyncLoad(dao, this)
asyncLoad.execute()
}
// Fragmentから呼ばれる
fun savePlayCall(playCall: PlayCall) {
val asyncSave = AsyncSave(dao, this)
asyncSave.execute(playCall)
}
}
// Coroutinesを使いたい
class AsyncLoad(private val dao: PlayCallDao, private val viewModel: FootballViewModel) : AsyncTask<Void, Void, List<PlayCall>>() {
override fun onPreExecute() {}
override fun doInBackground(vararg voids: Void): List<PlayCall>? {
val playCallMutableList = mutableListOf<PlayCall>()
dao.loadAllPlayCall().forEach { playCall ->
playCallMutableList.add(PlayCall(description = playCall.description))
}
return playCallMutableList
}
override fun onPostExecute(listOfPlayCalls: List<PlayCall>) {
viewModel.setPlayCallHistoryList(listOfPlayCalls)
}
}
class AsyncSave(private val dao: PlayCallDao, private val viewModel: FootballViewModel) : AsyncTask<PlayCall, PlayCall, Void>() {
override fun onPreExecute() {}
override fun doInBackground(vararg playCall: PlayCall): Void? {
dao.savePlayCall(PlayCallEntity(description = playCall.first().description))
return null
}
override fun onPostExecute(result: Void?) {
val asyncLoad = AsyncLoad(dao, viewModel)
asyncLoad.execute()
}
}
(
今後の編集予定箇所:
-
DIを実装する
Repository
でデータ管理に関する処理を実装できるよう修正したいです。
この程度のアプリではほとんど意味がありませんが、Repository
でラップしDIすることで以下のような様々な利点があるからです。
DIを実装し、あるデータの処理に関するメソッドを1つのRepository
に集約することで、
1.そのデータを扱う各ViewModel
の肥大化を抑えることができる
2.ViewModel
では、データの参照元(ローカル?リモート?どのDB?)を気にする必要がなくなる
3.修正時は、Repository
を修正するだけでよい
4.テストをしやすくなる(この利点を感じられるほど、私は十分にテストしたことがありませんが) -
Coroutinesの適用
現在AsyncTask
でDAO
を呼び出していますが、Coroutines
に移行したいです。
AsyncTask
だとデータを取得するためのクラスや保存するためのクラスを独自に定義する必要があり、ViewModelのファイルが肥大化してしまうからです。
また、コンストラクタとしてViewModel
やDao
を渡しており、この部分が回りくどく冗長であると感じるからです。
)
-> 2020/05/14 更新しました!
Coroutines を用いることで、非同期処理を1つのメソッドで簡潔に書けるようになりました。
詳しくは、【Android】はじめてのCoroutines をご覧ください。
class FootballViewModel {
private val dao = Application.database.playCallDao()
// 略
fun loadPlayCallHistoryList() {
val playCallMutableList = mutableListOf<PlayCall>()
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.Default) {
dao.loadAllPlayCall().forEach { playCall ->
playCallMutableList.add(PlayCall(description = playCall.description))
}
}
setPlayCallHistoryList(playCallMutableList)
}
}
fun savePlayCall(playCall: PlayCall) {
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.Default) {
dao.savePlayCall(PlayCallEntity(description = playCall.description))
}
loadPlayCallHistoryList()
}
}
}
まとめ
今回は、Roomを用いてSQLiteでのデータ管理を実装してみました。
SQLiteの知識に乏しいため不明な点も残っていますが、基本は抑えられたのではないかと考えています。
今後は、CoroutinesやDIの実装を進めアーキテクチャMVVMシリーズ実践編を完結する予定です。
コメント、編集依頼は絶賛募集中です!
ソースコード
ソースコードはGitHubにあげています。
Discussion