Kotlin で kintone をカスタマイズする
概要
kintone は、JavaScript でカスタマイズをすることができます。
@kintone/rest-api-client を用いることで、kintone REST API を使った実装が簡単になります。
また、Kotlin は Multiplatform で動作するため、JavaScript にトランスパイルすることで、ブラウザで動作させることができます。
Kotlin/JavaScript (Kotlin/JS) lets you transpile your Kotlin code, the Kotlin standard library, and any compatible dependencies to JavaScript. This way, your Kotlin applications can run in any environment that supports JavaScript.
本記事では、Kotlin を使った kintone カスタマイズにトライします。
Kotlin 用の @kintone/rest-api-client を自作し、それを使った実装例を示します。
詳細
kintone REST API を呼び出す方法は、下記が考えられます。他にもありますが代表的なものを記載します。
-
window.fetch()を用いて、kintone REST API を呼び出す。 - kintone JavaScript API を用いて、kintone REST API リクエストを送信する
- HTTP 通信用ライブラリを用いて、kintone REST API を呼び出す
1, 2 はブラウザ上での実行を前提としています。
今回は、1 の方針で進めます。
環境
ディレクトリ構成
.
├── gradlew
├── .gitignore
├── build
│ ├── kotlin-webpack
│ │ └── js
│ │ └── productionExecutable
│ │ ├── kintone-rest-api-client-kt.js
│ │ └── kintone-rest-api-client-kt.js.map
│ ├── ...
├── src
│ └── jsMain
│ └── kotlin
│ ├── kintone
│ │ └── client
│ │ ├── record
│ │ │ ├── Record.kt
│ │ │ ├── RecordApiImpl.kt
│ │ │ ├── RecordId.kt
│ │ │ ├── RecordResponse.kt
│ │ │ ├── RecordApi.kt
│ │ │ └── FieldType.kt
│ │ ├── kintone
│ │ │ ├── KintoneApi.kt
│ │ │ └── kintone.kt
│ │ └── KintoneClient.kt
│ └── main.kt
├── gradlew.bat
├── package.json
├── build.gradle.kts
├── settings.gradle.kts
├── README.md
└── gradle.properties
build.gradle.kts
plugins {
kotlin("multiplatform") version "2.3.0"
}
repositories {
mavenCentral()
}
kotlin {
js(IR) {
browser {
binaries.executable()
}
}
sourceSets {
val jsMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.10.2")
}
}
}
}
ソースコード解説
kintone.kt
kintone は kintone の画面上では、グローバル変数として参照できるため、Kotlin 側でも変数として定義します。
external val kintone: dynamic
KintoneApi.kt
kintone REST API を呼び出す Wrapper クラスです。
class KintoneApi {
companion object {
suspend fun call(
path: String,
method: String,
body: dynamic = js("{}")
): dynamic {
val url = kintone.api.url(path, true)
return kintone.api(
url,
method,
body
).unsafeCast<Promise<dynamic>>().await()
}
}
}
RecordApi.kt
今は、1件のレコードを取得する API だけですが、将来的に拡張できます。
interface RecordApi {
suspend fun getRecord(
appId: AppId,
recordId: RecordId
): Record
}
RecordApiImpl.kt
js() の中で、Kotlin 側の文字列が展開できません。
そのため、事前に空オブジェクトを用意して、プロパティを追加しています。
class RecordApiImpl : RecordApi {
override suspend fun getRecord(
appId: AppId,
recordId: RecordId
): Record {
val body = js("{}")
body.app = appId.value
body.id = recordId.value
val response = KintoneApi.call(
path = "/k/v1/record.json",
method = "GET",
body = body
)
return RecordResponse(
record = response.record.unsafeCast<Map<String, RawField>>()
).parse()
}
}
RecordId.kt
型安全のため、value class を用います。
value class AppId(val value: Int)
value class RecordId(val value: Int)
FieldType.kt
enum class FieldType {
__ID__,
RECORD_NUMBER,
CREATOR,
MODIFIER,
CREATED_TIME,
UPDATED_TIME,
SINGLE_LINE_TEXT,
}
Record.kt
サンプルなので、フィールド型を絞っています。
typealias FieldCode = String
data class Record(
val id: RecordId,
val fields: Map<FieldCode, FieldValue>
)
sealed interface FieldValue {
data class RecordNumber(val value: Int) : FieldValue
data class User(
val code: String,
val name: String
) : FieldValue
data class DateTime(val value: String) : FieldValue
data class SingleLineText(val value: String?) : FieldValue
data class Number(val value: String?) : FieldValue
}
RecordResponse.kt
Enum Class と when 式の組み合わせによって、網羅性をコンパイラレベルでチェックできます。
Kotlin のメリットが活かせるのがポイントです。
class RecordResponse(
val record: Map<String, RawField>
) {
fun parse(): Record {
val fields = mutableMapOf<FieldCode, FieldValue>()
var recordId: RecordId? = null
val _record = this.record // record は Kotlin の Map 型であり、JavaScript のオブジェクトではない。一度変数に入れないと JS 側で認識不可。
// JS 側で認識できるようにキャストする必要がある
js("Object.entries(_record)").unsafeCast<Array<Array<dynamic>>>().forEach { (fieldCode, raw) ->
val fieldType = enumValues<FieldType>().firstOrNull { it.name == raw.type }
when (fieldType) {
FieldType.__ID__ -> {
recordId = RecordId(raw.value.toString().toInt())
}
FieldType.RECORD_NUMBER -> {
fields[fieldCode] =
FieldValue.RecordNumber(raw.value.toString().toInt())
}
FieldType.CREATOR, FieldType.MODIFIER -> {
fields[fieldCode] = FieldValue.User(
code = raw.value.code as String,
name = raw.value.name as String
)
}
FieldType.CREATED_TIME, FieldType.UPDATED_TIME -> {
fields[fieldCode] = FieldValue.DateTime(raw.value as String)
}
FieldType.SINGLE_LINE_TEXT -> {
fields[fieldCode] = FieldValue.SingleLineText(raw.value as String?)
}
else -> {}
}
}
return Record(
id = recordId ?: error("Record ID not found"),
fields = fields
)
}
}
data class RawField(
val type: String,
val value: dynamic
)
Main.kt
client 用のインスタンスを作成し、レコード情報を取得します。
Kotlin coroutine の仕組みを利用した並行処理も簡単に実装できます。
fun main() {
val client = KintoneClient()
kintone.events.on("app.record.detail.show") { event: dynamic ->
console.log(event)
MainScope().launch {
val appId = AppId(kintone.app.getId())
val recordDeferred = async {
val recordId = RecordId((event.recordId as Number).toInt())
client.record.getRecord(appId, recordId)
}
val recordDeferred2 = async {
val recordId = RecordId(2)
client.record.getRecord(appId, recordId)
}
val (record, record2) = listOf(recordDeferred, recordDeferred2).awaitAll()
console.log("recordId: ", record.id.value)
val singleLineText = record.fields["string"] as? FieldValue.SingleLineText
console.log("single line text: ", singleLineText?.value)
console.log("record2Id: ", record2.id.value)
}
event
}
}
実行方法
./gradlew jsBrowserProductionWebpack
上記コマンド実行後、./build/kotlin-webpack/js/productionExecutable/kintone-rest-api-client-kt.js を kintone に JavaScriptカスタマイズとしてアップロードします。
実行結果

Dveloper tool から、ログ出力されていることが分かります。
また、Network タブを確認すると、並行処理されていました。
※ 事前に、フィールドコード string のフィールドを配置しています。
まとめ
本記事では、Kotlin を使った kintone カスタマイズ例を示しました。
また、Kotlin 用の @kintone/rest-api-client を自作しました。
個人的には、開発環境やボイラーテンプレートができてしまえば、JS よりも Kotlin で開発している方が体験が良かったです。
手軽さは JS ですが、型安全性に関しては Kotlin に軍配が上がります。
今後は、自作ライブラリを拡張したり、UI 操作に関する記事を書いたりしたいと思います。
Discussion