Latihan di Kotlin: Membuat aplikasi web di React dan Kotlin / JS

Dari penerjemah .







Hei! Ada stereotip tentang Kotlin bahwa itu adalah bahasa pengembangan yang hanya untuk Android. Faktanya, ini sama sekali tidak terjadi: bahasa ini secara resmi mendukung beberapa platform ( JVM , JS , Native ), dan juga tahu cara bekerja dengan pustaka untuk platform ini yang ditulis dalam bahasa lain. Dukungan semacam itu untuk "multiplatformity" memungkinkan tidak hanya untuk menulis semua jenis proyek dalam satu bahasa dalam satu bentuk, tetapi juga untuk menggunakan kembali kode saat menulis satu proyek untuk platform yang berbeda.







Pada artikel ini, saya menerjemahkan tutorial resmi Kotlin Hands-On tentang membangun situs web di Kotlin. Kami akan membahas banyak aspek pemrograman Kotlin / JS dan memahami cara bekerja dengan lebih dari sekadar DOM murni. Kami terutama akan berbicara tentang React JS , tetapi kami juga akan menyentuh sistem build Gradle , menggunakan dependensi dari NPM , memanggil REST API , menerapkan ke Heroku , dan akhirnya membuat aplikasi pemutar video .







Teks ini ditujukan bagi mereka yang tahu sedikit tentang Kotlin dan tidak tahu atau hampir tidak tahu React. Jika Anda lebih berpengalaman dalam masalah ini, maka bagian dari tutorial mungkin tampak terlalu sulit bagi Anda.







kotlin-bereaksi







, .







. , 09.04.2021.









  1. React – .
  2. .
  3. !
  4. NPM
  5. REST API
  6. :
  7. ?


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 (. ).







Hasil







, , GitHub. , .







, .







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



:

Tugas mulai server







IDE, , ./gradlew run



( Windows Gradle -: .\gradlew.bat run



).







, , , :

Halaman merah







(hot reload) a.k.a.



, – Kotlin/JS . run



Gradle.







, ( IDE – Stop; – Ctrl+C



).







IDEA, . IDEA , Gradle , :

Membuka edit







Run/Debug Configurations --continuous



:

Menambahkan argumen







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!"
        }
    }
}
      
      





:

Halo Dunia







, ! , . 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



. , :

kotlinconf-placeholder









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 – .





. , , . , , / .







, . , :

komponen root







, , :

komponen terpisah









. 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}"
            }
        }
    }
}
      
      





( , ), key



. , , – ! , , .







, 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
      
      





:

waspada







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. .



. , , . , :)

pasangan







-, – , . ( ) . ( , "" ).









-, : . , . , , . . ! , 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
        }
    }
}
      
      





, . , . , , .

pasangan







, .







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()
}
      
      





coroutineScope



. 25 , , .







, .







. , 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



, , .







:

data nyata







. , "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 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



).

, , !

heroku







final



.


10. :



- , .







React 16.8 . . : !







, , – 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



    , :

    1. ( count



      Int



      );
    2. ( setCount



      RSetState<Int> /* = (Int) -> Unit */



      ).
  • , setState



    .


, count



, . , , , .







State .







: 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



, .







Effect, "" , .









, - , , videoList



, . useState



, useEffect



API, 8.







, .







11. ?





, . , Kotlin/JS .









. , . , HTML , .









. , -. - , ( Ktor), , . - .







APIs



APIs, . ? ( : )? , , !







:



, , . CSS (grids) ( : ).









kotlin-wrappers JS , . ( ):









,



YouTrack. , . Slack. , #javascript



#react



.









- , . , .









. , , .







, !









, ! Kotlin/JS , JS, JSX – , , .







, , Kotlin DSL. JSX , Kotlin DSL , . , , , – . , , . Kotlin/JS !








All Articles