Tagbangers Blog

Spring + React の Web アプリを Kotlin のみで作成する

先月から Tagbangers にジョインした生粋のハマっ子こと JK です(元インターン生)

私の好きなプログラミング言語の1つは Kotlin です

Kotlin は JetBrains 社が開発した言語で主に Android アプリ開発で使用できる言語として有名です

JK ≒ JetBrains Kotlin

今回はそんな Kotlin を用いて簡単な Web アプリを1から作成していこうと思います!

GitHub

今回作成するプロジェクトの完成形は下記のリポジトリにプッシュしています

koyama-tagbangers/kotlin-react-spring-sample

What's the advantage of Kotlin

Kotlin の入門動画は下記がおすすめです
Java に触った経験のある方への説明となっていますが、非常にわかりやすいです

Kotlin は Java をより使いやすくした言語という印象で、非常に多くの便利な機能を持っています

特筆すべき点は Java の低いバージョン(最低で Java 6)でも最新の Kotlin の機能が使用でき、既存の Java プロジェクトに非常に組み込みやすいことです

これにより、古くから Java で開発が続いているプロジェクトでも Kotlin を用いて拡張ができたり、あるいは完全に Kotlin に移行することが可能です

そして最近では JVM の垣根を超え、JavaScript や マルチプラットフォームをターゲットとしたオールラウンダーな開発が行える言語としてより注目を浴びています

Project Overview

今回は簡単な TODO アプリを作成します

  • [機能] タスクの追加、削除、完了チェック更新
  • サーバはデータベースは持たず、インメモリにデータを保存

下記のフレームワークを使用します

フレームワーク通常主に使用する言語Kotlin Target
Spring BootJavaKotlin/JVM
ReactJavaScript (TypeScript)Kotlin/JS


いずれも Kotlin を使用した開発が可能です

今回は Gradle を使用してアプリの開発を1から行います
(Maven を用いても開発が可能ですが、後述の Kotlin Gradle DSL を使用したいので今回は Gradle を使用します!)

Step0: Install development tools

最初に JDK, Gradle のインストールを行います
JDK のバージョンは幾つでも問題ありません

今回は下記のバージョンで開発を行っています

$ java -version
openjdk version "11.0.9" 2020-10-20
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.9+11)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.9+11, mixed mode)
$ gradle --version
Welcome to Gradle 6.7!
Here are the highlights of this release:
 - File system watching is ready for production use
 - Declare the version of Java your build requires
 - Java 15 support
For more details see https://docs.gradle.org/6.7/release-notes.html
------------------------------------------------------------
Gradle 6.7
------------------------------------------------------------
Build time:   2020-10-14 16:13:12 UTC
Revision:     312ba9e0f4f8a02d01854d1ed743b79ed996dfd3
Kotlin:       1.3.72
Groovy:       2.5.12
Ant:          Apache Ant(TM) version 1.10.8 compiled on May 10 2020
JVM:          11.0.9 (AdoptOpenJDK 11.0.9+11)
OS:           Mac OS X 10.15.7 x86_64

Step1: Setup Gradle project

それでは開発の下地を作ります

開発用の空ディレクトリを作成し、gradle init コマンドで初期化を行います

$ gradle init
Starting a Gradle Daemon (subsequent builds will be faster)
Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 1
Select build script DSL:
  1: Groovy
  2: Kotlin
Enter selection (default: Groovy) [1..2] 2
Project name (default: kotlin-todo-app): kotlin-todo-app
> Task :init
Get more help with your project: Learn more about Gradle by exploring our samples at https://docs.gradle.org/6.7/samples
BUILD SUCCESSFUL in 33s
2 actionable tasks: 2 executed

Gradle プロジェクトでは通常 Groovy を用いて設定を行いますが、ここでも代わりに Kotlin を使用することが可能です(正確には Gradle Kotlin DSL と呼ばれます)

成功すると下記の様な Gradle プロジェクトが展開されます

拡張子が gradle.kts のファイルは Gradle Kotlin DSL 用のファイルです

今回はさらにこの中にフロントエンド用及びバックエンド用のサブプロジェクトを用意します

プロジェクト直下に frontend と backend という名前のフォルダを作成して build.gradle.kts ファイルを作成します

そして settings.gradle.kts にサブプロジェクトの情報を追加します

rootProject.name = "kotlin-todo-app"
include("backend", "frontend")

これで準備は完了です

Step2: Implement backend application

今回は Spring Data REST を用いて簡潔なコードで API を実装します

データベースはインメモリの H2 Database を使用します

backend/build.gradle.kts に下記を入力します

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.3.5.RELEASE"
    id("io.spring.dependency-management") version "1.0.10.RELEASE"
    kotlin("jvm") version "1.4.10"
    kotlin("plugin.spring") version "1.4.10"
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-data-rest")
    runtimeOnly("com.h2database:h2")
    implementation("org.springframework.boot:spring-boot-devtools")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "11"
    }
}

plugins の kotlin("jvm") で JVM ターゲットの Kotlin を選択します
下部の kotlinOptions の jvmTarget はインストールした JDK のバージョンに合わせます(JDK 8 を使用する場合は 1.8)

次に backend/src/main/kotlin/application ディレクトリを作成して直下に Main.kt ファイルを作成します

Main.kt に下記を入力します

package application

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class TodoApplication

fun main(args: Array<String>) {
    runApplication<TodoApplication>(*args)
}

これは Spring Boot の最小の起動コードですが Java と違ってエントリーポイントの関数の書き方が異なっています (psvm less!)

アプリケーションの起動を行います
プロジェクト直下で下記のコマンドを入力します

./gradlew :backend:bootRun

開発用のローカルサーバーが 8080 ポートで起動します
curl コマンドでリクエストを送り、下記の様に帰ってくれば成功です

$ curl localhost:8080
{
  "_links" : {
    "profile" : {
      "href" : "http://localhost:8080/profile"
    }
  }
}

次に API の実装を行います API の設計は次の通りです

  • GET /api/todo -> Todo リストを返す
  • POST /api/todo -> Todo を追加
  • PUT /api/todo/{id} -> {id} に該当する Todo を上書き更新
  • DELETE /api/todo/{id} -> {id} に該当する Todo を削除

Todo が持つ情報は次の通りです

情報
ID数値
タスク名文字列
完了済みかどうか真偽値


これらを実装するために Main.kt の層に Api.kt ファイルを追加して、下記を入力します

package application

import org.springframework.data.repository.CrudRepository
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id

@Entity
data class Todo(
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        val id: Long = 0,
        val content: String = "",
        val done: Boolean = false
)

interface TodoRepository : CrudRepository<Todo, Long>

@RestController
@RequestMapping("/api/todo")
class TodoController(private val repository: TodoRepository) {
    @GetMapping
    fun findAll() = repository.findAll()

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun save(@RequestBody todo: Todo) = repository.save(todo)

    @PutMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    fun update(@RequestBody todo: Todo, @PathVariable id: Long) = repository.findById(id).ifPresent {
        repository.save(todo.copy(id = id))
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    fun deleteById(@PathVariable id: Long) = repository.deleteById(id)
}

Spring Data REST と Kotlin の強力な機能の組み合わせで、たった 42 行で実装できました(細かいバリデーション等は今回省略します)
Kotlin では1ファイルに複数クラス・インターフェースを定義できるので、ファイルの大量生成をある程度防げます

data class Todo の箇所は Kotlin の Data Class を使用することで Java Beans のメソッドやの比較メソッドなどを自動生成をしています
後述の TodoController では repository.save(todo.copy(id = id)) の部分で、自動生成された copy メソッドを用いています

class TodoController では Kotlin のプライマリコンストラクタを使用してメンバ変数を定義しています
また、各 API 関数の実装は代入演算子を用いて簡潔に記述することができます(ラムダ記法に似ています)

最後に確認用のモックデータを記述します
backend/src/main/resources ディレクトリを作成して、data.sql ファイルを作成し下記を入力します

INSERT INTO todo (content, done) VALUES ('Task1', false), ('Task2', true);

再度アプリケーションを起動して curl コマンドなどで /api/todo に対する操作ができることを確認します

$ curl localhost:8080/api/todo
[{"id":1,"content":"Task1","done":false},{"id":2,"content":"Task2","done":true}]
$curl -XPOST -H 'Content-Type: application/json' localhost:8080/api/todo -d '{"content": "Task3"}'
{"id":3,"content":"Task3","done":false}
$ curl -XPUT -H 'Content-Type: application/json' localhost:8080/api/todo/1 -d '{"done": true}'
$ curl -XDELETE -H 'Content-Type: application/json' localhost:8080/api/todo/2
$ curl localhost:8080/api/todo
[{"id":1,"content":"","done":true},{"id":3,"content":"Task3","done":false}]⏎

Step3: Implement frontend application

次にフロントエンド側の実装を行います

Kotlin では React のラッパーパッケージが用意されており、スムーズに開発することが可能です
また、公式の React を使用したチュートリアルも用意されています

Building Web Applications with React and Kotlin/JS

frontend/build.gradle.kts を作成して下記を入力します

import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig

plugins {
    kotlin("js") version "1.4.10"
    kotlin("plugin.serialization") version "1.4.10"
}

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    implementation(kotlin("stdlib-js"))
    implementation("org.jetbrains:kotlin-react:17.0.0-pre.129-kotlin-1.4.10")
    implementation("org.jetbrains:kotlin-react-dom:17.0.0-pre.129-kotlin-1.4.10")
    implementation(npm("react", "17.0.1"))
    implementation(npm("react-dom", "17.0.1"))
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:1.0-M1-1.4.0-rc")
}

kotlin {
    js {
        browser {
            runTask {
                devServer = KotlinWebpackConfig.DevServer(
                        port = 3000,
                        proxy = mapOf("/api" to mapOf("target" to "http://localhost:8080")),
                        contentBase = listOf("$buildDir/processedResources/js/main")
                )
            }
        }
    }
}

バックエンドと異なり、kotlin("js") と記述することで、JS をターゲットとして Kotlin を使用できます
dependencies スコープに npm の記述が見られますが、JS をターゲットにした際は npm パッケージを用いることが可能です

Kotlin/JS では Webpack をラップしており、開発時は Webpack Dev Server を使用します
デフォルトのポート番号が 8080 ポートなのでバックエンド側と競合しない様にポート番号を 3000 に変更します
また、フロントエンド側の /api 以降のパスをバックエンドサーバーにフォワーディングするための設定も行っています

次に、エントリーポイントとなる index.html ファイルを frontend/src/main/resources に作成します

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>React + Spring Todo App in React</title>
</head>
<body>
<div id="root"></div>
<script src="frontend.js"></script>
</body>
</html>

スクリプト名はデフォルトではプロジェクト名になるため、今回は frontend.js と指定しています

まずは API 通信を行わない、フロントエンドオンリーなコードを書きます
frontend/src/main/kotlin/Main.kt を作成して下記を入力します

import kotlinx.browser.document
import kotlinx.html.InputType
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction
import kotlinx.html.js.onSubmitFunction
import org.w3c.dom.HTMLInputElement
import react.RProps
import react.child
import react.dom.*
import react.functionalComponent
import react.useState

data class Todo(
        var id: Long = 0,
        var content: String = "",
        var done: Boolean = false
)

val mockTodoList = listOf(Todo(content = "Task1"), Todo(content = "Task2"))

val app = functionalComponent<RProps> {
    val (todoList, setTodoList) = useState(mockTodoList)
    val (inputText, setInputText) = useState("")

    div {
        form {
            attrs {
                onSubmitFunction = {
                    setTodoList(todoList.toMutableList().apply {
                        add(Todo(content = inputText))
                    })
                    setInputText("")
                    it.preventDefault()
                }
            }
            input {
                attrs {
                    type = InputType.text
                    value = inputText
                    onChangeFunction = {
                        setInputText((it.target as HTMLInputElement).value)
                    }
                }
            }
            input {
                attrs {
                    type = InputType.submit
                    value = "Add"
                    disabled = inputText.isBlank()
                }
            }
        }
        todoList.forEachIndexed { index, todo ->
            div {
                button {
                    attrs {
                        onClickFunction = {
                            setTodoList(todoList.toMutableList().apply {
                                this[index].done = !todo.done
                            })
                        }
                    }
                    +"${if (todo.done) '✅' else '⬜'}"
                }
                +todo.content
                button {
                    attrs {
                        onClickFunction = {
                            setTodoList(todoList.toMutableList().apply {
                                removeAt(index)
                            })
                        }
                    }
                    +"DELETE"
                }
            }
        }
    }
}

fun main() {
    render(document.getElementById("root")) {
        h1 {
            +"React + Spring Todo App in React"
        }
        child(app)
    }
}

React の独特の JSX 記法を Kotlin の独特の DSL 記法で実現しています

div / button などの実態はラムダを引数とした関数です、そして "+" は演算子オーバーロードという機能を利用しています

// 冗長な書き方
div({ it -> it.childList.add("Hoge") })
// 上記の省略記法
div { +"Hoge" }

アトリビュートは attrs ブロックに記述します、ここは JSX 記法に比べて少しわかりづらいかもしれません

React をラップしたパッケージの恩恵のおかげで、関数コンポーネントや React Hook などの最新の機能も使用することができます!

それでは開発サーバを起動して動作を確認します
下記のコマンドで起動します(--continuous オプションをつけることでコードの変更時に自動的にリビルド&リロードが走ります)

./gradlew :frontend:run

タスクの追加・更新・削除が行えました!

それでは API 通信を含んだ実装を行います、ファイル内の規模が大きくなるのでの3つのファイルにわけて開発を行います

  • [Main.kt] エントリーポイント、バックエンドとの API 通信を行う
  • [Form.kt] テキストインプットを用いてタスクの追加が行えるコンポーネント
  • [Card.kt] Todo リストの1タスクに相当するコンポーネント

それぞれ下記の様に実装を行います

Main.kt

import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.await
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import org.w3c.fetch.RequestInit
import react.*
import react.dom.div
import react.dom.h1
import react.dom.render
import kotlin.js.json

@Serializable
data class Todo(
        var id: Long = 0,
        var content: String = "",
        var done: Boolean = false
)

val app = functionalComponent<RProps> {
    val (loading, setLoading) = useState(true)
    val (todoList, setTodoList) = useState(emptyList<Todo>())

    val toggleSummit: (String, () -> Unit) -> Unit = { content, onSucceed ->
        GlobalScope.launch {
            try {
                setLoading(true)
                val data = window.fetch("/api/todo", object : RequestInit {
                    override var method: String? = "POST"
                    override var headers = json().apply {
                        this["Content-Type"] = "application/json"
                    }
                    override var body = Json.encodeToString(Todo.serializer(), Todo(content = content))
                })
                        .await()
                        .text()
                        .await()
                        .let { Json.decodeFromString(Todo.serializer(), it) }
                setTodoList(todoList.toMutableList().apply { add(data) })
                onSucceed()
            } catch (err: Throwable) {
                throw err
            } finally {
                setLoading(false)
            }
        }
    }

    val toggleHandle: (Int) -> () -> Unit = { index ->
        {
            GlobalScope.launch {
                try {
                    val data = todoList[index].copy().apply { this.done = !this.done }
                    setLoading(true)
                    window.fetch("/api/todo/${data.id}", object : RequestInit {
                        override var method: String? = "PUT"
                        override var headers = json().apply {
                            this["Content-Type"] = "application/json"
                        }
                        override var body = Json.encodeToString(Todo.serializer(), data)
                    })
                            .await()
                            .text()
                            .await()
                    setTodoList(todoList.toMutableList().apply { this[index] = data })
                } catch (err: Throwable) {
                    throw err
                } finally {
                    setLoading(false)
                }
            }
        }
    }

    val toggleDelete: (Int) -> () -> Unit = { index ->
        {
            GlobalScope.launch {
                try {
                    val data = todoList[index]
                    setLoading(true)
                    window.fetch("/api/todo/${data.id}", object : RequestInit {
                        override var method: String? = "DELETE"
                    })
                            .await()
                            .text()
                            .await()
                    setTodoList(todoList.toMutableList().apply {
                        remove(data)
                    })
                } catch (err: Throwable) {
                    throw err
                } finally {
                    setLoading(false)
                }
            }
        }
    }

    useEffect(emptyList()) {
        GlobalScope.launch {
            try {
                setLoading(true)
                val data = window.fetch("/api/todo")
                        .await()
                        .text()
                        .await()
                        .let { Json.decodeFromString(ListSerializer(Todo.serializer()), it) }
                setTodoList(data)
            } catch (err: Throwable) {
                throw err
            } finally {
                setLoading(false)
            }
        }
    }

    div {
        inputForm {
            this.loading = loading
            onSubmit = toggleSummit
        }
        todoList.forEachIndexed { index, todo ->
            todoCard {
                key = todo.id.toString()
                this.todo = todo
                this.loading = loading
                onToggle = toggleHandle(index)
                onDelete = toggleDelete(index)
            }
        }
    }
}

fun main() {
    render(document.getElementById("root")) {
        h1 {
            +"React + Spring Todo App in React"
        }
        child(app)
    }
}

Form.kt

import kotlinx.html.InputType
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onSubmitFunction
import org.w3c.dom.HTMLInputElement
import react.*
import react.dom.form
import react.dom.input

interface FormProps : RProps {
    var loading: Boolean
    var onSubmit: (String, () -> Unit) -> Unit
}

val inputForm = functionalComponent<FormProps> { props ->
    val (inputText, setInputText) = useState("")

    form {
        attrs {
            onSubmitFunction = {
                props.onSubmit(inputText) {
                    setInputText("")
                }
                it.preventDefault()
            }
        }
        input {
            attrs {
                type = InputType.text
                value = inputText
                disabled = props.loading
                onChangeFunction = {
                    setInputText((it.target as HTMLInputElement).value)
                }
            }
        }
        input {
            attrs {
                type = InputType.submit
                value = "Add"
                disabled = inputText.isBlank() or props.loading
            }
        }
    }
}

fun RBuilder.inputForm(handler: FormProps.() -> Unit) = child(inputForm) {
    attrs { handler() }
}

Card.kt

import kotlinx.html.js.onClickFunction
import react.RBuilder
import react.RProps
import react.child
import react.dom.button
import react.dom.div
import react.functionalComponent

interface TodoProps : RProps {
    var todo: Todo
    var onToggle: () -> Unit
    var onDelete: () -> Unit
    var loading: Boolean
}

val todoCard = functionalComponent<TodoProps> { props ->
    div {
        button {
            attrs {
                disabled = props.loading
                onClickFunction = { props.onToggle() }
            }
            +"${if (props.todo.done) '✅' else '⬜'}"
        }
        +"${props.todo.content} (id: ${props.todo.id})"
        button {
            attrs {
                disabled = props.loading
                onClickFunction = { props.onDelete() }
            }
            +"DELETE"
        }
    }
}

fun RBuilder.todoCard(handler: TodoProps.() -> Unit) = child(todoCard) {
    attrs { handler() }
}

fetch による API 通信処理は Kotlin Coroutine を使用して GlobalScope.launch 内で行うことで非同期に行わせます
Coroutine は Java のスレッドに似ていますが、より軽量に動作するため Kotlin ではスレッドの代わりによく用いられます

GlobalScope.launch {
    try {
        setLoading(true)
        val data = window.fetch("/api/todo")
                .await()
                .text() // Json text
        console.log(data)
    } catch (err: Throwable) {
        throw err
    }
}

JSON のパース、生成は kotlinx.serialization を使用します(JSON.parse はうまく動作しませんでした)
データクラスに対して @Serializable アノテーションをつけるだけなので非常に簡単です
また、このパッケージはマルチプラットフォームに使用できるため Kotlin のアプリ開発において非常に重宝します

@Serializable
data class Hoge(val value: String)

val hoge: Hoge = Json.decodeFromString(Hoge.serializer(), """{"value": "Value"}""")

リストを返す様なレスポンスを変換する場合は以下の様な書き方になります

val hogeList: List<Hoge> = Json.decodeFromString(ListSerializer(Todo.serializer()), """[{"value": "Value"}]""")

それでは動作を見ていきます
事前にバックエンドサーバの起動(./graldew :backend:bootRun)を裏で行っておいてください

上記では Chrome Dev Tools の Network 機能を利用して通信速度を意図的に遅くしています

API 通信を介して動作が行えていることが確認できます
バックエンドサーバがデータを持っているので、ブラウザを更新してもデータが保持されます(バックエンドサーバを終了するとデータも破棄されます)

バックエンドのアプリの起動は時間がかかるので何も表示されない場合はしばらく経ってからブラウザを更新してください

Step4: Build applications and run in Docker

最後にプロジェクトのビルドを行い、Docker 上でアプリを動かします

プロジェクトルート上で下記のコマンドを打つとサブプロジェクトを含めたビルドを行います

./gradlew clean build

成果物はそれぞれ下記に出力されます

  • backend/build/libs
    • backend.jar
  • frontend/build/distributions
    • frontend.js
    • index.html

これらの動作確認を Docker 上で行います

事前に Docker をインストールしてください

プロジェクト直下に docker-compose.yml を作成して下記を入力します

version: "3.8"

services:
  frontend:
    image: nginx:1.19
    ports:
      - 80:80
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./frontend/build/distributions:/etc/nginx/html
  backend:
    image: openjdk:11-jdk
    ports:
      - 8080:8080
    volumes:
      - ./backend/build/libs:/etc
    command: java -jar /etc/backend.jar

フロントエンドサーバで使用する Nginx にリバースプロキシの設定を行うためプロジェクト直下に nginx.conf ファイルを作成して下記を入力します

server {
    location /api {
        proxy_pass http://host.docker.internal:8080/api;
        proxy_redirect off;
    }
}

Docker を立ち上げます、下記のコマンドを入力します

docker-compose up

バックエンドサーバの起動に少し時間がかかるので、ある程度待ってから http://localhost にアクセスしてアプリケーションが動作していれば成功です!

Conclusion

Kotlin のみでモダンな Web 開発が行えました(思っていたより記事のボリュームが増えてしまいました)

Spring の開発を Kotlin 行うことでより快適で強力な開発を行うことが可能です、お馴染みの雛壇プロジェクト生成サイトの Spring Initializr も Kotlin に対応しています

一方で Kotlin の JS 開発を初めて実践しましたが、React ラッパーがあるのは衝撃的でした
TypeScript 開発に比べると少し難解な印象を受けましたが、ポテンシャルを感じました
Styled Components なども使える様でかなり面白いです

Kotlin に興味を持っていただけたら幸いです

ぜひ、今日から Kotlin を用いた開発にチャレンジしてみましょう!