Von einem Übersetzer .
Hallo! Es gibt ein Stereotyp über Kotlin, dass es eine Sprache für die Entwicklung nur für Android ist. Tatsächlich ist dies überhaupt nicht der Fall: Die Sprache unterstützt offiziell mehrere Plattformen ( JVM , JS , Native ) und weiß auch, wie man mit Bibliotheken für diese Plattformen arbeitet, die in anderen Sprachen geschrieben sind. Diese Unterstützung für "Multiplattformity" ermöglicht nicht nur das Schreiben aller Arten von Projekten in einer Sprache in einer einzigen Form, sondern auch das Wiederverwenden des Codes beim Schreiben eines Projekts für verschiedene Plattformen.
In diesem Artikel übersetze ich das offizielle Kotlin Hands-On-Tutorial zum Erstellen von Websites in Kotlin. Wir werden viele Aspekte der Kotlin / JS-Programmierung behandeln und verstehen, wie man mit mehr als nur einem reinen DOM arbeitet. Wir werden hauptsächlich über React JS sprechen , aber wir werden auch das Gradle- Build-System ansprechen , indem wir Abhängigkeiten von NPM verwenden , die REST-API aufrufen , auf Heroku bereitstellen und schließlich eine Video-Player-Anwendung erstellen .
Der Text richtet sich an diejenigen, die ein wenig Kotlin kennen und React nicht oder kaum kennen. Wenn Sie in diesen Angelegenheiten erfahrener sind, scheinen Ihnen Teile des Tutorials übermäßig gekaut zu sein.

1.
, Kotlin/JS React . React , . , .
React , - . JavaScript.
Kotlin/JS React, Gradle org.jetbrains.kotlin.js
. , React .
, - (DSL) , . , , .
, , HTML CSS. , .
KotlinConf , . KotlinConf 2018 - 1300 . YouTube, – "". – KotlinConf Explorer (. ).

, .
2.
, , . , – IntelliJ IDEA ( 2020.3
, Community Edition) (1.4.30
) – . , ( Windows, MacOS Linux).
, .
GitHub IntelliJ IDEA (, File | New | Project from Version Control... Git | Clone...).
Kotlin/JS Gradle , - . Gradle , .
, , .
: , , Gradle , – .
Gradle
React, , . Gradle , .
, build.gradle.kts
repositories
. .
dependencies
:
dependencies {
// React, React DOM + Wrappers ( 3)
implementation("org.jetbrains:kotlin-react:17.0.1-pre.148-kotlin-1.4.21")
implementation("org.jetbrains:kotlin-react-dom:17.0.1-pre.148-kotlin-1.4.21")
implementation(npm("react", "17.0.1"))
implementation(npm("react-dom", "17.0.1"))
// Kotlin Styled ( 3)
implementation("org.jetbrains:kotlin-styled:5.2.1-pre.148-kotlin-1.4.21")
implementation(npm("styled-components", "~5.2.1"))
// Video Player ( 7)
implementation(npm("react-youtube-lite", "1.0.1"))
// Share Buttons ( 7)
implementation(npm("react-share", "~4.2.1"))
// Coroutines ( 8)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3")
}
, IDEA Gradle . , Reimport All Gradle Projects - Gradle ( ).
HTML
JavaScript , JS HTML , . src/main/resources/index.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 Gradle , ("") JavaScript , . HTML confexplorer.js
(, , , followingAlong
, followingAlong.js
).
JavaScript, ( #root
) . , , .
: HTML, , onLoad
body
. Kotlin/JS body
.
"Hello, World" , – , . , . src/main/kotlin/Main.kt
:
import kotlinx.browser.document
fun main() {
document.bgColor = "red"
}
.
Kotlin/JS Gradle webpack-dev-server, IDE .
, run
browserDevelopmentRun
- Gradle. other
( ), kotlin browser
:

IDE, , ./gradlew run
( Windows Gradle -: .\gradlew.bat run
).
, , , :

(hot reload) a.k.a.
, – Kotlin/JS . run
Gradle.
, ( IDE – Stop; – Ctrl+C
).
IDEA, . IDEA , Gradle , :

Run/Debug Configurations --continuous
:

Run (|>
) .
, : ./gradlew run --continuous
.
, Gradle . , :
document.bgColor = "blue"
, , – .
. . , .
, , , . -, , - . -, , . -, - , , Gradle , – - .
, Kotlin/JS, . , : HTML . browserDevelopmentWebpack
, build/distributions
build/developmentExecutable
. index.html
, .
, ...
Kotlin/JS , . !
master
.
3. –
Hello, World. .
src/main/kotlin/Main.kt
:
import react.dom.*
import kotlinx.browser.document
fun main() {
render(document.getElementById("root")) {
h1 {
+"Hello, React+Kotlin/JS!"
}
}
}
:

, ! , . render
kotlin-react-dom ( ) . , src/main/resources/index.html
ID root
, . – . , HTML , DSL.
HTML
kotlin-react DSL, HTML . , DSL .
, . , - , , !
+
:
– +
. . h1
– , . +
, unaryPlus
, HTML .
, +
" ".
HTML
, , () HTML. HTML, . , HTML:
<h1>KotlinConf Explorer</h1>
<div>
<h3>Videos to watch</h3>
<p>John Doe: Building and breaking things</p>
<p>Jane Smith: The development process</p>
<p>Matt Miller: The Web 7.0</p>
<h3>Videos watched</h3>
<p>Tom Jerry: Mouseless development</p>
</div>
<div>
<h3>John Doe: Building and breaking things</h3>
<img src="https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder">
</div>
Kotlin DSL. . , , :
h1 {
+"KotlinConf Explorer"
}
div {
h3 {
+"Videos to watch"
}
p {
+"John Doe: Building and breaking things"
}
p {
+"Jane Smith: The development process"
}
p {
+"Matt Miller: The Web 7.0"
}
h3 {
+"Videos watched"
}
p {
+"Tom Jerry: Mouseless development"
}
}
div {
h3 {
+"John Doe: Building and breaking things"
}
img {
attrs {
src = "https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder"
}
}
}
render
. IntelliJ IDEA , (quick-fixes) Alt+Enter
. , :

HTML DSL HTML. – , . , , , – HTML DSL , .
- . KotlinVideo
, ( Main.kt
, – ), external
– , API:
external interface Video {
val id: Int
val title: String
val speaker: String
val videoUrl: String
}
data class KotlinVideo(
override val id: Int,
override val title: String,
override val speaker: String,
override val videoUrl: String
) : Video
: . Main.kt
:
val unwatchedVideos = listOf(
KotlinVideo(1, "Building and breaking things", "John Doe", "https://youtu.be/PsaFVLr8t4E"),
KotlinVideo(2, "The development process", "Jane Smith", "https://youtu.be/PsaFVLr8t4E"),
KotlinVideo(3, "The Web 7.0", "Matt Miller", "https://youtu.be/PsaFVLr8t4E")
)
val watchedVideos = listOf(
KotlinVideo(4, "Mouseless development", "Tom Jerry", "https://youtu.be/PsaFVLr8t4E")
)
HTML, , ! HTML . p
, :
for (video in unwatchedVideos) {
p {
+"${video.speaker}: ${video.title}"
}
}
watchedVideos
. , . , , , , .
CSS
, , : , . - .css
index.html
, , Kotlin DSL – CSS.
kotlin-styled styled-components , . CSS-in-JS. , , .
CSS DSL, Gradle. :
dependencies {
//...
// Kotlin Styled ( 3)
implementation("org.jetbrains:kotlin-styled:5.2.1-pre.148-kotlin-1.4.21")
implementation(npm("styled-components", "~5.2.1"))
//...
}
div
h3
styled
, , styledDiv
styledH3
. css
. , , :
styledDiv {
css {
position = Position.absolute
top = 10.px
right = 10.px
}
h3 {
+"John Doe: Building and breaking things"
}
img {
attrs {
src = "https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder"
}
}
}
, IDEA . , :
import kotlinx.css.*
import styled.*
Alt+Enter
.
. – , . CSS Grids, ( ). ( fontFamily
) ( sans-serif
), , , ( color
).
step-02-first-static-page
.
4. React – .
. , , . , , / .
, . , :

, , :

. App
, . App.kt
src/main/kotlin
. App
, RComponent
( React Component). (RProps
RState
), :
import react.*
@JsExport
class App : RComponent<RProps, RState>() {
override fun RBuilder.render() {
// HTML!
}
}
HTML render
. . main
- App
. : App
, child
:
fun main() {
render(document.getElementById("root")) {
child(App::class) {}
}
}
? , – . , , .
VideoList.kt
. App
, VideoList
, RComponent
HTML DSL unwatchedVideos
:
import react.*
import react.dom.*
@JsExport
class VideoList : RComponent<RProps, RState>() {
override fun RBuilder.render() {
for (video in unwatchedVideos) {
p {
+"${video.speaker}: ${video.title}"
}
}
}
}
App
:
div {
h3 {
+"Videos to watch"
}
child(VideoList::class) {}
h3 {
+"Videos watched"
}
child(VideoList::class) {}
}
: App
. . , .
, - . , , . props
. , .
, . . VideoList.kt
:
external interface VideoListProps : RProps {
var videos: List<Video>
}
VideoList
, :
@JsExport
class VideoList : RComponent<VideoListProps, RState>() {
override fun RBuilder.render() {
for (video in props.videos) {
p {
key = video.id.toString()
+"${video.speaker}: ${video.title}"
}
}
}
}
, VideoList
( App
) . unwatchedVideos
watchedVideos
:
child(VideoList::class) {
attrs.videos = unwatchedVideos
}
, , . , . , .
fun RBuilder.videoList(handler: VideoListProps.() -> Unit): ReactElement {
return child(VideoList::class) {
attrs.handler()
}
}
, : videoList
RBuilder
. handler
– - VideoListProps
, Unit
. child
( VideoList
), handler
attrs
.
– :
videoList { videos = unwatchedVideos }
, child
, class
attrs
, . , . ! App
.
- – . , . : alert
.
VideoList.render
. , p
:
p {
key = video.id.toString()
attrs {
onClickFunction = {
window.alert("Clicked $video!")
}
}
+"${video.speaker}: ${video.title}"
}
IntelliJ IDEA , Alt+Enter
. :
import kotlinx.html.js.onClickFunction
import kotlinx.browser.window
:

onClickFunction
, . Kotlin/JS . . ,onClickFunction
.
?
. (|>
). – . – :
external interface VideoListState : RState {
var selectedVideo: Video?
}
:
-
VideoList
,VideoListState
–RComponent<..., VideoListState>
. - .
-
onClickFunction
selectedVideo
, . ,setState
.
, :
@JsExport
class VideoList : RComponent<VideoListProps, VideoListState>() {
override fun RBuilder.render() {
for (video in props.videos) {
p {
key = video.id.toString()
attrs {
onClickFunction = {
setState {
selectedVideo = video
}
}
}
if (video == state.selectedVideo) {
+"|> "
}
+"${video.speaker}: ${video.title}"
}
}
}
}
setState
. UI .
, React FAQ.
step-03-first-component
.
5. .
. , , . , :)

-, – , . ( ) . ( , "" ).
-, : . , . , , . . ! , App
. , VideoList
.
:
external interface AppState : RState {
var currentVideo: Video?
}
App
:
@JsExport
class App : RComponent<RProps, AppState>()
VideoListState
, . , , :
@JsExport
class VideoList : RComponent<VideoListProps, RState>()
App
VideoList
. VideoListProps
, :
external interface VideoListProps : RProps {
var videos: List<Video>
var selectedVideo: Video?
}
, :
if (video == props.selectedVideo) {
+"|> "
}
, : , setState
onClickFunction
. , - .
, , . -: . , ? – , Video
Unit
:
external interface VideoListProps : RProps {
var videos: List<Video>
var selectedVideo: Video?
var onSelectVideo: (Video) -> Unit
}
onClickFunction
:
onClickFunction = { props.onSelectVideo(video) }
, . , . videoList
:
videoList { videos = unwatchedVideos selectedVideo = state.currentVideo onSelectVideo = { video -> setState { currentVideo = video } } }
watchedVideos
.
, : , , . , , .
step-04-composing-components
.
6. !
, . .
, – ( -). , : , . , Video
, . VideoPlayer
VideoPlayer.kt
:
import kotlinx.css.*
import kotlinx.html.js.onClickFunction
import react.*
import react.dom.*
import styled.*
external interface VideoPlayerProps : RProps {
var video: Video
}
@JsExport
class VideoPlayer : RComponent<VideoPlayerProps, RState>() {
override fun RBuilder.render() {
styledDiv {
css {
position = Position.absolute
top = 10.px
right = 10.px
}
h3 {
+"${props.video.speaker}: ${props.video.title}"
}
img {
attrs {
src = "https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder"
}
}
}
}
}
fun RBuilder.videoPlayer(handler: VideoPlayerProps.() -> Unit): ReactElement {
return child(VideoPlayer::class) {
this.attrs(handler)
}
}
styledDiv
( App.kt
) . , - – let
, let
, currentVideo
null
:
state.currentVideo?.let { currentVideo -> videoPlayer { video = currentVideo } }
. VideoPlayer
.
, VideoPlayer
. , .
-. , , , , . .
VideoPlayerProps
:
external interface VideoPlayerProps : RProps {
var video: Video
var onWatchedButtonPressed: (Video) -> Unit
var unwatchedVideo: Boolean
}
, . CSS : . HTML DSL render
, h3
img
:
styledButton {
css {
display = Display.block
backgroundColor = if (props.unwatchedVideo) Color.lightGreen else Color.red
}
attrs {
onClickFunction = {
props.onWatchedButtonPressed(props.video)
}
}
if (props.unwatchedVideo) {
+"Mark as watched"
} else {
+"Mark as unwatched"
}
}
VideoPlayer
, .
unwatched
watched
, .
. ! :
external interface AppState : RState {
var currentVideo: Video?
var unwatchedVideos: List<Video>
var watchedVideos: List<Video>
}
init
. , App
:
override fun AppState.init() {
unwatchedVideos = listOf(
KotlinVideo(1, "Building and breaking things", "John Doe", "https://youtu.be/PsaFVLr8t4E"),
KotlinVideo(2, "The development process", "Jane Smith", "https://youtu.be/PsaFVLr8t4E"),
KotlinVideo(3, "The Web 7.0", "Matt Miller", "https://youtu.be/PsaFVLr8t4E")
)
watchedVideos = listOf(
KotlinVideo(4, "Mouseless development", "Tom Jerry", "https://youtu.be/PsaFVLr8t4E")
)
}
unwatchedVideos
watchedVideos
Main.kt
, Main.kt
(un
)watchedVideos
, IDE , state.
(un
)watchedVideos
.
, . :
videoPlayer {
video = currentVideo
unwatchedVideo = currentVideo in state.unwatchedVideos
onWatchedButtonPressed = {
if (video in state.unwatchedVideos) {
setState {
unwatchedVideos -= video
watchedVideos += video
}
} else {
setState {
watchedVideos -= video
unwatchedVideos += video
}
}
}
}
, , , .
, . , , , . !
. .
step-05-more-components
.
7. NPM
, . , , . , , .
– .
, . react-youtube-lite
. API README.
. react-youtube-lite
, Gradle. :
dependencies {
// ...
// Video Player ( 7)
implementation(npm("react-youtube-lite", "1.0.1"))
// ...
}
– NPM Gradle npm
. yarn
, Kotlin/JS Gradle , , .
NPM , : , . , IDE . . ReactYouTube.kt
:
@file:JsModule("react-youtube-lite")
@file:JsNonModule
import react.*
@JsName("ReactYouTubeLite")
external val reactPlayer: RClass<dynamic>
JavaScript – , , . – require("react-youtube-lite").default
JS. : " , , RClass<dynamic>
".
, , . dynamic
, . , , - (, ).
, , ( external
), README . – . , – . :
@file:JsModule("react-youtube-lite")
@file:JsNonModule
import react.*
@JsName("ReactYouTubeLite")
external val reactPlayer: RClass<ReactYouTubeProps>
external interface ReactYouTubeProps : RProps {
var url: String
}
VideoPlayer
! img
:
reactPlayer { attrs.url = props.video.videoUrl }
KotlinConf ( ). – . , , . , , react-share. Gradle:
dependencies {
// ...
// Share Buttons ( 7)
implementation(npm("react-share", "~4.2.1"))
// ...
}
. , , : , EmailShareButton
EmailIcon
. . ; ReactShare.kt
:
@file:JsModule("react-share")
@file:JsNonModule
import react.RClass
import react.RProps
@JsName("EmailIcon")
external val emailIcon: RClass<IconProps>
@JsName("EmailShareButton")
external val emailShareButton: RClass<ShareButtonProps>
@JsName("TelegramIcon")
external val telegramIcon: RClass<IconProps>
@JsName("TelegramShareButton")
external val telegramShareButton: RClass<ShareButtonProps>
external interface ShareButtonProps : RProps {
var url: String
}
external interface IconProps : RProps {
var size: Int
var round: Boolean
}
. reactPlayer
( styledDiv
, ):
styledDiv {
css {
display = Display.flex
marginBottom = 10.px
}
emailShareButton {
attrs.url = props.video.videoUrl
emailIcon {
attrs.size = 32
attrs.round = true
}
}
telegramShareButton {
attrs.url = props.video.videoUrl
telegramIcon {
attrs.size = 32
attrs.round = true
}
}
}
, . , . , , .

, .
step-06-packages-from-npm
.
8. REST API
, . , REST API.
API, https://my-json-server.typicode.com/kotlin-hands-on/kotlinconf-json/videos/1. API – videos
, . API . , , Video
( ;)
). , .
JS
. Kotlin/JS , . Fetch API, HTTP REST API.
JavaScript – . , , . , - . , , , . , :
window.fetch("https://url...").then {
it.json().then {
it.unsafeCast<Video>()
//...
}
}
. , .
(structured concurrency) – . , . . .
, Gradle :
dependencies {
//...
// Coroutines ( 8)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9")
}
, !
App.kt
, , REST API:
suspend fun fetchVideo(id: Int): Video {
val response = window
.fetch("https://my-json-server.typicode.com/kotlin-hands-on/kotlinconf-json/videos/$id")
.await()
.json()
.await()
return response as Video
}
. :
import kotlinx.browser.window
import kotlinx.coroutines.*
, suspend . fetch
, id
API. (await
), JSON, . external interface Video
. , IDE – JavaScript fetch
: , Video
. . : , @Suppress
, unsafeCast
(response.unsafeCast<Video>()
).
. window.fetch
json
. , . , (await
) . , , . await
, ( suspend
). , .
suspend
, , 25. fetchVideos
, 25 . , suspend – async
. :
suspend fun fetchVideos(): List<Video> = coroutineScope {
(1..25).map { id ->
async {
fetchVideo(id)
}
}.awaitAll()
}
. , init
App
:
override fun AppState.init() {
unwatchedVideos = listOf()
watchedVideos = listOf()
val mainScope = MainScope()
mainScope.launch {
val videos = fetchVideos()
setState {
unwatchedVideos = videos
}
}
}
, init
, setState
unwatchedVideos
. - , , , unwatchedVideos
. setState
, , .
:

. , "Hello, World" .
, , , , .
step-07-using-external-rest-api
.
9.
.
, Gradle build
- IntelliJ IDEA ./gradlew build
. , , , DCE (dead code elimination – ).
, build/distributions
. JS , HTML , . , , , HTTP , GitHub Pages .
Heroku
Heroku . ; , .
git init heroku create git add . git commit -m "initial commit"
JVM , Heroku (, Ktor Spring Boot), , . Heroku:
heroku buildpacks:set heroku/gradle heroku buildpacks:add https://github.com/heroku/heroku-buildpack-static.git
heroku/gradle
stage
Gradle . , build
, , :
// Heroku Deployment ( 9)
tasks.register("stage") {
dependsOn("build")
}
buildpack-static
, static.json
. root
:
{
"root": "build/distributions"
}
, , :
git add -A git commit -m "add stage task and static content root configuration" git push heroku master
master (, ,step*
), , master Heroku (, :git push heroku step-08-deploying-to-production:master
).
, , !

final
.
10. :
- , .
, , – state effect. , .
, . , . – this
. , , :
external interface WelcomeProps : RProps {
var name: String
}
val welcome = functionalComponent<WelcomeProps> { props ->
h1 {
+"Hello, ${props.name}"
}
}
, external interface
. . functionalComponent
render
.
: child
:
child(welcome) {
attrs.name = "Kotlin"
}
:
fun RBuilder.welcome(handler: WelcomeProps.() -> Unit) = child(welcome) {
attrs.handler()
}
4. welcome { name = "Kotlin" }
.
, - . .
State
, . :
val counter = functionalComponent<RProps> {
val (count, setCount) = useState(0)
button {
attrs.onClickFunction = { setCount(count + 1) }
+"$count"
}
}
, :
useState
0
–Int
. , , (useState<String?>(null)
).-
useState
, :
- (
count
Int
); - (
setCount
RSetState<Int> /* = (Int) -> Unit */
).
- (
- ,
setState
.
, count
, . , , , .
: useState
– -. , , :
val counter = functionalComponent<RProps> {
var count by useState(0)
button {
attrs.onClickFunction = { ++count }
+"$count"
}
}
Effect
, - – API WebSocket . , h3
:
val randomFact = functionalComponent<RProps> {
val (randomFact, setRandomFact) = useState<String?>(null)
useEffect(emptyList()) {
GlobalScope.launch {
val fortyTwoFact = window.fetch("http://numbersapi.com/42").await().text().await()
setRandomFact(fortyTwoFact)
}
}
h3 { +(randomFact ?: "Fetching...") }
}
, , , . useEffect
, setRandomFact
.
, useEffect
. – , – . , , useEffect
. API . .
, setRandomFact
, .
, - , , videoList
, . useState
, useEffect
API, 8.
, .
11. ?
, . , Kotlin/JS .
. , . , HTML , .
. , -. - , ( Ktor), , . - .
APIs
:
, , . CSS (grids) ( : ).
kotlin-wrappers JS , . ( ):
,
– YouTrack. , . Slack. , #javascript
#react
.
, !
, ! Kotlin/JS , JS, JSX – , , .
, , Kotlin DSL. JSX , Kotlin DSL , . , , , – . , , . Kotlin/JS !