Help us understand the problem. What is going on with this article?

AndroidでRoom+RxJavaを実装する

本記事ではAACのRoomPersistenceLibraryとRxJavaを組み合わせた、基本的な実装のお話をしていこうと思います。
記事内のコードはKotlinで記述していますが、Coroutineなどは使っていないため、基本的にJavaに置き換えることも可能です。

対象読者

  • Room初学者
  • Room + RxJavaに興味がある方

セットアップ

最初にbuild.gradle(app)に以下を記述します。

def room_version='2.1.0-alpha06'
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
kapt "androidx.room:room-compiler:$room_version"

implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
  • androidx.room:room-rxjava2を記述することによって、DAOの戻り値でSingleやMaybeなどを指定することができます。
  • room_versionに関しては2.1.0以上を使ってください。理由は後ほど詳しく記述しますが、2.1.0からInsertやdeleteの戻り値にCompletableを指定できるようになったからです。
  • AndroidXに対応していないと、2.0.0以上のバージョンを使うことができないため、1.1.1を使ってください。詳しくは こちら のサイトが参考になると思います。

Entity

次にEntityを記述します。

@Entity(tableName = "user")
data class UserEntity constructor(
    @PrimaryKey(autoGenerate = true) //default false
    val id: Long = 0,

    @ColumnInfo(name = "user_name")
    val userName: String,

    val createAt: String
)
  • @Entityこれをつけることによって、Roomで利用できるEntityになります。tableNameはオプションで、クラス名と実際に生成されるTable名を変えたいときにつけます。
  • @PrimaryKeyは、1つのEntityに最低1つはつける必要があります。複数のPrimaryKeyを持つ場合は、以下のように@Entityアノテーション内に定義できます。
@Entity(primaryKeys = ["id", "user_name"])
  • autoGenerate = trueになっている場合、idに0を渡すと、自動的にincrementしてくれます。idを指定する必要がない場合は、0を指定してください。

  • @Ignoreを定義することによって、そのカラムを無視することができます。しかしdata classで@Ignoreを定義するとEntities and Pojos must have a usable public constructor.というエラーが吐かれます。これは全てのカラムにデフォルト値を指定することによって解消できます。

@ColumnInfo(name = "user_name")
var userName: String = "",

@Ignore
var createAt: String = ""

DAO

次にData Access Object(DAO)を記述します。

@Dao
interface UserDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertiOrUpdateUser(entity: UserEntity): Completable

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertiOrUpdateUsers(entities: List<UserEntity>): Completable

    @Query("SELECT * FROM user WHERE id = :id")
    fun findById(id: Long): Single<UserEntity>

    @Delete
    fun delete(entity: UserEntity): Completable
}
  • @Insertは、UserEntity単体、複数(List)のどちらでもInserすることができます。また、onConflictは、任意の値を指定することによって、PrimaryKeyが同じものがInsertされた場合の挙動を指定することができます。今回は上書きするようにOnConflictStrategy.REPLACEを指定しました。その他の指定については こちら のサイトが参考になると思います。

  • @Insertの戻り値はCompletableを指定できます。

  • @Deleteでは引数にUserEntityを渡すことによって、渡したEntityを削除することができます。この戻り値もCompletableを指定することができます。

Query

@Queryの戻り値にはMaybe, Single, Flowable/Observableを指定することができます。各々挙動が違うので、要件に合ったものを選ぶといいと思います。

Maybe

@Query("SELECT * FROM user WHERE id = :id")
fun findById(id: Long): Maybe<UserEntity>
  • データベース内に該当するUserEntityがない場合、何も返さずにonCompleteが呼ばれます。
  • データベース内に該当するUserEntityがある場合、onSuccessが呼ばれ、終了します(onCompleteは呼ばれません)。
  • Maybeが終了した後にUserEntityが更新されても何もしません。

Single

@Query("SELECT * FROM user WHERE id = :id")
fun findById(id: Long): Single<UserEntity>
  • データベース内に該当するUserEntityがない場合、onError(EmptyResultSetException)が呼ばれます。
    • データベース内に該当するUserEntityがないかつ、List型を返す場合はonError(EmptyResultSetException)が呼ばれず、空のListが返されます。
  • データベース内に該当するUserEntityがある場合、onSuccessが呼ばれ、終了します。
  • Singleが終了した後にUserEntityが更新されても何もしません。

Flowable/Observable

@Query("SELECT * FROM user WHERE id = :id")
fun findById(id: Long): Flowable<UserEntity>
  • データベース内に該当するUserEntityがない場合、何も返さず、何も呼ばれません。
  • データベース内に該当するUserEntityがある場合、onNextが呼ばれます。
  • UserEntityが更新されるたびに、自動でonNextが呼ばれます。

Database

次にDatabaseを記述します。

@Database(entities = [UserEntity::class], version = SampleDatabase.DATABASE_VERSION)
abstract class SampleDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object {
        const val DATABASE_VERSION = 1
        private const val DATABASE_NAME = "sample.db"

        private var instance: SampleDatabase? = null

        fun init(context: Context) {
            Room.databaseBuilder(context, SampleDatabase::class.java, DATABASE_NAME)
                .build().also { instance = it }
        }

        fun getInstance() = instance
    }
}
  • RoomDatabaseを継承したabstractクラスを作成します。
  • @Database内のentitiesにアプリ内で使うEntityを宣言します。Entityが増えるたびに、ここに追加していく形になります。
  • @Database内のversionにデータベースのversionを定義します。最小値は1です。
  • abstract fun userDao(): UserDaoの部分はDAOの宣言になります。DAOが増えるたびに、ここに追加していくことになります。
  • init()やgetInstance()を生やしておき、外部から呼び出せるようにしておきます。

Databaseの初期化

最後に上で定義したDatabaseをApplicationクラスで初期化します。

class SampleApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        SampleDatabase.init(this)
    }
}

簡単ですね。これでSampleDatabaseをどこからでも呼び出せるようになりました。

使ってみる

それでは準備が整ったので、実際に使ってみようと思います。
今回は
1. 複数のUserEntityをInsert
2. idからUserEntityを取得
3. 2で取得したUserEntityを削除
この3つの処理を書いていこうと思います。

val users: List<UserEntity> = //任意の値を入れてください

SampleDatabase.getInstance()?.let { sampleDatabase ->
    val userDao = sampleDatabase.userDao()

    userDao
        .insertiOrUpdateUsers(users)
        .andThen(userDao.findById(1)) //Single<UserEntity>を返しています
        .flatMapCompletable { user -> userDao.delete(user) }
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe({}, { e -> e.printStackTrace() })
}

たったこれだけです! すごくシンプルに書けます。

  • subscribeOn()ここだけ説明しておきます。Room実行は基本的にBackgroundThreadを指定しないといけません。MainThreadで実行するとExceptionが吐かれます。もしMainThreadでの実行を許可したい場合は、RoomのInstance生成時に以下のような定義が必要になります。
Room.databaseBuilder(context, SampleDatabase::class.java, DATABASE_NAME)
    .allowMainThreadQueries() //MainThreadでの実行を許可します
    .build().also { instance = it }

最後に

RoomはThread制限があったり、Queryが間違っていたらBuild時に教えてくれたり、とても親切なライブラリです。
皆さんも是非使ってみてください。

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした