December 22, 2020

Kotlin Multiplatform 対応したRealmを使ってみた

この記事はKotlin Advent Calendar 2020の22日目の記事です。

Kotlin Advent Calendarに参加するのは今年で6年連続6回目になりました🎉🐦
毎年22日近辺を書いています。マイルストーンの時から書いている記事もあるので情報が古くなっているものもありますが、 過去にはこんな記事を書いていました。

2020年はMPP(KMP)にも大きな進展がありました!
Kotlin1.4がリリースされたり、 当初はKotlin Multiplatform Projectとして、Android, Native(iOS, Linux, wasm...etc), WEB(JS), Server全てを一気に対応する方針で進んできましたが、一旦は一番需要があるMobileに振り切って、 Kotlin Multiplatform Mobile(通称KMM)という名前でalphaリリースが行われました🎉
もちろんJS等の対応をやめたということではなく、Mobileの枠を区切って力を入れることでMPPの利用者を増やすことを目的としているんだと思います(勝手な推測)
引き続きKotlin JS等には期待しています。

最近の私ですが、Kotlin Multiplatform Project(MPP)でガッツリ大規模サービスを開発していきたい気持ちもありつつ、 現所属の会社は少人数(フロント1or2人)で素早くプロトタイプを開発していくフェーズで 今年は主にFlutterで開発する機会が多くなってしまいました。
とはいえKotlinへの愛は絶えていないのでキャッチアップは細々と続けています。
FlutterはFlutterで結構楽しいです🎯
今回はMPPのガッツリネタというよりは、2020年12月15日にDeveloper PreveiwがリリースされたばかりのRealm Kotlinを試してみたので、使ってみた系の記事を書きたいと思います。 ドキュメントはまだDeveloper PreviewでAPI Designを定義している状態なので、今後APIが変更になる可能性が高いのはご注意ください。

Realm Kotlin

Realm Kotlinのリポジトリはこちらです。

https://github.com/realm/realm-kotlin

API Design OverviewがGoogle Docsで用意されています。 提案モードが用意されているので今ならAPI設計に物申せます!

https://docs.google.com/document/d/1RSPNO95wZAAojYlFwshSpLiuEu9ZqXptO58RDoPHKNc/edit

こっちはモノレポにするかとかいろいろなメリット・デメリットとかも載ってるので、 それを見るのも面白いのでぜひ見てみてください。

https://docs.google.com/document/d/10adRFquingm_JgyjDhUzcYXIDJsDG2A1ldFw53GSVJQ/edit

使い方

Project作成

まずはIntelliJ(Andorid Studio)からプロジェクトを作成します。
KMMのリリースと同時にプラグインが追加されているので、追加しましょう。

kmm plugin

これを使ってプロジェクトを作成すると、簡単にKMMのプロジェクトが設定できます。

as kmm

Gradle設定

/build.gradle.kts

buildscript {
    repositories {
        gradlePluginPortal()
        jcenter()
        google()
        mavenCentral()
        maven(url = "http://oss.jfrog.org/artifactory/oss-snapshot-local") // 追加
    }
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.21")
        classpath("com.android.tools.build:gradle:4.1.0")
        classpath("io.realm.kotlin:plugin-gradle:0.0.1-SNAPSHOT") // 追加
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
        maven(url = "http://oss.jfrog.org/artifactory/oss-snapshot-local") // 追加
    }
}

/shared/build.gradle.kts

import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

plugins {
    kotlin("multiplatform")
    id("com.android.library")
    id("realm-kotlin") // 追加
}

kotlin {
    android()
    iosX64("ios") { // A1
        binaries {
            framework {
                baseName = "shared"
            }
        }
    }
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("io.realm.kotlin:library:0.0.1-SNAPSHOT") // B 追加
            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test-common"))
                implementation(kotlin("test-annotations-common"))
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("com.google.android.material:material:1.2.1")
            }
        }
        val androidTest by getting {
            dependencies {
                implementation(kotlin("test-junit"))
                implementation("junit:junit:4.13")
            }
        }
        val iosMain by getting
        val iosTest by getting
    }
}

android {
    compileSdkVersion(29)
    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    defaultConfig {
        minSdkVersion(24)
        targetSdkVersion(29)
    }
}

val packForXcode by tasks.creating(Sync::class) {
    group = "build"
    val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
    val framework =
        kotlin.targets.getByName<KotlinNativeTarget>("ios").binaries.getFramework(mode) // A2
    inputs.property("mode", mode)
    dependsOn(framework.linkTask)
    val targetDir = File(buildDir, "xcode-frameworks")
    from({ framework.outputDirectory })
    into(targetDir)
}

tasks.getByName("build").dependsOn(packForXcode)

コメントしている箇所はデフォルトのKMMプロジェクトから変更している箇所になります。

  • A1, A2

Realm Kotlinはまだbetaバージョンです。 そのため2020/12/21現在はまだ x86_64 のアーティファクトしか配布されていません。
なのでiOS側はarmを除く必要があります。

  • B

SharedモジュールにRealm Kotlinの依存を追加します。 Kotlin1.4まではAndroid, iOS各プラットフォーム毎に依存を自分で追加しないと動かなかったのですが、 1.4からはcommonMain 1箇所のみの定義で良くなったのでとてもスッキリしました!

Shared Code

DBに使うモデルには、 RealmModelを継承したクラスに@RealmObjectannotationをつけます。
ここはdata classにしたかったのですが、普通のclassでないとコンパイルできませんでした🥺

@RealmObject
class Person : RealmModel {
    var name: String = "hoge"
    var age: Int = 46
}

次にSchema用にRealmModule annotationをつけたクラスを用意します。
サンプルコードではannotationの引数にRealmModelを指定していますが、なくても動作はしました🤔

@RealmModule(Person::class)
class Entities

準備はこれだけで、あとはShared Module側で初期化処理をします。
とても簡単で、Realm.openにRalm configurationを渡すだけです Realm configurationには他に名前やパスの設定ができます。

private val realm: Realm by lazy {
    val configuration = RealmConfiguration.Builder()
        .schema(Entities())
        .build()

    Realm.open(configuration)
}
insert
fun addPerson(name: String, age: Int): Person {
    realm.beginTransaction()
    val person = realm.create(Person::class).apply {
        this.name = name
        this.age = age
    }
    realm.commitTransaction()
    return person
}

AndroidのRealmとは違って、KMMのRealmはclose処理がありません。

select
fun persons(): List<Person> {
    return realm.objects(Person::class)
}

fun queryPerson(name: String): List<Person> {
    return realm.objects(Person::class).query("name = $0", name)
}
delete
fun deletePerson(person: Person) {
    realm.beginTransaction()
    Realm.delete(person)
    realm.commitTransaction()
}

fun deletePersons() {
    realm.beginTransaction()
    realm.objects(Person::class).delete()
    realm.commitTransaction()
}

特定の行を削除する時だけRealmオブジェクトからおこないます。

Platform Code

Shared moduleのRealmインスタンスはObject classで定義するなり、DIするなりしてSingletonにすると良いと思います。

Shared moduleにSingletonで定義されているとして、
あとはプラットフォーム側で

Database.addPerson("abc", 20)
val person = Database.queryPerson("abc")
Log.v("android", person.toString())

みたいにするだけです。 プラットフォーム側のコードはとてもシンプルに書けますね。

現状提供されている機能はこれだけです。

Coroutines support

DBからの値をリアクティブに返却する機能はもはや必須と言っても過言ではないでしょう。
KMMなのでKotlin CoroutinesのFlowで返して貰えると嬉しいです。
もちろんRealmも考えているみたいですが、まだ使うことはできません。 Docsがあるので今後実装されることを期待しましょう。

https://docs.google.com/document/d/1H9CVA928omjKIB19MqYUj7Pysy3Lb0a6WQcv9IAGPwg/edit

まとめ

先日リリースされたばかりのRealm Kotlinを早速使ってみました。
まだまだDeveler Previewの段階で基本的な機能(insert, select, delete)ぐらいしか実装されていませんが、SQLiteを使うまでもない場合には選択肢の1つとしてあると嬉しいですね。
今後の開発に期待しましょう!!

明日は優秀な同僚@oboenikuiさんのmockkの話です。

© AAkira 2019