Pull to refresh

Практика по Котлину: Создание веб приложений на React и Kotlin/JS

Reading time38 min
Views27K
Original author: JetBrains – Kotlin Team

От переводчика.


Привет! Про Kotlin есть стереотип, будто бы это язык для разработки только под Android. На самом деле, это совсем не так: язык официально поддерживает несколько платформ (JVM, JS, Native), а также умеет работать с библиотеками для этих платформ, написанных на других языках. Такая поддержка "мультиплатформенности" позволяет не только писать всевозможные проекты на одном языке в единой форме, но и переиспользовать код при написании одного проекта под разные платформы.


В этой статье я перевожу официальный туториал Kotlin Hands-On о создании веб сайтов на Котлине. Мы рассмотрим многие аспекты программирования на Kotlin/JS и поймем, как работать не только с чистым DOM. В основном будем говорить о React JS, но также коснемся системы сборки Gradle, использования зависимостей из NPM, обращения к REST API, деплоя на Heroku, и в итоге сделаем приложение-видеоплеер.


Текст ориентирован на тех, кто немного знает Котлин и не знает или почти не знает Реакт. Если вы более опытны по этим вопросам, то части туториала могут показаться вам чрезмерно разжеванными.


kotlin-react


Надеюсь, статья удовлетворит пусть даже непопулярные запросы на материалы о Котлине на русском.


Предлагать правки в эту статью лучше всего на ГитХабе. Текущий перевод построен на версии оригинального туториала, актуальной на 09.04.2021.


Содержание


  1. Введение
  2. Настройка
  3. Первая страница на Реакте – статичная
  4. React – о реакциях. Наш первый компонент
  5. Работаем совместно. Композиция компонентов
  6. Больше компонентов!
  7. Использование NPM пакетов
  8. Используем внешнее REST API
  9. Деплоим в продакшен и в облако
  10. В дополнение: современный Реакт с хуками
  11. Что дальше?

Шаг 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 (см. скриншот ниже).


Результат


Исходный код финального приложения, как и промежуточные результаты, можно найти в репозитории на 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:
Задача для запуска сервера


Если хочется запускать не из 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!"
        }
    }
}

После сборки изменившегося проекта в браузере можно увидеть магию:
hello-world


Ура, вы только что написали свой первый веб сайт на чистом Котлине с Реактом! Давайте попробуем понять, как работает этот код. Функция 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 – о реакциях. Наш первый компонент


Основная идея


Базовые строительные блоки в Реакте называются компонентами. Комбинируя компоненты, часть из которых в свою может быть комбинацией других более маленьких, мы создаем приложение. Делая компоненты переиспользуемыми и обобщенными, мы можем помещать их в несколько мест в приложении, не дублируя код и/или логику.


На самом деле, корневой элемент нашего рендеринга тоже можно представить как компонент. Если мы отметим его рамкой, то это будет выглядеть примерно так:
root-component


А если посмотреть на структуру приложения, то можно найти следующие компоненты, каждый из которых имеет свою ответственность:
split-components


Корневой компонент


Давайте разобьем приложение на компоненты в соответствии с его структурой. Начнем с явного объявления главного компонента 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

Теперь при клике на элементе списка в браузере мы увидим всплывающее сообщение о выбранном элементе:
alert


Оформлять значение 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. Работаем совместно. Композиция компонентов


Сделанные нами на предыдущем шаге пара списков сами по себе вполне работают. Однако, если мы кликнем по одному видео в каждом из списков, мы можем выбрать два видео одновременно. Это неправильно, ведь у нас только один плеер :)
pair


По-хорошему, у обоих списков должно быть единое состояние – выбранное видео, которое будет одним на все приложение. Но единое состояние не может (и не должно) храниться в разных компонентах. Принято выносить состояние наверх (как еще говорят, "поднимать" состояние).


Вынос состояния наверх


Чтобы не прибивать гвоздями разные компоненты друг к другу и не создавать спагетти-код, можно воспользоваться иерархией компонентов Реакта: передавать атрибуты из родительского компонента. Если компонент хочет изменять состояние соседнего компонента, это следует делать через общего родителя. Значит, состояние должно быть не в соседнем компоненте, а именно в родителе. Миграция состояния из компонента к родителю называется выносом состояния. Давайте выносить его в нашем случае! Для этого нам нужно добавить состояние для нашего родительского компонента, 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
        }
    }
}

Теперь можно проверить, что кнопки действительно работают. Если кликнуть по ним, должно открыться окно. Если же ничего не происходит, возможно, у вас включен скрывающий такие кнопки блокировщик рекламы.
pair


Если хотите, можете написать декларации для других кнопок в этой библиотеке и тоже добавить их на экран.


Состояние проекта после выполнения этого шага доступно в ветке 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, мы подскажем рендереру Реакта, что может быть необходима перерисовка.


По возвращении в браузер мы наконец должны увидеть настоящий список видео:
real data


На этом часть по разработке в этой практике подошла к концу. Мы прошли долгий путь, от начальной производной "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 и сообщества вокруг него!

Only registered users can participate in poll. Log in, please.
Как вам идея таких переводов в целом и в частности?
75% Переводы английских туториалов на русский нужны; этот перевод неплохой, продолжай писать подобное.30
2.5% Переводы туториалов нужны, но качество этого перевода низкое.1
17.5% Переводы туториалов не нужны: лучше сразу в оригинале читать. Но этот перевод хороший.7
2.5% Переводы туториалов не нужны, а переводчик этого туториала зря потратил время.1
2.5% Другое мнение (в комментариях/в личке/при себе).1
40 users voted. 8 users abstained.
Tags:
Hubs:
Total votes 8: ↑7 and ↓1+6
Comments7

Articles