16
10

More than 5 years have passed since last update.

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

Posted at

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


16
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10