Posted at

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


Room

公式ページ

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


構成要素

※公式ページから引用

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

Database: データベース内のエンティティとデータアクセスオブジェクトのリストを定義

Entity: 各エンティティに対して、アイテムを保持するためのデータベーステーブルが作成される

Dao: データベースにアクセスするメソッドを定義する役割を果たす


:computer:環境構築


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


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を作成します。

@Entity

data class UserData(
@PrimaryKey(autoGenerate = true)
val id: Long,
val name: String
)

id に関しては @PrimaryKey(autoGenerate = true)で主キーかつ

自動採番するように設定しています。

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


@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つ前に作成した UserDataid と紐づけています。

@ColumnInfo(name = "xxxxx") では名前が実際の列名と異なる場合に設定しています。


Converter作成

Entityの MessageDataDate 型を使用していますが、

実際のDBデータにはDate型は無い為、変換が必要になります。

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を作成します。

@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を作成

@Dao

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


Database作成

@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

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

class MessageRepository(private val messageDao: MessageDao) {

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


  • ViewModel

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 になっていないか要確認

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