Kotlinでフロントエンドを書きたい!
Kotlinには、Kotlin/JSという KotlinをJavaScriptにトランスパイル出来る機能 があります。
これによりKotlinのモダンな文法でフロントエンドやサーバーサイドJSの開発を行うことができます。
フロントエンドでは、公式から React用のKotlinラッパー が提供されており、Kotlinを使ったReactの開発が出来るようになっています。
また、create-react-appのように、create-react-kotlin-app というものも用意されており、これを使えば面倒な設定無しですぐに開発ができます。
今回は、create-react-kotlin-appを使って Reactの公式チュートリアル をやってみたので、それを見ながらKotlinを使ったReact開発についてお話したいと思います。
環境構築などは公式のREADMEを参照してください。
コードだけ見たい方は こちら
追記
READMEの日本語訳が公式にマージされました!
英語が辛くて進められない方はぜひこちらをご参照ください!
この記事が対象とする人
- JavaScriptはちょっと分かる人
- Reactのチュートリアルくらいはやったことがあるレベル以上の人
- Kotlinに興味がある人
- Kotlinがちょっとわかる人
ディレクトリ構造
src
├── Board
│ ├── Board.css
│ └── Board.kt
├── Game
│ ├── Game.css
│ └── Game.kt
├── Square
│ ├── Square.css
│ └── Square.kt
└── main
├── main.css
└── main.kt
関数コンポーネント
関数コンポーネントは、例えばチュートリアルのSquareコンポーネントは次のように書くことができます。
package Square
import kotlinx.html.js.onClickFunction
import org.w3c.dom.events.Event
import react.RBuilder
import react.dom.button
fun RBuilder.square(value: String?, onClickFunction: (Event) -> Unit) =
button(classes = "square"){
+(value ?: "")
attrs.onClickFunction = onClickFunction
}
Kotlinでrenderを書くときは、RBuilderというクラスの 拡張関数 として定義します。
今回の場合は square
という拡張関数を定義しました。
これはpropsを引数として受け取る事ができ、その 型を指定することもできます。
Kotlinは null安全 な言語なので、String?
のようにオプショナルな型(nullを許容する型)を指定することもできます。(そうしない場合、nullが入ろうとするとビルドの段階で弾いてくれます)
また、Elmのように、HTMLタグは関数として書くことができます。
コンポーネント
通常のコンポーネントは、以下のように記述します。
package Board
import Square.square
import react.RBuilder
import react.RComponent
import react.RProps
import react.RState
import react.dom.div
class Board: RComponent<Board.Props, RState>() {
interface Props: RProps {
var squares: Array<String?>
var onClickFunction: (Int) -> Unit
}
private fun RBuilder.renderSquare(i: Int) {
square(value = props.squares[i],
onClickFunction = { props.onClickFunction(i) })
}
override fun RBuilder.render() {
div {
div(classes = "board-row") {
renderSquare(0)
renderSquare(1)
renderSquare(2)
}
div(classes = "board-row") {
renderSquare(3)
renderSquare(4)
renderSquare(5)
}
div(classes = "board-row") {
renderSquare(6)
renderSquare(7)
renderSquare(8)
}
}
}
}
fun RBuilder.board(squares:Array<String?>, onClickFunction: (Int) -> Unit) = child(Board::class) {
attrs.squares = squares
attrs.onClickFunction = onClickFunction
}
まず、RComponentクラスを継承したクラスとして、コンポーネントのクラスを定義します。
Propsを受け取る場合はこのように、 RPopsを継承する interface
として定義します。
当然こちらもしっかりと型を記述することができます、見やすくていいですね。
interface Props: RProps {
var squares: Array<String?>
var onClickFunction: (Int) -> Unit
}
ステート
package Game
import Board.board
import kotlinx.html.js.onClickFunction
import react.RBuilder
import react.RComponent
import react.RProps
import react.RState
import react.dom.button
import react.dom.div
import react.dom.li
import react.dom.ol
import kotlin.arrayOf
import react.setState
class Game: RComponent<RProps, Game.State>() {
init {
state.apply {
history = arrayOf(HistoryEntry(Array(9) { null }))
xIsNext = true
stepNumber = 0
}
}
interface State: RState {
var history: Array<HistoryEntry>
var xIsNext: Boolean
var stepNumber: Int
}
data class HistoryEntry(var squares: Array<String?>)
private fun handleClick(i: Int){
val history = state.history.sliceArray(0..state.stepNumber)
val current= history.last()
val squares = current.squares.copyOf()
if (calculateWinner(squares) != null || squares[i] != null) return
squares[i] = if(state.xIsNext) "X" else "O"
setState{
this.history = history + HistoryEntry(squares)
xIsNext = !state.xIsNext
stepNumber = history.size
}
}
private fun calculateWinner(squares:Array<String?>): String? {
val lines: Array<Array<Int>> = arrayOf(
arrayOf(0, 1, 2),
arrayOf(3, 4, 5),
arrayOf(6, 7, 8),
arrayOf(0, 3, 6),
arrayOf(1, 4, 7),
arrayOf(2, 5, 8),
arrayOf(0, 4, 8),
arrayOf(2, 4, 6)
)
for (line in lines) {
val (a, b, c) = line
if (squares[a] != null && squares[a] == squares[b] && squares[a] == squares[c]) {
return squares[a]
}
}
return null
}
private fun jumpTo (step: Int) {
setState{
stepNumber = step
xIsNext = (step % 2) == 0
}
}
private fun RBuilder.moves () =
state.history.mapIndexed{ step, _ ->
val desc = when {
step != 0 -> "Go to move #$step"
else -> "Go to game start"
}
li {
button {
+ desc
attrs.onClickFunction = { jumpTo(step) }
}
}
}
override fun RBuilder.render() {
val history = state.history
val current = history[state.stepNumber]
val winner = calculateWinner(current.squares)
val status: String =
when {
winner != null -> "Winner: $winner"
else -> "Next player: ${if (state.xIsNext) "X" else "O"}"
}
div(classes = "game") {
div(classes = "game-board") {
board(current.squares){
handleClick(it)
}
}
div(classes = "game-info") {
div { + status}
ol{ moves ()}
}
}
}
}
fun RBuilder.game() = child(Game::class) {}
Stateを使う場合も、Propsのようにしっかり interface
で定義します。初期化は init
内部で行います。
Kotlinでは data class
があるので、今回の HistoryEntry
のように型を自分で定義することもできます。
初期化を分ける、interfaceでしっかりと型を定義する、などにより、より見通しが良くて安全な状態管理ができます。
メリット
- モダンな文法で書ける -> ここまで紹介してきたコードに頻出する
when
などのモダンな文法を使用できます - オブジェクト指向の考え方が使える
- JSXより読みやすい(好みによるかも)
- 圧倒的安全性
- バックエンドやモバイルネイティブと同じ言語で書ける -> これはJSも同じなので好みや案件によりそう
- IntelliJ IDEAの恩恵を全面的に受けられる
デメリット
- 学習コストが高い -> オブジェクト指向やJavaから受け継いだ考え方を覚えなければいけない、自由度が高すぎる
- 開発コストが高い -> 安全性のために型をいちいちしっかり定義しないと行けないので、それを手間に感じるのならおすすめしません
- 情報がない -> Kotlin/JS自体がかなり最近の技術なので、日本語で書かれた情報は殆どありません、逆に今記事を書けば誰でも先駆者になれます
- jsライブラリを使いたいときの設定がかなりめんどくさい/情報がない
まとめ
総合的には、まだまだ難点も多く、実際の案件で運用するのは難しいかもしれませんが、コミュニティを見ているとJetBrainsはかなり本気だと思うので、そのうちTypeScript並に大きくなってくれるのではないでしょうか。
個人的には、文法の表現力が非常に高く、型もきっちり書けるので書いていてとても楽しかったです。
君もKotlin/JSでモダンにフロントエンド開発をして先駆者になろう!