【Android】はじめてのRoom

[【Android】分かった気になれる！アーキテクチャ・MVVM概説](https://qiita.com/iTakahiro/items/6b1b22efa69e55cea3fa) ではアーキテクチャ・MVVMの概要をコードを記述せずに概念のみ説明してみました。
その後の投稿では、実践編として各ライブラリを実際にコードを記述し記事にしています。
**これまでの記事** :

- [【Android】分かった気になれる！アーキテクチャ・MVVM概説](https://qiita.com/iTakahiro/items/6b1b22efa69e55cea3fa) 
- [【Android】はじめてのDataBinding](https://qiita.com/iTakahiro/items/b5fe2b186750c6e774e5)

MVVMシリーズ実践編第二回目のテーマは、[Room](https://developer.android.com/training/data-storage/room/index.html)です。
DBの一つであるSQLiteを容易に扱うことのできるRoomライブラリについて記事を書こうと思います。

下図のMVVMの構造において、Roomが担うのは黄緑の枠線で示された部分です。
参照：https://developer.android.com/jetpack/docs/guide#overview

![mvvm_room.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296819/758b20f1-62d2-85ff-1d95-6334140b06b2.png)

# はじめに
本記事では、Roomを使用しDB(SQLite)でのデータ保存・取得処理を実装します。
今回は、添付の画面キャプチャーのようなアプリを例に説明していきます。
このアプリは、[前回記事：【Android】はじめてのDataBinding](https://qiita.com/iTakahiro/items/b5fe2b186750c6e774e5)で作成したアプリにDBへのデータ保存機能を追加したものです。
「SET!!!」ボタンを押下したときに入力した文字列(Play Call)をDBに保存します。

(
Play Callとは
アメリカンフットボールの試合中に伝えられる作戦のことです。
本アプリでは、入力した文字列をPlay Callに見立てて各種オブジェクトを命名しました。
)

|データ保存の流れ|次回アプリ起動時|
|---|---|
|![room_american_football.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296819/af7a5b11-c6dd-6ba7-ec29-110ca5664d06.gif)|![Screenshot_1577005696.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296819/89f962d4-b986-6409-626b-5ef962b3c7ed.png)|

# Roomの導入
まず、appレベルの`build.gradle`にて、`dependencies`にRoomライブラリを追加し`sync`を実行します。
これで、導入は完了し各種Roomが提供するコンポーネントを使用することができるようになりました！

```gradle:build.gradle(app)
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公式ガイド](https://developer.android.com/training/data-storage/room)によると、`@Database`アノテーションがついたクラスは以下の条件を満たす必要があります。

- `RoomDatabase`クラスを継承すること
- Databaseに関連づけられている`Entity`のリストをアノテーションに含むこと
- `@Dao`アノテーションで定義されたインターフェース`Dao`の抽象メソッドを含むこと

**本アプリでの実装：**

```kotlin:AppDatabase.kt
@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のカラムに追加されません。つまり、そのデータは永続化されません。

**本アプリでの実装：**

```kotlin:PlayCallEntity.kt
@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文](https://docs.oracle.com/cd/E57425_01/121/LNPCC/GUID-431532CF-1652-4EB6-AD2A-7E695744FADE.htm) : Oracle公式ガイド

**本アプリでの実装：**

```kotlin
@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`をアプリ内で使う準備ができました。

```kotlin:Application.kt
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`を呼び出しました。
まず、重要な部分を抽出します。

```kotlin
// PlayCallDaoをインスタンス化
private val dao = Application.database.playCallDao()

// daoのデータ取得処理を呼び出す
dao.loadAllPlayCall()

// daoのデータ保存処理を呼び出す
// playCallはメソッドに渡されたPlayCallクラスの引数
dao.savePlayCall(PlayCallEntity(description = playCall.first().description))
```

次に、実際の実装です。
DAO内のメソッドはメインスレッドからは呼び出せないため、今回はひとまず`AsyncTask`を採用しています。
これをViewから呼び出すことで、最初に添付したキャプチャーのように入力履歴のリストを表示することができます。
（RecyclerView実装の説明は省略）

```kotlin:FootballViewModel.kt
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](https://qiita.com/iTakahiro/items/e39533b7fc07573a14da) をご覧ください。

```kotlin

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](https://developer.android.com/training/data-storage/room)を用いてSQLiteでのデータ管理を実装してみました。
SQLiteの知識に乏しいため不明な点も残っていますが、基本は抑えられたのではないかと考えています。
今後は、CoroutinesやDIの実装を進めアーキテクチャMVVMシリーズ実践編を完結する予定です。

コメント、編集依頼は絶賛募集中です！

### ソースコード
ソースコードはGitHubにあげています。
https://github.com/iTakahiro/ArchitectureFootball

### 関連記事
- [【Android】分かった気になれる！アーキテクチャ・MVVM概説](https://qiita.com/iTakahiro/items/6b1b22efa69e55cea3fa) 
- [【Android】はじめてのDataBinding](https://qiita.com/iTakahiro/items/b5fe2b186750c6e774e5)

### 参考にした資料
- [Room](https://developer.android.com/training/data-storage/room) : Google公式ガイド
- [基本的なSQL文](https://docs.oracle.com/cd/E57425_01/121/LNPCC/GUID-431532CF-1652-4EB6-AD2A-7E695744FADE.htm) : Oracle公式ガイド