# Веб приложение с Kotlin.js: Начало

Почему Kotlin.js? 

Почему стоит использовать Kotlin для веб разработки? Вот несколько причин: 
* Знакомство: Если вы пришли из компилируемого языка типа Java, Swift или C#, вы найдете Kotlin очень простым для изучения, и вы должны уже быть знакомы с ним, если вы разработчик для андроид. 
* Дружелюбные инструменты: IDE может помочь вам во многом с JavaScript, так как Kotlin идет с первоклассной поддержкой в IDE, значит вы можете находить, общие ошибки при наборе кода. 
* Распространение кода между платформами: С помощью Kotlin Multiplatform Project, вы можете написать бизнес логику вашего приложения один раз и распространять его на множество платформ включая бэкенд, браузер фротенд, Android и iOS клиенты. 
* Совместимость: Возможность вызывать JavaScript код из Kotlin кода очень важна. Она позволяет вам использовать уже готовый JavaScript код, который был уже написан на Kotlin. Эта совместимость имеет и обратное направление. Можно вызывать Kotlin код из JavaScript. 
* Проблемы JavaScript: Kotlin отличный выбор если отказались от веб разработки в связи с распространением JavaScript проблем, таких как необходимость работы с динамической типизацией, странным логическим сравнением или прототипированием. 
* Требования: для этой инструкции, вам необходимо базовое знание веб-программирования и знакомство с Kotlin и IntelliJ Idea. Если вы полностью новичок в Kotlin, возможно вы захотите почитать книгу "Kotlin Apprentice" или видео курс "Программирования на Kotlin" 

# Начнем. 

Вы собираетесь создать приложение книжный магазин. Приложение будет получать данные о книгах с вебсервиса, отображать обложки книг с названием, описанием, ценой и кнопкой открыть страничку с деталями. Вот как будет выглядеть конечный результат: 

![](https://notepad.gasick.ru/uploads/images/gallery/2020-06/scaled-1680-/image-1593118305977.png) 

Чтобы следовать данному руководству, вам необходимо использовать IntelliJ IDEA 2018.8.7 или позднее (бесплатная Community Edition будет достаточно), и Kotlin 1.2.61 или более поздний. 

Начинаем с загрузки материала для этого руководства используя кнопку "Download Materials" в верхней или нижней части окна программы. Затем запускаем IntelliJ IDEA  и создаем новый проект. 

В окне нового проекта, выберите Kotlin в левой части панели и  Kotlin/JS в правой части. Затем нажмите "Next" 

Новый проект, шаг 1 

Для имени проекта используйте `bookstore`. Выберите место расположения проекта, или используйте по умолчанию. Затем нажмите "Finish" 

Новый проект, шаг 2 

Распакуйте скаченные материалы, и из начальной папки, скопируйте **index.html**  и **styles.css** в вашу папку с проектом. Структура проекта должна выглядеть следующим образом: 

![](https://notepad.gasick.ru/uploads/images/gallery/2020-06/scaled-1680-/image-1593118817100.png) 

# Создание главной функции 

Создайте новый Kotlin файл с именем **Main.kt** в папке src и добавьте в нее `main()` функцию как показано ниже: 

```kotlin
fun main(args: Array<String>) { 
  println("Hello world!") 
} 
``` 
*Заметка*: Функция приложения `main()` может быть в любом Kotlin файле с любым именем, поэтому использование **Main.kt** не строго обязательно, но вы должны иметь только одну функцию `main()` в вашем проекте. 

Теперь откроем "Build" меню и нажмем "Build Project" чтобы скомпилировать ваш Kotlin код в JavaScript. IDE сгенерирует следующую папку в корне вашего проекта. 

Нам нужно упомянуть 2 выходных файла в данном проекте в **index.html**: 
* kotlin.js: это стандартная Kotlin библиотека, реализованная в JavaScript 
* bookstore.js: Это JavaScript, в который был скомпилирован Kotlin. 
*Заметка*: Оба файла уже ссылаются для вас внутри скриптами на начальный **index.html** файл, но вам возможно нужно проверить их пути на случай если вы ввели другое название проекта. 

Теперь откроем **index.html** в вашем браузере, и затем откроем консоль разработчика. Вы должны увидеть "Hello World!",как показано на скриншоте: 

![](https://notepad.gasick.ru/uploads/images/gallery/2020-06/scaled-1680-/image-1593285839826.png) 

*Заметка*: Каждый раз меняя код kotlin, необходимо билдить ваш проект и затем обновлять HTML страницу чтобы увидеть изменения. 

# Вызываем JavaScript код из Kotlin 

В **index.html** файле, внутри тега со скриптом, вы найдете JavaScript функцию названную `getApiUrl()`, который возвращает URL необходимый вам для получения данных магазина в формате JSON
  
```kotlin 
<script> 
function getApiUrl(){ 
  return "https://gist.githubusercontent.com/tarek360/4578e33621011e18829bad0c8d1c8cdf/raw/06d185bebc3e14a56dfa85f53288daddd4ff6a2b/books.json"; 
} 
</script> 
```   

Есть множество путей для доступа JavaScript функции или переменно из кода Kotlin. Один из них это использование функции `js()`, которая позволяет передавать простой код JavaScript как строку. 

Добавим следуюущую строку кода в **Main.kt** файл, вне  `main()` функции. 
```kotlin 
val API_URL = js("getApiUrl()") as String 
``` 
Здесь вы передаете строку `getApiUrl()` в  `js()` функцию. Теперь `getApiUrl()` функция всегда возвращает строку, вы можете привести её безопасно к String Kotlin, и хранить как обычное значение. 

Теперь, обновим `main()` функцию для выведения значения `API_URL` свойства вместо "Hello world!" 

```kotlin 
fun main(args: Array<String>) { 
  println(API_URL) 
} 
```
Билдим проект и обновляем index.html в браузере. Вы дожны увидеть значение `API_URL` переменной, выведенной в консоль, как на скриншоте ниже 

![](https://notepad.gasick.ru/uploads/images/gallery/2020-06/scaled-1680-/image-1593286796376.png) 

Теперь у вас есть URL сохраненный в `API_URL` значении, который вы используете позднее. 

Очистим `main()` функцию, чтобы подготовить её к тому, что будет происходить дальше. 

![](https://notepad.gasick.ru/uploads/images/gallery/2020-06/scaled-1680-/image-1593286886581.png) 

Чтобы получить данные от сервера их отобразить их в UI, необходимо создать новый Kotlin класс для отображения одной книги. Создадим файл **Book.kt**  в **src** папке и поместит тут класс данных: 

```kotlin 
data class Book(val title: String, 
                val price: String, 
                val description: String, 
                val url: String, 
                val coverUrl: String) 
``` 
Каждая книга имеет заголовок, цену, описание, URL для страницы с деталями на сайте и ссылку на картинку обложки. 

# Создаем приложение 

Вы будете использовать простую архитектуру MVP в приложении. Класс отображению будет содержать всю бизнес логику, пока класс страницы работает как вью. Перед созаднием этих классов, создадим контракт между ними. 

*Заметка*: Если вы не имеет опыта работы с MVP, то посмотрите учебное пособие `Getting Started with MVP on Android`. 

Создадим новый Kotlin интерфейс, названный `BookStoreContract` (как обычно, в его собственном файле в папке `src`) который определяет подключение между вью и представителем. Добавим следующий код: 

```kotlin 
interface BookStoreContract { 
  interface View { 
    fun showBooks(books: List<Book>) // 1 
    fun showLoader() // 2 
    fun hideLoader() // 3 
  } 
  
  interface Presenter { 
    fun attach(view: View) // 4 
    fun loadBooks() // 5 
  } 
} 
``` 

Эта вью сможет: 
* Отображать список книг, предоставленных ей 
* Показывать индикатор загрузки пока приложение получает данные от сервера 
* Прятать индикатор загрузки. 

Что касается от представителя, он может: 
* Отображать результаты на любой вью которая предоставлена 
* Начинать загрузку данных о книгах из источника данных, в этом случае, это удаленный сервер. 
  
  
Выполнив это, вы теперь можете создать класс `BookStorePage`, и добавить в нее такой код: 

```kotlin  
class BookStorePage(private val presenter: BookStoreContract.Presenter) : BookStoreContract.View { 
  override fun showBooks(books: List<Book>) { 
  } 
  override fun showLoader() { 
  } 
  override fun hideLoader() { 
  } 
} 
``` 

Этот класс имеет конструктор с `BookStoreContract.Presenter` параметром. Он реализует `BookStoreContract.View` интерфейс с тремя необходимыми методами (пока пустыми) 

Создадим `BookStorePresenter` класс и добавим следующий код:   

```kotlin 
// 1 
class BookStorePresenter : BookStoreContract.Presenter { 
  // 2 
  private lateinit var view: BookStoreContract.View 
  // 3 
  override fun attach(view: BookStoreContract.View) { 
    this.view = view 
  } 
  // 4 
  override fun loadBooks() { 
  } 
} 
``` 

В этом классе, вы: 
* Реализовали  `BookStoreContract.Presenter` интерфейс. 
* Добавили свойство для хранения сылки на вью. 
* Реализовали метод `attach()` из `BookStoreContract.Presenter` интерфейса, и проинициализировать вью свойство из полученного параметра. 
* Реализовали `loadBooks()` метод требуемый `BookStoreContract.Presenter` интерфейсом(пока пустым). 

# Получение данных с сервера 

Вам нужен пут для получения данных с сервера. Чтоыб это сделать, добавьте следующий метод в `BookStorePresenter` класс. 

```kotlin 
// 1 
private fun getAsync(url: String, callback: (String) -> Unit) { 
  // 2 
  val xmlHttp = XMLHttpRequest() 
  // 3 
  xmlHttp.open("GET", url) 
  // 4 
  xmlHttp.onload = { 
    // 5 
    if (xmlHttp.readyState == 4.toShort() && xmlHttp.status == 200.toShort()) { 
      // 6 
      callback.invoke(xmlHttp.responseText)  
    } 
  } 
  // 7 
  xmlHttp.send() 
} 
``` 
Нажмите *option+return* на Mac или *Alt+Enter* на PC, чтобы добавить в импорт класс XMLHttpRequest. 

Пройдем по тому, что мы делаем, шаг за шагом. 
 
* Создание метода который делает сетевые запросы. Он берет URL  для получения, так же как и функция со `String` параметром, которая передать результат сетевого вызова. 
* Создаем объект `XMLHttpRequest`. `
* Указываем этот запрос посылает `HTTP GET` запрос на заданный URL. 
* Указываем `callback` который будет выполнен, когда запрос завершится.
* Проверяем если запрос имеет состояние выполнен, и если он имеет статус код 200. 
* Вызываем `callback` функцию, принимаемую как параметр, и передаем её содержание сетевого ответа как строки. 
* Вызываем `send()` чтобы произвести HTTP запрос настроенный ранее. 

Теперь, вы можете использовать вспомогательный метод для реализации `loadBooks()`: 

```kotlin 
override fun loadBooks() { 
  //1 
  view.showLoader() 
  //2 
  getAsync(API_URL) { response -> 
    //3 
    val books = JSON.parse<Array<Book>>(response) 
    //4 
    view.hideLoader() 
    //5 
    view.showBooks(books.toList()) 
  } 
} 
``` 

В этой части кода: 
* Просим вью показать загрузочный индикатор прежде, чем начать загружать данные. 
* Делаем асинхронный запрос чтобы получить данные о книгах 
* Парсим JSON ответ полученный как массив объектов данных клласа `Book`  
* Просим вью спрятать индикатор загрузки, так как вы закончили загрузку и парсинг 
* Просим вью отобразить список книг 

  

Вы можете пройтись по массиву книг и распечатать заголовок каждый книги в консоль, чтобы быть уверенным, что все работает правильно. Чтобы это сделать добавим следующие линии кода после того, как книги будут распарсены: 

  
```kotlin 
books.forEach { book -> 
  println(book.title) 
} 
``` 

Чтобы протестировать код представителя, обновим `main()` функцию для чтения: 

```kotlin 
fun main(args: Array<String>) { 
  val bookStorePresenter = BookStorePresenter() 
  val bookStorePage = BookStorePage(bookStorePresenter) 
  bookStorePresenter.attach(bookStorePage) 
  bookStorePresenter.loadBooks() 
}   
``` 

Тут мы создаем новый объект `BookStorePresenter`, и затем объект `BookStorePage`, переданные странице от объекта представителя через его конструктор. Теперь вы добавляете страницу в представителя и вызывается `loadBooks()` непосредственно на представителе. 

Сбилдим и запустим проект и обновим **index.html**. Вы должны увидеть лог как на картинке ниже: 

![](https://notepad.gasick.ru/uploads/images/gallery/2020-06/scaled-1680-/image-1593332490662.png) 

После выполнения этого тестирования, уберите цикл `forEach` с выражением `print` внутри `loadBooks()` 

*Заметка*: Если вы пытаетесь печатать саму книгу(`println(book)`), это нормально видеть только объект `Object` повторяющийся  снова и снова в выходе. Это потому что вызов `JSON.parce` создает чистый JavaScript объект вместо вызова конструктора класса Book Kotlin. 

Это значит что вы сможете читать его свойства, но любой метод ожидающий класс будет пропущен - включая  автогерированную реализацию `toSting()`. Если вам необходим более надежный метод анализа решения который сделает это правильно, вы можете изучить подробнее `kotlinx.serialization` библиотеку. 

# Создание UI 

Файл **index.html**  содержит два `div` тега с `ID`, названный `loader`  и `content`. Для начала загрузочный индикатор, который вы можете показывать пока ваше приложение загружает данные, и прячет, когда эта загрузка выполнена. Затем контейнер, в который будет добавлены все карточки книг. 

Для доступа в **DOM** элементы в вашем Kotlin коде, добавьте следующие два новых свойства в класс `BookStorePage` как показано ниже.   

```kotlin 
private val loader = document.getElementById("loader") as HTMLDivElement 
private val content = document.getElementById("content") as HTMLDivElement 
``` 

Вы всегда можете получить элемент в **DOM** по его `ID`, используя объект документа и `getElementeById()` метод, так же как это делается в JavaScript. 

Метод `getElementById()` возвращает общий `Element`, который вы можете привести к другому типу элемента если нужно.(похоже на то на как метод `findViewById()` работает в Android). 

# Меняем видимость загрузчика 

Обновим методы `showLoader()` и `hideLoader()` в `BookStorePage` следующий образом. 

```kotlin 
override fun showLoader() { 
  loader.style.visibility = "visible" 
} 
override fun hideLoader() { 
  loader.style.visibility = "hidden" 
} 
``` 

Снова, вы используете обычную **DOM** модель для изменения визуальныйх свойств элементов `visible` и `hidden`, как и трубется. 

Элемент загрузчик, видимый по умолчанию, поэтому вы должны видеть его, когда открываете страницу index.html. 

![](https://koenig-media.raywenderlich.com/uploads/2018/12/loading-1.gif) 

Проверьте ваш код и спрячьте загрузчик изменим следующим образом функцию `main()`: 

```kotlin 
fun main(args: Array<String>) { 
  val bookStorePresenter = BookStorePresenter() 
  val bookStorePage = BookStorePage(bookStorePresenter) 
  bookStorePage.hideLoader() 
} 
``` 

Вы обновили `main` функцию вызывать `hideLoader()` напрямую чтобы прятать индикатор который был до этого видим. 

Сбилдим проект и обновим **index.html** в вашем браузере. Загрузчик должен теперь исчезнуть. 

# Создание элементов книги. 

Теперь, вы создадите карточку отображения каждой книги, как показано ниже 

![](https://koenig-media.raywenderlich.com/uploads/2018/12/book_card-222x320.png) 

Создайте новый класс и назовите его `CardBuilder`. В этом классе вы создадите `HTMLElement` для представления книги, свяжите детали книги с ним, и примените CSS. Начните с класса как показано ниже: 
  
```kotlin 
class CardBuilder { 
  
  fun build(book: Book): HTMLElement { 
    // 1 
    val containerElement = document.createElement("div") as HTMLDivElement 
    val imageElement = document.createElement("img") as HTMLImageElement 
    val titleElement = document.createElement("div") as HTMLDivElement 
    val priceElement = document.createElement("div") as HTMLDivElement 
    val descriptionElement = document.createElement("div") as HTMLDivElement 
    val viewDetailsButtonElement = document.createElement("button") as HTMLButtonElement 
  
    // 2 
    bind(book = book, 
        imageElement = imageElement, 
        titleElement = titleElement, 
        priceElement = priceElement, 
        descriptionElement = descriptionElement, 
        viewDetailsButtonElement = viewDetailsButtonElement) 
  
    // 3 
    applyStyle(containerElement, 
        imageElement = imageElement, 
        titleElement = titleElement, 
        priceElement = priceElement, 
        descriptionElement = descriptionElement, 
        viewDetailsButtonElement = viewDetailsButtonElement) 
  
    // 4 
    containerElement 
        .appendChild( 
            imageElement, 
            titleElement, 
            descriptionElement, 
            priceElement, 
            viewDetailsButtonElement 
        ) 
    // 5     
    return containerElement 
  } 
  
  // 6 
  private fun Element.appendChild(vararg elements: Element) { 
    elements.forEach { 
      this.appendChild(it) 
    } 
  } 
} 
``` 

Нужно много чего еще сделать, давайте посмотрим на шаги по очереди: 

* Создать новые элементы используя `createElement()` API барузера, передавая имя HTML тэга для созадния. Для примера, используейте `div` для создания `HTMLDivElement()` и `img` чтобы создать `HTMLImageElement`. 
* Свяжите класс данных `book` c созданным `HTML` элементом. Вы скоро реализуете этот метод `bind()`  
* Примените некоторые CSS классы к `HTML` элементам. Так же реализуете `applyStyle()` метод ниже. 
* Добавите все отдельные `HTML` элементы в один контейнер. 
* Вернете контейнер, который является корневым элементов карточек. 
* Напишите расширение функции которая позволяет вам добавлять переменное количество дочерних к элементы, вместо вызова обычного `appendChild()` метода множество раз. 

# Создание данных 

Заполните элементы данными, добавьте следующий код. 

Заполнените элементы данными, добавьте следующие метод в класс `CardBuilder`: 

```kotlin 
private fun bind(book: Book, 
                 imageElement: HTMLImageElement, 
                 titleElement: HTMLDivElement, 
                 priceElement: HTMLDivElement, 
                 descriptionElement: HTMLDivElement, 
                 viewDetailsButtonElement: HTMLButtonElement) { 
  // 1 
  imageElement.src = book.coverUrl  
  // 2 
  titleElement.innerHTML = book.title 
  priceElement.innerHTML = book.price 
  descriptionElement.innerHTML = book.description 
  viewDetailsButtonElement.innerHTML = "view details" 
   
  // 3 
  viewDetailsButtonElement.addEventListener("click", { 
    window.open(book.url) 
  }) 
} 
``` 

В этом методе, вы: 
* Укажите ссылку на обложку книги как источник элемента картинки в карточке. 
* Укажите текстовое содержание для различных текстовых элементов. 
* Добавьте слушателя события `click` к элементу кнопка, которая будет вести к URL книги если нажата кнопка. 

# Применение CSS 

До сих пор отсутствует метод `applyStyle()`, который вы должны так же добавить в класс `CardBuilder`. 

```kotlin 
private fun applyStyle(containerElement: HTMLDivElement, 
                       imageElement: HTMLImageElement, 
                       titleElement: HTMLDivElement, 
                       priceElement: HTMLDivElement, 
                       descriptionElement: HTMLDivElement, 
                       viewDetailsButtonElement: HTMLButtonElement) { 
  containerElement.addClass("card", "card-shadow") 
  imageElement.addClass("cover-image") 
  titleElement.addClass("text-title", "float-left") 
  descriptionElement.addClass("text-description", "float-left") 
  priceElement.addClass("text-price", "float-left") 
  viewDetailsButtonElement.addClass("view-details", "ripple", "float-right") 
} 
``` 

Этот метода добавляет верные CSS классы, который вам нужны для стилизации карточек книг с помощью "material design". Вы можете найти эти классы уже настроенными в style.css. Для примера, тень карточки CSS класса дает "material" тень карточке контейнера, и `float-left` css класс выравнивает элементы по левой стороне. 

# Создание карточки. 

Вернемся назад в  `BookStorePage` класс и начнем использовать код создания этой карточки. Первый, добавим свойство в класс, который будет хранить объект `CardBuilder` 

```kotlin 
private val cardBuilder = CardBuilder() 
``` 

Теперь идем в `showBooks()` метод и добавялем следующий код: 

```kotlin 
books.forEach { book -> 
  val card = cardBuilder.build(book) 
  content.appendChild(card) 
} 
``` 

Этот код проходит через список книг, и для каждой книги, созадет `HTML` элемент, отображающий её. Теперь, это добавляет элемент к содержанию `div` который мы наблюдали из *DOM* ранее.  

# Отображение Страницы книжного магазина  

Вы почти закончили. Добавим следующий метод в класс `BookStorePage`. 

```kotlin 
fun show() { 
  presenter.attach(this) 
  presenter.loadBooks() 
} 
``` 

Этот код указывает текущий `BookStorePage` экземпляр как представитель вью, который может получать `callback` от него. И затем просит представителя начать загрузку книг. 

Идите в `main()` функцию и обновите, чтобы она вызывала `show()` метод в `bookStorePage`. Полный метод `main()` должен теперь выглядеть таким образом: 

```kotlin 
fun main(args: Array<String>) { 
  val bookStorePresenter = BookStorePresenter() 
  val bookStorePage = BookStorePage(bookStorePresenter) 
  bookStorePage.show() 
} 
``` 

Сбилдим проект, и обновим **index.html**. 

Вы должны увидеть загрузчик мельком, прежде чем приложение закончит загрузку книги. Затем отобразятся книжные каточки. Карточки должны иметь теперь, когда вы будете наводить на нее мышку, и кнопка "View Details" должна вас вести на нужную страницу книги. 

![](https://koenig-media.raywenderlich.com/uploads/2018/12/end_product-1.png) 

Ура! Вы воздали ваше первое web приложение на Kotlin :] 

Счастливое лицо!