ReactとKotlin/JSでWebアプリケーションを構築する — チュートリアル
このチュートリアルでは、Kotlin/JSとReactフレームワークを使ってブラウザアプリケーションを構築する方法を学びます。具体的には以下の内容を行います。
- 一般的なReactアプリケーションの構築に関連するタスクを完了する。
- KotlinのDSLが、簡潔かつ統一的に概念を表現しつつ可読性を損なわないようにどのように利用できるかを探求し、完全にKotlinで本格的なアプリケーションを記述できるようにする。
- 既製のnpmコンポーネントの使用方法、外部ライブラリの使用方法、および最終アプリケーションの公開方法を学ぶ。
成果物として、KotlinConfイベントに特化した_KotlinConf Explorer_ウェブアプリが作成され、カンファレンストークへのリンクが含まれます。ユーザーはすべてのトークを1つのページで視聴し、視聴済みまたは未視聴としてマークすることができます。
このチュートリアルは、Kotlinの事前知識とHTMLおよびCSSの基本的な知識があることを前提としています。Reactの基本的な概念を理解していると、一部のサンプルコードの理解に役立つかもしれませんが、厳密には必須ではありません。
NOTE
最終アプリケーションはこちらから入手できます。
開始する前に
IntelliJ IDEAの最新バージョンをダウンロードしてインストールします。
プロジェクトテンプレートをクローンし、IntelliJ IDEAで開きます。このテンプレートには、必要なすべての構成と依存関係が含まれた基本的なKotlin Multiplatform Gradleプロジェクトが含まれています。
build.gradle.kts
ファイルの依存関係とタスク:
kotlindependencies { // React, React DOM + Wrappers implementation(enforcedPlatform("org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom:1.0.0-pre.430")) implementation("org.jetbrains.kotlin-wrappers:kotlin-react") implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom") // Kotlin React Emotion (CSS) implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion") // Video Player implementation(npm("react-player", "2.12.0")) // Share Buttons implementation(npm("react-share", "4.4.1")) // Coroutines & serialization implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") }
- このチュートリアルで使用するJavaScriptコードを挿入するための、
src/jsMain/resources/index.html
にあるHTMLテンプレートページ:
html<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello, Kotlin/JS!</title> </head> <body> <div id="root"></div> <script src="confexplorer.js"></script> </body> </html>
Kotlin/JSプロジェクトは、ビルド時にすべてのコードとその依存関係を、プロジェクトと同じ名前である
confexplorer.js
という単一のJavaScriptファイルに自動的にバンドルします。一般的なJavaScriptの慣習として、ブラウザがスクリプトの前にすべてのページ要素を読み込むことを保証するために、ボディの内容(root
divを含む)が最初にロードされます。
src/jsMain/kotlin/Main.kt
にあるコードスニペット:kotlinimport kotlinx.browser.document fun main() { document.bgColor = "red" }
開発サーバーを起動する
デフォルトでは、Kotlin Multiplatform Gradleプラグインには組み込みのwebpack-dev-server
のサポートが含まれており、手動でサーバーを設定することなくIDEからアプリケーションを実行できます。
プログラムがブラウザで正常に実行されることをテストするには、IntelliJ IDEA内のGradleツールウィンドウからrun
またはbrowserDevelopmentRun
タスク(other
またはkotlin browser
ディレクトリで利用可能)を呼び出して開発サーバーを起動します。
ターミナルからプログラムを実行するには、代わりに./gradlew run
を使用します。
プロジェクトがコンパイルされバンドルされると、ブラウザウィンドウに赤い空白のページが表示されます。
ホットリロード / 連続モードを有効にする
変更を加えるたびに手動でプロジェクトをコンパイルおよび実行する必要がないように、_連続コンパイル_モードを設定します。続行する前に、実行中の開発サーバーインスタンスをすべて停止していることを確認してください。
Gradleの
run
タスクを初めて実行した後にIntelliJ IDEAが自動生成する実行構成を編集します:Run/Debug Configurationsダイアログで、実行構成の引数に
--continuous
オプションを追加します:変更を適用した後、IntelliJ IDEA内のRunボタンを使用して開発サーバーを再起動できます。ターミナルから連続Gradleビルドを実行するには、代わりに
./gradlew run --continuous
を使用します。この機能をテストするには、Gradleタスクの実行中に
Main.kt
ファイルのページの色を青に変更します:kotlindocument.bgColor = "blue"
するとプロジェクトが再コンパイルされ、リロード後にブラウザページが新しい色になります。
開発プロセス中は、開発サーバーを連続モードで実行したままにできます。変更を加えると、自動的にページが再ビルドおよびリロードされます。
NOTE
この状態のプロジェクトは、master
ブランチのこちらで確認できます。
ウェブアプリのドラフトを作成する
Reactで最初の静的ページを追加する
アプリで簡単なメッセージを表示するには、Main.kt
ファイルのコードを以下に置き換えます。
import kotlinx.browser.document
import react.*
import emotion.react.css
import csstype.Position
import csstype.px
import react.dom.html.ReactHTML.h1
import react.dom.html.ReactHTML.h3
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.p
import react.dom.html.ReactHTML.img
import react.dom.client.createRoot
import kotlinx.serialization.Serializable
fun main() {
val container = document.getElementById("root") ?: error("Couldn't find root container!")
createRoot(container).render(Fragment.create {
h1 {
+"Hello, React+Kotlin/JS!"
}
})
}
render()
関数は、kotlin-react-domに、フラグメント内の最初のHTML要素をroot
要素にレンダリングするよう指示します。この要素は、テンプレートに含まれていたsrc/jsMain/resources/index.html
で定義されているコンテナです。- 内容は
<h1>
ヘッダーで、型安全なDSLを使用してHTMLをレンダリングします。 h1
はラムダパラメータを取る関数です。文字列リテラルの前に+
記号を追加すると、実際には演算子オーバーロードを使用してunaryPlus()
関数が呼び出されます。これは、文字列を囲まれたHTML要素に追加します。
プロジェクトが再コンパイルされると、ブラウザはこのHTMLページを表示します。
HTMLをKotlinの型安全なHTML DSLに変換する
React用のKotlinのラッパーには、純粋なKotlinコードでHTMLを記述することを可能にするドメイン固有言語 (DSL)が付属しています。この点では、JavaScriptのJSXに似ています。しかし、このマークアップがKotlinであるため、オートコンプリートや型チェックなど、静的型付け言語のすべての利点が得られます。
将来のウェブアプリの従来のHTMLコードと、Kotlinでの型安全なバリアントを比較します。
Kotlinコードをコピーし、main()
関数内のFragment.create()
関数呼び出しを更新して、以前のh1
タグを置き換えます。
ブラウザがリロードされるのを待ちます。ページは次のようになるはずです。
マークアップでKotlinのコンストラクトを使用して動画を追加する
このDSLを使用してKotlinでHTMLを記述することにはいくつかの利点があります。ループ、条件、コレクション、文字列補間など、通常のKotlinのコンストラクトを使用してアプリを操作できます。
これで、ハードコードされた動画リストをKotlinオブジェクトのリストに置き換えることができます。
Main.kt
に、すべての動画属性を1か所にまとめるためのVideo
データクラスを作成します:kotlindata class Video( val id: Int, val title: String, val speaker: String, val videoUrl: String )
未視聴動画と視聴済み動画の2つのリストをそれぞれ作成します。これらの宣言を
Main.kt
のファイルレベルに追加します:kotlinval unwatchedVideos = listOf( Video(1, "Opening Keynote", "Andrey Breslav", "https://youtu.be/PsaFVLr8t4E"), Video(2, "Dissecting the stdlib", "Huyen Tue Dao", "https://youtu.be/Fzt_9I733Yg"), Video(3, "Kotlin and Spring Boot", "Nicolas Frankel", "https://youtu.be/pSiZVAeReeg") ) val watchedVideos = listOf( Video(4, "Creating Internal DSLs in Kotlin", "Venkat Subramaniam", "https://youtu.be/JzTeAM8N1-o") )
これらの動画をページで使用するには、Kotlinの
for
ループを記述して未視聴のVideo
オブジェクトのコレクションを反復処理します。「視聴する動画」の下にある3つのp
タグを次のスニペットに置き換えます:kotlinfor (video in unwatchedVideos) { p { +"${video.speaker}: ${video.title}" } }
「視聴済み動画」に続く単一のタグのコードを修正するために、同じプロセスを適用します。
kotlinfor (video in watchedVideos) { p { +"${video.speaker}: ${video.title}" } }
ブラウザがリロードされるのを待ちます。レイアウトは以前と同じままのはずです。ループが機能していることを確認するために、リストにさらに動画を追加できます。
型安全なCSSでスタイルを追加する
Emotionライブラリ用のkotlin-emotionラッパーは、CSS属性(動的なものも含む)をJavaScriptとHTMLのすぐ隣で指定することを可能にします。概念的には、これはCSS-in-JSに似ていますが、Kotlin向けです。DSLを使用する利点は、Kotlinコードのコンストラクトを使用して書式設定ルールを表現できることです。
このチュートリアルのテンプレートプロジェクトには、kotlin-emotion
を使用するために必要な依存関係がすでに含まれています。
dependencies {
// ...
// Kotlin React Emotion (CSS) (chapter 3)
implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion")
// ...
}
kotlin-emotion
を使用すると、HTML要素div
とh3
内にcss
ブロックを指定し、そこでスタイルを定義できます。
動画プレーヤーをページ右上の角に移動するには、CSSを使用して動画プレーヤーのコード(スニペットの最後のdiv
)を調整します。
div {
css {
position = Position.absolute
top = 10.px
right = 10.px
}
h3 {
+"John Doe: Building and breaking things"
}
img {
src = "https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder"
}
}
他のスタイルも自由に試してみてください。たとえば、fontFamily
を変更したり、UIにcolor
を追加したりできます。
アプリコンポーネントを設計する
Reactの基本的な構成要素は_コンポーネント_と呼ばれます。コンポーネント自体も、他のより小さなコンポーネントで構成できます。コンポーネントを組み合わせることで、アプリケーションを構築します。コンポーネントを汎用的で再利用可能なように構造化すれば、コードやロジックを重複させることなく、アプリの複数の部分でそれらを使用できるようになります。
render()
関数の内容は、一般的に基本的なコンポーネントを記述します。現在のアプリケーションのレイアウトは次のようになっています。
アプリケーションを個々のコンポーネントに分解すると、各コンポーネントがそれぞれの責務を処理する、より構造化されたレイアウトになります。
コンポーネントは特定の機能をカプセル化します。コンポーネントを使用すると、ソースコードが短くなり、読み書きが容易になります。
メインコンポーネントを追加する
アプリケーションの構造の作成を開始するには、まずroot
要素へのレンダリングのためのメインコンポーネントであるApp
を明示的に指定します。
src/jsMain/kotlin
フォルダに新しいApp.kt
ファイルを作成します。このファイル内に次のスニペットを追加し、
Main.kt
から型安全なHTMLをそこへ移動します:kotlinimport kotlinx.coroutines.async import react.* import react.dom.* import kotlinx.browser.window import kotlinx.coroutines.* import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import emotion.react.css import csstype.Position import csstype.px import react.dom.html.ReactHTML.h1 import react.dom.html.ReactHTML.h3 import react.dom.html.ReactHTML.div import react.dom.html.ReactHTML.p import react.dom.html.ReactHTML.img val App = FC<Props> { // typesafe HTML goes here, starting with the first h1 tag! }
FC
関数は、関数コンポーネントを作成します。Main.kt
ファイルで、main()
関数を次のように更新します:kotlinfun main() { val container = document.getElementById("root") ?: error("Couldn't find root container!") createRoot(container).render(App.create()) }
これでプログラムは
App
コンポーネントのインスタンスを作成し、指定されたコンテナにレンダリングします。
Reactの概念の詳細については、ドキュメントとガイドを参照してください。
リストコンポーネントを抽出する
watchedVideos
とunwatchedVideos
の各リストが動画のリストを含んでいるため、単一の再利用可能なコンポーネントを作成し、リストに表示される内容だけを調整するのは理にかなっています。
VideoList
コンポーネントはApp
コンポーネントと同じパターンに従います。FC
ビルダー関数を使用し、unwatchedVideos
リストのコードを含んでいます。
src/jsMain/kotlin
フォルダに新しいVideoList.kt
ファイルを作成し、以下のコードを追加します:kotlinimport kotlinx.browser.window import react.* import react.dom.* import react.dom.html.ReactHTML.p val VideoList = FC<Props> { for (video in unwatchedVideos) { p { +"${video.speaker}: ${video.title}" } } }
App.kt
で、VideoList
コンポーネントをパラメータなしで呼び出して使用します:kotlin// . . . div { h3 { +"Videos to watch" } VideoList() h3 { +"Videos watched" } VideoList() } // . . .
今のところ、
App
コンポーネントはVideoList
コンポーネントによって表示されるコンテンツを制御できません。これはハードコードされているため、同じリストが2回表示されます。
コンポーネント間でデータを渡すためにpropsを追加する
VideoList
コンポーネントを再利用するため、異なるコンテンツで埋められるようにする必要があります。アイテムのリストをコンポーネントへの属性として渡す機能を追加できます。Reactでは、これらの属性は_props_と呼ばれます。コンポーネントのpropsがReactで変更されると、フレームワークは自動的にコンポーネントを再レンダリングします。
VideoList
の場合、表示する動画のリストを含むpropsが必要になります。VideoList
コンポーネントに渡すことができるすべてのpropsを保持するインターフェースを定義します。
VideoList.kt
ファイルに以下の定義を追加します:kotlinexternal interface VideoListProps : Props { var videos: List<Video> }
external修飾子は、インターフェースの実装が外部で提供されることをコンパイラに伝え、宣言からJavaScriptコードを生成しようとしないようにします。
VideoList
のクラス定義を調整し、FC
ブロックにパラメータとして渡されるpropsを使用するようにします:kotlinval VideoList = FC<VideoListProps> { props -> for (video in props.videos) { p { key = video.id.toString() +"${video.speaker}: ${video.title}" } } }
key
属性は、Reactレンダラーがprops.videos
の値が変更されたときに何をすべきかを判断するのに役立ちます。キーを使用して、リストのどの部分を更新する必要があり、どれが同じままであるかを決定します。リストとキーの詳細については、Reactガイドを参照してください。App
コンポーネントで、子コンポーネントが適切な属性でインスタンス化されていることを確認します。App.kt
で、h3
要素の下にある2つのループを、unwatchedVideos
とwatchedVideos
の属性を持つVideoList
の呼び出しに置き換えます。Kotlin DSLでは、VideoList
コンポーネントに属するブロック内でそれらを割り当てます:kotlinh3 { +"Videos to watch" } VideoList { videos = unwatchedVideos } h3 { +"Videos watched" } VideoList { videos = watchedVideos }
リロード後、ブラウザはリストが正しくレンダリングされることを示します。
リストをインタラクティブにする
まず、ユーザーがリストのエントリをクリックしたときにポップアップするアラートメッセージを追加します。VideoList.kt
で、現在の動画を含むアラートをトリガーするonClick
ハンドラー関数を追加します。
// . . .
p {
key = video.id.toString()
onClick = {
window.alert("Clicked $video!")
}
+"${video.speaker}: ${video.title}"
}
// . . .
ブラウザウィンドウのリストアイテムのいずれかをクリックすると、次のようなアラートウィンドウで動画に関する情報が表示されます。
TIP
ラムダとして直接onClick
関数を定義するのは簡潔でプロトタイプ作成に非常に便利です。しかし、Kotlin/JSでの等価性の現在の動作のため、パフォーマンスの観点からは、クリックハンドラーを渡す最も最適化された方法ではありません。レンダリングパフォーマンスを最適化したい場合は、関数を変数に格納して渡すことを検討してください。
値を保持するための状態を追加する
ユーザーにアラートを出すだけでなく、選択された動画を▶︎の三角形で強調表示する機能を追加できます。そのためには、このコンポーネントに固有の_状態_を導入します。
状態はReactのコア概念の1つです。現代のReact(いわゆる_Hooks API_を使用)では、状態はuseState
フックを使用して表現されます。
VideoList
宣言の先頭に以下のコードを追加します:kotlinval VideoList = FC<VideoListProps> { props -> var selectedVideo: Video? by useState(null) // . . .
VideoList
関数コンポーネントは状態(現在の関数呼び出しとは独立した値)を保持します。状態はnull許容で、Video?
型を持ちます。そのデフォルト値はnull
です。- Reactの
useState()
関数は、関数の複数の呼び出しにわたって状態を追跡するようフレームワークに指示します。たとえば、デフォルト値を指定しても、Reactはデフォルト値が最初にのみ割り当てられることを保証します。状態が変化すると、コンポーネントは新しい状態に基づいて再レンダリングされます。 by
キーワードは、useState()
が委譲プロパティとして機能することを示します。他の変数と同様に、値を読み書きします。useState()
の背後にある実装は、状態を機能させるために必要な機構を処理します。
State Hookについてさらに詳しく知るには、Reactドキュメントを参照してください。
onClick
ハンドラーとVideoList
コンポーネントのテキストを次のように変更します:kotlinval VideoList = FC<VideoListProps> { props -> var selectedVideo: Video? by useState(null) for (video in props.videos) { p { key = video.id.toString() onClick = { selectedVideo = video } if (video == selectedVideo) { +"▶ " } +"${video.speaker}: ${video.title}" } } }
- ユーザーが動画をクリックすると、その値が
selectedVideo
変数に割り当てられます。 - 選択されたリストエントリがレンダリングされると、三角形が先頭に追加されます。
状態管理の詳細については、React FAQを参照してください。
- ユーザーが動画をクリックすると、その値が
ブラウザを確認し、リスト内の項目をクリックしてすべてが正しく機能していることを確認します。
コンポーネントを構成する
現在、2つの動画リストはそれぞれ独立して機能しており、各リストが選択された動画を追跡しています。プレイヤーが1つしかないにもかかわらず、ユーザーは未視聴リストと視聴済みリストの両方からそれぞれ1つずつ、合計2つの動画を選択できます。
リストは、自身の中と、兄弟リストの中の両方でどの動画が選択されているかを追跡することはできません。その理由は、選択された動画が_リスト_の状態ではなく、_アプリケーション_の状態の一部であるためです。これは、個々のコンポーネントから状態を_持ち上げる_必要があることを意味します。
状態を持ち上げる (Lift state)
Reactは、propsが親コンポーネントから子コンポーネントにのみ渡されることを保証します。これにより、コンポーネントが密結合になるのを防ぎます。
コンポーネントが兄弟コンポーネントの状態を変更したい場合、それは親コンポーネントを介して行う必要があります。その時点で、状態はもはや子コンポーネントのいずれにも属さず、全体を統括する親コンポーネントに属します。
コンポーネントから親への状態の移行プロセスは_状態の持ち上げ_(lifting state)と呼ばれます。アプリでは、currentVideo
をApp
コンポーネントの状態として追加します。
App.kt
で、App
コンポーネントの定義の先頭に以下を追加します:kotlinval App = FC<Props> { var currentVideo: Video? by useState(null) // . . . }
VideoList
コンポーネントは状態を追跡する必要がなくなります。代わりに、現在の動画をプロパティとして受け取ります。VideoList.kt
のuseState()
呼び出しを削除します。選択された動画をpropとして受け取るように
VideoList
コンポーネントを準備します。そのためには、VideoListProps
インターフェースを拡張してselectedVideo
を含めます:kotlinexternal interface VideoListProps : Props { var videos: List<Video> var selectedVideo: Video? }
三角形の条件を、
state
の代わりにprops
を使用するように変更します:kotlinif (video == props.selectedVideo) { +"▶ " }
ハンドラーを渡す
現時点では、propに値を割り当てる方法がないため、onClick
関数は現在の設定では機能しません。親コンポーネントの状態を変更するには、再度状態を持ち上げる必要があります。
Reactでは、状態は常に親から子へと流れます。そのため、子コンポーネントの1つから_アプリケーション_の状態を変更するには、ユーザーインタラクションを処理するロジックを親コンポーネントに移動し、そのロジックをpropとして渡す必要があります。Kotlinでは、変数に関数の型を持たせることができることを忘れないでください。
VideoListProps
インターフェースを再度拡張し、Video
を受け取りUnit
を返す関数である変数onSelectVideo
を含むようにします:kotlinexternal interface VideoListProps : Props { // ... var onSelectVideo: (Video) -> Unit }
VideoList
コンポーネントで、新しいpropをonClick
ハンドラーで使用します:kotlinonClick = { props.onSelectVideo(video) }
これで、
selectedVideo
変数をVideoList
コンポーネントから削除できます。App
コンポーネントに戻り、selectedVideo
とonSelectVideo
のハンドラーを2つの動画リストそれぞれに渡します:kotlinVideoList { videos = unwatchedVideos // and watchedVideos respectively selectedVideo = currentVideo onSelectVideo = { video -> currentVideo = video } }
視聴済み動画リストに対しても前のステップを繰り返します。
ブラウザに戻り、動画を選択したときに選択が重複なく2つのリスト間を移動することを確認してください。
その他のコンポーネントを追加する
動画プレーヤーコンポーネントを抽出する
これで、現在プレースホルダー画像である、別の自己完結型コンポーネントである動画プレーヤーを作成できます。動画プレーヤーは、トークのタイトル、トークの著者、および動画へのリンクを知る必要があります。この情報は各Video
オブジェクトにすでに含まれているため、propとして渡してその属性にアクセスできます。
新しい
VideoPlayer.kt
ファイルを作成し、以下の実装をVideoPlayer
コンポーネントに追加します:kotlinimport csstype.* import react.* import emotion.react.css import react.dom.html.ReactHTML.button import react.dom.html.ReactHTML.div import react.dom.html.ReactHTML.h3 import react.dom.html.ReactHTML.img external interface VideoPlayerProps : Props { var video: Video } val VideoPlayer = FC<VideoPlayerProps> { props -> div { css { position = Position.absolute top = 10.px right = 10.px } h3 { +"${props.video.speaker}: ${props.video.title}" } img { src = "https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder" } } }
VideoPlayerProps
インターフェースがVideoPlayer
コンポーネントがnull許容でないVideo
を受け取ることを指定しているため、App
コンポーネントでそれに応じた処理を確実に行います。App.kt
で、動画プレーヤーの以前のdiv
スニペットを以下に置き換えます。kotlincurrentVideo?.let { curr -> VideoPlayer { video = curr } }
let
スコープ関数は、VideoPlayer
コンポーネントがstate.currentVideo
がnullでない場合にのみ追加されることを保証します。
これでリストのエントリをクリックすると動画プレーヤーが表示され、クリックされたエントリの情報が読み込まれます。
ボタンを追加して接続する
ユーザーが動画を視聴済みまたは未視聴としてマークし、2つのリスト間で移動できるようにするには、VideoPlayer
コンポーネントにボタンを追加します。
このボタンは2つの異なるリスト間で動画を移動するため、状態変更を処理するロジックをVideoPlayer
から_持ち上げ_、親からpropとして渡す必要があります。ボタンは、動画が視聴済みかどうかに基づいて異なる表示になるはずです。これもpropとして渡す必要がある情報です。
VideoPlayer.kt
のVideoPlayerProps
インターフェースを拡張して、それら2つのケースのプロパティを含めます:kotlinexternal interface VideoPlayerProps : Props { var video: Video var onWatchedButtonPressed: (Video) -> Unit var unwatchedVideo: Boolean }
これで、実際のコンポーネントにボタンを追加できます。以下のスニペットを
VideoPlayer
コンポーネントのボディ、h3
タグとimg
タグの間にコピーします:kotlinbutton { css { display = Display.block backgroundColor = if (props.unwatchedVideo) NamedColor.lightgreen else NamedColor.red } onClick = { props.onWatchedButtonPressed(props.video) } if (props.unwatchedVideo) { +"Mark as watched" } else { +"Mark as unwatched" } }
スタイルを動的に変更できるKotlin CSS DSLの助けを借りて、基本的なKotlinの
if
式を使用してボタンの色を変更できます。
動画リストをアプリケーションの状態に移動する
ここで、App
コンポーネント内のVideoPlayer
の使用箇所を調整します。ボタンがクリックされると、動画は未視聴リストから視聴済みリストへ、またはその逆に移動するはずです。これらのリストは実際に変更できるようになったため、それらをアプリケーションの状態に移動します。
App.kt
で、以下のプロパティをuseState()
呼び出しとともにApp
コンポーネントの先頭に追加します:kotlinval App = FC<Props> { var currentVideo: Video? by useState(null) var unwatchedVideos: List<Video> by useState(listOf( Video(1, "Opening Keynote", "Andrey Breslav", "https://youtu.be/PsaFVLr8t4E"), Video(2, "Dissecting the stdlib", "Huyen Tue Dao", "https://youtu.be/Fzt_9I733Yg"), Video(3, "Kotlin and Spring Boot", "Nicolas Frankel", "https://youtu.be/pSiZVAeReeg") )) var watchedVideos: List<Video> by useState(listOf( Video(4, "Creating Internal DSLs in Kotlin", "Venkat Subramaniam", "https://youtu.be/JzTeAM8N1-o") )) // . . . }
すべてのデモデータが
watchedVideos
とunwatchedVideos
のデフォルト値に直接含まれているため、ファイルレベルの宣言は不要になりました。Main.kt
で、watchedVideos
とunwatchedVideos
の宣言を削除します。動画プレーヤーに属する
App
コンポーネントのVideoPlayer
の呼び出し箇所を次のように変更します:kotlinVideoPlayer { video = curr unwatchedVideo = curr in unwatchedVideos onWatchedButtonPressed = { if (video in unwatchedVideos) { unwatchedVideos = unwatchedVideos - video watchedVideos = watchedVideos + video } else { watchedVideos = watchedVideos - video unwatchedVideos = unwatchedVideos + video } } }
ブラウザに戻り、動画を選択してボタンを数回押します。動画は2つのリスト間を移動します。
npmのパッケージを使用する
アプリを使用可能にするには、実際に動画を再生する動画プレーヤーと、コンテンツを共有するためのいくつかのボタンが必要です。
Reactには豊富なエコシステムがあり、この機能を自分で構築する代わりに、既製のコンポーネントをたくさん使用できます。
動画プレーヤーコンポーネントを追加する
プレースホルダーの動画コンポーネントを実際のYouTubeプレーヤーに置き換えるには、npmのreact-player
パッケージを使用します。これは動画を再生でき、プレーヤーの表示を制御できます。
コンポーネントのドキュメントとAPIの説明については、GitHubのREADMEを参照してください。
build.gradle.kts
ファイルを確認します。react-player
パッケージはすでに含まれているはずです:kotlindependencies { // ... // Video Player implementation(npm("react-player", "2.12.0")) // ... }
ご覧のように、ビルドファイルの
dependencies
ブロックでnpm()
関数を使用することで、Kotlin/JSプロジェクトにnpm依存関係を追加できます。その後、Gradleプラグインがこれらの依存関係のダウンロードとインストールを代行します。これを行うために、独自のバンドルされたYarnパッケージマネージャーのインストールを使用します。Reactアプリケーション内からJavaScriptパッケージを使用するには、外部宣言を提供してKotlinコンパイラに何を期待するかを伝える必要があります。
新しい
ReactYouTube.kt
ファイルを作成し、以下の内容を追加します。kotlin@file:JsModule("react-player") @file:JsNonModule import react.* @JsName("default") external val ReactPlayer: ComponentClass<dynamic>
コンパイラが
ReactPlayer
のような外部宣言を見ると、対応するクラスの実装が依存関係によって提供されると仮定し、そのコードを生成しません。最後の2行は、
require("react-player").default;
のようなJavaScriptインポートと同等です。これらは、コンポーネントが実行時にComponentClass<dynamic>
に準拠することが確実であることをコンパイラに伝えます。
しかし、この設定では、ReactPlayer
が受け入れるpropsのジェネリック型がdynamic
に設定されています。これは、コンパイラがあらゆるコードを受け入れることを意味し、実行時に問題を引き起こすリスクがあります。
より良い代替案は、この外部コンポーネントのpropsにどのようなプロパティが属するかを指定するexternal interface
を作成することです。コンポーネントのREADMEでpropsのインターフェースについて学ぶことができます。この場合、url
とcontrols
のpropsを使用します。
ReactYouTube.kt
の内容を、dynamic
を外部インターフェースに置き換えることで調整します:kotlin@file:JsModule("react-player") @file:JsNonModule import react.* @JsName("default") external val ReactPlayer: ComponentClass<ReactPlayerProps> external interface ReactPlayerProps : Props { var url: String var controls: Boolean }
これで、新しい
ReactPlayer
を使用して、VideoPlayer
コンポーネントの灰色のプレースホルダー四角形を置き換えることができます。VideoPlayer.kt
で、img
タグを以下のスニペットに置き換えます:kotlinReactPlayer { url = props.video.videoUrl controls = true }
ソーシャルシェアボタンを追加する
アプリケーションのコンテンツを共有する簡単な方法は、メッセンジャーやメール用のソーシャルシェアボタンを用意することです。これにも既製のReactコンポーネントを使用できます。たとえば、react-shareがあります。
build.gradle.kts
ファイルを確認します。このnpmライブラリはすでに含まれているはずです:kotlindependencies { // ... // Share Buttons implementation(npm("react-share", "4.4.1")) // ... }
Kotlinから
react-share
を使用するには、さらに基本的な外部宣言を記述する必要があります。GitHubの例を見ると、シェアボタンはたとえばEmailShareButton
とEmailIcon
という2つのReactコンポーネントで構成されていることがわかります。異なる種類のシェアボタンとアイコンはすべて同じインターフェースを持っています。動画プレーヤーの場合と同じ方法で、各コンポーネントの外部宣言を作成します。新しい
ReactShare.kt
ファイルに以下のコードを追加します。kotlin@file:JsModule("react-share") @file:JsNonModule import react.ComponentClass import react.Props @JsName("EmailIcon") external val EmailIcon: ComponentClass<IconProps> @JsName("EmailShareButton") external val EmailShareButton: ComponentClass<ShareButtonProps> @JsName("TelegramIcon") external val TelegramIcon: ComponentClass<IconProps> @JsName("TelegramShareButton") external val TelegramShareButton: ComponentClass<ShareButtonProps> external interface ShareButtonProps : Props { var url: String } external interface IconProps : Props { var size: Int var round: Boolean }
アプリケーションのユーザーインターフェースに新しいコンポーネントを追加します。
VideoPlayer.kt
で、ReactPlayer
の使用直前のdiv
内に2つのシェアボタンを追加します:kotlin// . . . div { css { position = Position.absolute top = 10.px right = 10.px } EmailShareButton { url = props.video.videoUrl EmailIcon { size = 32 round = true } } TelegramShareButton { url = props.video.videoUrl TelegramIcon { size = 32 round = true } } } // . . .
ブラウザを確認し、ボタンが実際に機能するかどうかを確認できます。ボタンをクリックすると、動画のURLを含む_共有ウィンドウ_が表示されるはずです。ボタンが表示されない、または機能しない場合は、広告ブロッカーやソーシャルメディアブロッカーを無効にする必要があるかもしれません。
react-shareで利用可能な他のソーシャルネットワークのシェアボタンでも、この手順を自由に繰り返してください。
外部REST APIを使用する
これで、アプリ内のハードコードされたデモデータを、REST APIからの実際のデータに置き換えることができます。
このチュートリアルでは、小さなAPIがあります。これはvideos
という単一のエンドポイントのみを提供し、リストから要素にアクセスするための数値パラメータを取ります。ブラウザでAPIにアクセスすると、APIから返されるオブジェクトがVideo
オブジェクトと同じ構造を持っていることがわかります。
KotlinからJSの機能を使用する
ブラウザにはすでに多種多様なWeb APIが搭載されています。Kotlin/JSにはこれらのAPIのラッパーがすぐに含まれているため、Kotlin/JSからそれらを使用することもできます。一例として、HTTPリクエストの作成に使用されるfetch APIがあります。
最初の潜在的な問題は、fetch()
のようなブラウザAPIが非ブロッキング操作を実行するためにコールバックを使用することです。複数のコールバックが連続して実行されるべき場合、それらはネストする必要があります。当然、コードは深くインデントされ、ますます多くの機能が互いに入れ子になり、読みづらくなります。
これを克服するために、そのような機能にはより良いアプローチであるKotlinのコルーチンを使用できます。
2つ目の問題は、JavaScriptの動的な型付けの性質から生じます。外部APIから返されるデータの型について保証はありません。これを解決するために、kotlinx.serialization
ライブラリを使用できます。
build.gradle.kts
ファイルを確認します。関連するスニペットはすでに存在しているはずです。
dependencies {
// . . .
// Coroutines & serialization
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
}
シリアライズを追加する
外部APIを呼び出すと、JSON形式のテキストが返されます。これは、Kotlinオブジェクトとして操作できるように変換する必要があります。
kotlinx.serialization
は、JSON文字列からKotlinオブジェクトへの変換を記述できるようにするライブラリです。
build.gradle.kts
ファイルを確認します。対応するスニペットはすでに存在しているはずです:kotlinplugins { // . . . kotlin("plugin.serialization") version "2.1.21" } dependencies { // . . . // Serialization implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") }
最初の動画をフェッチする準備として、シリアライズライブラリに
Video
クラスについて伝える必要があります。Main.kt
で、その定義に@Serializable
アノテーションを追加します:kotlin@Serializable data class Video( val id: Int, val title: String, val speaker: String, val videoUrl: String )
動画をフェッチする
APIから動画をフェッチするには、App.kt
(または新しいファイル)に以下の関数を追加します。
suspend fun fetchVideo(id: Int): Video {
val response = window
.fetch("https://my-json-server.typicode.com/kotlin-hands-on/kotlinconf-json/videos/$id")
.await()
.text()
.await()
return Json.decodeFromString(response)
}
- _中断関数_である
fetch()
は、与えられたid
を持つ動画をAPIからフェッチします。この応答には時間がかかる場合があるため、結果をawait()
します。次に、コールバックを使用するtext()
が応答からボディを読み取ります。その後、その完了をawait()
します。 - 関数の値を返す前に、
kotlinx.coroutines
の関数であるJson.decodeFromString
に渡します。これは、リクエストから受け取ったJSONテキストを適切なフィールドを持つKotlinオブジェクトに変換します。 window.fetch
関数呼び出しはPromise
オブジェクトを返します。通常、Promise
が解決され結果が利用可能になったときに呼び出されるコールバックハンドラーを定義する必要があります。しかし、コルーチンを使用すると、それらのプロミスをawait()
できます。await()
のような関数が呼び出されると、メソッドはその実行を停止(中断)します。Promise
が解決可能になると、その実行が続行されます。
ユーザーに動画の選択肢を提供するために、上記のAPIから25本の動画をフェッチするfetchVideos()
関数を定義します。すべてのリクエストを並行して実行するには、Kotlinのコルーチンが提供するasync
機能を使用します。
App.kt
に以下の実装を追加します:kotlinsuspend fun fetchVideos(): List<Video> = coroutineScope { (1..25).map { id -> async { fetchVideo(id) } }.awaitAll() }
構造化された並行処理の原則に従い、実装は
coroutineScope
でラップされています。これにより、25個の非同期タスク(リクエストごとに1つ)を開始し、それらすべてが完了するのを待つことができます。これでアプリケーションにデータを追加できます。
mainScope
の定義を追加し、App
コンポーネントが以下のスニペットで始まるように変更します。デモ値をemptyLists
インスタンスに置き換えることも忘れないでください:kotlinval mainScope = MainScope() val App = FC<Props> { var currentVideo: Video? by useState(null) var unwatchedVideos: List<Video> by useState(emptyList()) var watchedVideos: List<Video> by useState(emptyList()) useEffectOnce { mainScope.launch { unwatchedVideos = fetchVideos() } } // . . .
MainScope()
はKotlinの構造化された並行処理モデルの一部であり、非同期タスクが実行されるスコープを作成します。useEffectOnce
はもう1つのReact フック(特に、useEffect
フックの簡易版)です。コンポーネントが_副作用_を実行することを示します。これは自身をレンダリングするだけでなく、ネットワーク経由で通信も行います。
ブラウザを確認します。アプリケーションは実際のデータを表示するはずです:
ページをロードすると:
App
コンポーネントのコードが呼び出されます。これにより、useEffectOnce
ブロックのコードが開始されます。App
コンポーネントは、視聴済み動画と未視聴動画の空のリストでレンダリングされます。- APIリクエストが完了すると、
useEffectOnce
ブロックはそれをApp
コンポーネントの状態に割り当てます。これにより再レンダリングがトリガーされます。 App
コンポーネントのコードが再び呼び出されますが、useEffectOnce
ブロックは2回目は実行_されません_。
コルーチンの動作について深く理解したい場合は、コルーチンに関するこのチュートリアルを参照してください。
本番環境とクラウドにデプロイする
アプリケーションをクラウドに公開し、他の人がアクセスできるようにする時が来ました。
プロダクションビルドをパッケージ化する
すべての資産をプロダクションモードでパッケージ化するには、IntelliJ IDEAのツールウィンドウから、または./gradlew build
を実行して、Gradleのbuild
タスクを実行します。これにより、DCE(デッドコード削除)などの様々な改善が適用された最適化されたプロジェクトビルドが生成されます。
ビルドが完了すると、デプロイに必要なすべてのファイルが/build/dist
にあります。これには、JavaScriptファイル、HTMLファイル、およびアプリケーションの実行に必要なその他のリソースが含まれます。これらは静的HTTPサーバーに配置したり、GitHub Pagesを使用して提供したり、選択したクラウドプロバイダーでホストしたりできます。
Herokuにデプロイする
Herokuは、独自のドメインでアクセス可能なアプリケーションを簡単に起動できるようにします。彼らのフリーティアは開発目的には十分でしょう。
アカウントを作成します。
プロジェクトルートにいる間にターミナルで以下のコマンドを実行して、Gitリポジトリを作成し、Herokuアプリをアタッチします:
bashgit init heroku create git add . git commit -m "initial commit"
Herokuで実行される通常のJVMアプリケーション(KtorやSpring Bootで書かれたものなど)とは異なり、このアプリは静的HTMLページとJavaScriptファイルを生成するため、それに応じて提供する必要があります。必要なビルドパックを調整して、プログラムを適切に提供できます:
bashheroku buildpacks:set heroku/gradle heroku buildpacks:add https://github.com/heroku/heroku-buildpack-static.git
heroku/gradle
ビルドパックが適切に実行されるようにするため、build.gradle.kts
ファイルにstage
タスクが必要です。このタスクはbuild
タスクと同等であり、対応するエイリアスはファイルの最後にすでに含まれています:kotlin// Heroku Deployment tasks.register("stage") { dependsOn("build") }
buildpack-static
を設定するために、新しいstatic.json
ファイルをプロジェクトルートに追加します。ファイル内に
root
プロパティを追加します:xml{ "root": "build/distributions" }
たとえば、以下のコマンドを実行してデプロイをトリガーできます:
bashgit add -A git commit -m "add stage task and static content root configuration" git push heroku master
TIP
メイン以外のブランチからプッシュする場合は、main
リモートにプッシュするようにコマンドを調整してください。例: git push heroku feature-branch:main
。
デプロイが成功すると、人々がインターネット上でアプリケーションにアクセスするために使用できるURLが表示されます。
NOTE
この状態のプロジェクトは、finished
ブランチのこちらで確認できます。
次のステップ
さらに機能を追加する
結果として得られたアプリは、React、Kotlin/JSなどの分野でより高度なトピックを探求するための出発点として使用できます。
- 検索。タイトルや著者などでトークのリストをフィルタリングするための検索フィールドを追加できます。ReactでのHTMLフォーム要素の動作について学びましょう。
- 永続化。現在、アプリケーションはページがリロードされるたびに視聴者のウォッチリストを失います。Kotlinで利用可能なWebフレームワーク(Ktorなど)のいずれかを使用して独自のバックエンドを構築することを検討してください。あるいは、クライアント側で情報を保存する方法を調べてください。
- 複雑なAPI。多くのデータセットとAPIが利用可能です。アプリケーションにあらゆる種類のデータを引き込むことができます。たとえば、猫の写真のビジュアライザーや、ロイヤリティフリーのストックフォトAPIを構築できます。
スタイルの改善: レスポンシブとグリッド
アプリケーションのデザインはまだ非常にシンプルであり、モバイルデバイスや狭いウィンドウでは見栄えがよくありません。CSS DSLをさらに探求して、アプリのアクセシビリティを向上させましょう。
コミュニティに参加してヘルプを得る
問題を報告し、ヘルプを得るための最良の方法は、kotlin-wrappersイシュートラッカーです。問題のチケットが見つからない場合は、遠慮なく新しいチケットを提出してください。公式のKotlin Slackに参加することもできます。そこには#javascript
と#react
のチャンネルがあります。
コルーチンについてさらに学ぶ
並行コードの書き方についてもっと詳しく知りたい場合は、コルーチンに関するチュートリアルを参照してください。
Reactについてさらに学ぶ
Reactの基本的な概念と、それがKotlinにどのように変換されるかを理解した今、Reactのドキュメントに概説されている他のいくつかの概念をKotlinに変換することができます。