Room + Coroutines + Koin + Kotlin構成での実装

## Room

[公式ページ](https://developer.android.com/jetpack/androidx/releases/room)

今回は `Kotlin + Room + Coroutines + Koin` の構成で進めていきたいと思います。

### 構成要素

<img src="https://img.esa.io/uploads/production/attachments/5529/2019/06/25/18452/0082decb-dd61-4eab-b643-9d103b039636.png" width="420" />


※公式ページから引用

以下の3つの要素で構成されている

・[Database](https://developer.android.com/reference/androidx/room/Database.html): データベース内のエンティティとデータアクセスオブジェクトのリストを定義
・[Entity](https://developer.android.com/reference/androidx/room/Entity.html): 各エンティティに対して、アイテムを保持するためのデータベーステーブルが作成される
・[Dao](https://developer.android.com/reference/androidx/room/Dao.html): データベースにアクセスするメソッドを定義する役割を果たす

###  :computer:環境構築
****

必要なパッケージをインストールします。

```gradle:app/build.gradle
    def koin_version = '1.0.2'
    implementation "org.koin:koin-android:$koin_version"
    implementation "org.koin:koin-androidx-scope:$koin_version"
    implementation "org.koin:koin-androidx-viewmodel:$koin_version"

    def coroutines_version = '1.2.2'
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
    // viewmodel + coroutines
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha02"

    def room_version = "2.1.0"
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
```

### :pencil: 実装
****
簡単なサンプルとしてメッセージを扱う構成のDatabaseを実装していきます。

#### Entity作成

まずメッセージの所有者を扱うユーザー管理用のEntityを作成します。

```kotlin
@Entity
data class UserData(
    @PrimaryKey(autoGenerate = true)
    val id: Long,
    val name: String
)
```
`id` に関しては `@PrimaryKey(autoGenerate = true)`で主キーかつ
自動採番するように設定しています。

次ににメッセージを保存するEntityを作成

```kotlin

@Entity(foreignKeys = [ForeignKey(
    entity = UserData::class,
    parentColumns = arrayOf("id"),
    childColumns = arrayOf("owner_id")
)])

data class MessageData(
    @PrimaryKey(autoGenerate = true)
    val id: Long,
    @ColumnInfo(name = "owner_id")
    val ownerId: Long,
    val body: String,
    val type: Int,
    @ColumnInfo(name = "is_owner")
    val isOwner: Boolean,
    @ColumnInfo(name = "is_delete")
    val isDelete: Boolean,
    @ColumnInfo(name = "created_at")
    val createdAt: Date
) 
```
`ForeignKey` で外部キーを`owner_id` に設定し、
1つ前に作成した `UserData` の `id` と紐づけています。
`@ColumnInfo(name = "xxxxx")` では名前が実際の列名と異なる場合に設定しています。

### Converter作成

Entityの `MessageData` で `Date` 型を使用していますが、
実際のDBデータにはDate型は無い為、変換が必要になります。

```kotlin
class DateConverter {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date?
        = value?.let { Date(it) }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long?
        = date?.let { it.time }
}
```
上記では、Date型をLongの値へ変換しDBデータとして格納するように変換しています。

### Dao作成

ユーザーを検索したり登録するDaoを作成します。

```kotlin
@Dao
interface UserDao {
    // 登録されている全ユーザーを取得
    @Query("SELECT * FROM UserData")
    suspend fun findAll(): List<UserData>
    // userIdで指定したユーザーを検索
    @Query("SELECT * FROM UserData WHERE id = :userId")
    suspend fun find(userId: Long): UserData
    // ユーザー登録
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun add(userData: UserData)
}
```

メッセージ一覧の取得や、登録を行うDaoを作成

```kotlin
@Dao
interface MessageDao {
    // メッセージ登録
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun add(messageData: MessageData)
    // 全メッセージ取得
    @Query("SELECT * FROM MessageData")
    suspend fun findAll(): List<MessageData>
}
```

### Database作成

```kotlin
@androidx.room.Database(entities = [
    MessageData::class,
    UserData::class
], version = 1)
@TypeConverters(DateConverter::class)
abstract class Database: RoomDatabase() {
    abstract fun messageDao(): MessageDao
    abstract fun userDao(): UserDao
}
```
EntitiyとConverter、Dao生成メソッドを定義しています。

### Koinを使用してDI

```kotlin
object KoinModule {

    fun appModule() = module {
        // Databaseインスタンスをシングルトンで生成
        single { Room.databaseBuilder(androidContext(), Database::class.java, "xxx.db").build() }
    }

    fun messageModule() = module {
        // Daoを生成
        factory { get<Database>().messageDao() }
        // viewModelのinjection
        viewModel { MessageViewModel(get()) }
        // repositoryのinjection
        single { MessageRepository(get()) }
    }
}
```
### 使用する

- MessageRepository

```kotlin
class MessageRepository(private val messageDao: MessageDao) {

    /**
     * Query all messages.
     */
    suspend fun getMessages(): List<MessageData>
        = messageDao.findAll()
}
```

- ViewModel

```kotlin
class MessageViewModel(private val messageRepository: MessageRepository): ViewModel() {

    private var messageList = MutableLiveData<List<MessageData>>()

    /**
     * LiveData取得
     *
     * @return {@link LiveData}
     */
    fun getMessagesLiveData(): LiveData<List<MessageData>> = messageList

    /**
     * List更新
     */
    fun fetchList() {
        viewModelScope.launch {
            messageList.value = messageRepository.getMessages()
        }
    }
}
```

###  :bomb: バッドノウハウ
****
- ビルド時に以下のエラーが発生 :warning:

```
org.koin.error.BeanInstanceCreationException: Can't create definition for 'Factory [name='ListViewModel',class='xxxxxxxxxxxx.ui.ListViewModel']' due to error :
		Can't create definition for 'Factory [name='MessageBoxesRepository',class='xxxxxxxxxxxx.repository.MessageBoxesRepository']' due to error :
		Can't create definition for 'Factory [name='MessageDao',class='xxxxxxxxxxxx.repository.db.dao.MessageDao']' due to error :
		Can't create definition for 'Single [name='Database',class='xxxxxxxxxxxx.repository.db.Database']' due to error :
		cannot find implementation for xxxxxxxxxxxx.repository.db.Database. Database_Impl does not exist
....
```

`kapt` としないといけない所が `annotationProcessor` になっていないか要確認

```gradle
apply plugin: 'kotlin-kapt'
...
kapt "androidx.room:room-compiler:$room_version"
``` 

----

- ビルド時に以下のエラーが発生 :warning:

```
Maybe you forgot to add the referenced entity in the entities list of the @Database annotation?
```
`@ForeignKey` を使った時に↑のエラーに遭遇

エラーメッセージ通り、`@Database` アノテーションにEntityを追加するのを忘れていただけ...

### :link: 参考URL
****

- [MVP with RxJava2 + Room + Koin - Carlos Macías Martín - Medium](https://medium.com/@c4rlosmm96/mvp-with-rxjava2-room-koin-6f9492b94500)
- [RoomにCoroutineサポートが入ったので触ってみた - Qiita](https://qiita.com/tyoshihisa/items/7ebfe58bee5dd994c09a)
- [KotlinでRoomを使う - Qiita](https://qiita.com/yukiyamadajp/items/73bffb6a3697cb62f9e1)
- [bufferapp/clean-architecture-koin-boilerplate: A fork of our clean architecture boilerplate, this time using Koin](https://github.com/bufferapp/clean-architecture-koin-boilerplate)