108
73

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

create-react-kotlin-appでイケてるフロントエンド開発

Last updated at Posted at 2019-06-29

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コンポーネントは次のように書くことができます。

Square.kt
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タグは関数として書くことができます。

コンポーネント

通常のコンポーネントは、以下のように記述します。

Board.kt
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
    }

ステート

Game.kt
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でモダンにフロントエンド開発をして先駆者になろう!

108
73
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
108
73

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?