Если вы видите что-то необычное, просто сообщите мне. Skip to main content

Веб приложение с 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"

Начнем.

Вы собираетесь создать приложение книжный магазин. Приложение будет получать данные о книгах с вебсервиса, отображать обложки книг с названем, описанием, ценой и кнопкой открыть страничку с деталями. Вот как будет выглдяеть конечный результат:

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

Создание главной функции

Создайте новый Kotlin файл с именем Main.kt в папке src и добавьте в нее main() функцию как показано ниже:

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!",как показано на скриншоте:

Заметка: Каждый раз меняя код kotlin, необходимо билдить ваш проект и затем обновлять HTML страницу чтобы увидеть изменения.

Вызываем JavaScript код из Kotlin

В index.html файле, внутри тега со скриптом, вы найдете JavaScript функцию названную getApiUrl(), который возвращает URL необходимый вам для получения данных магазина в формате JSON

<script>
function getApiUrl(){
  return "https://gist.githubusercontent.com/tarek360/4578e33621011e18829bad0c8d1c8cdf/raw/06d185bebc3e14a56dfa85f53288daddd4ff6a2b/books.json";
}
</script>

Есть множество путей для доступа JavaScript функции или переменно из кода Kotlin. Один из них это использование функции js(), которая позволяет передавать простой код JavaScript как строку.

Добавим следуюущую строку кода в Main.kt файл, вне main() функции.

val API_URL = js("getApiUrl()") as String

Здесь вы передаете строку getApiUrl() в js() функцию. Теперь getApiUrl() функция всегда возвращает строку, вы можете привести её безопасно к String Kotlin, и хранить как обычное значение.

Теперь, обновим main() функцию для выведения значения API_URL свойства вместо "Hello world!"

fun main(args: Array<String>) {
  println(API_URL)
}

Билдим проект и обновляем index.html в браузере. Вы дожны увидеть значение API_URL переменной выведенной в консоль, как на скриншоте ниже

Теперь у вас есть URL сохраненный в API_URL значении, который вы используете позднее.

Очистим main() функцию, чтобы подготовить её к тому, что будет происходить дальше.

Representing Books

Чтобы получить данные от сервера их отобразить их в UI, необходимо создать новый Kotlin класс для отображения одной книги. Созадим файл Book.kt в src папке и поместит тут класс данных:

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) который определяет подключение между вью и представителем. Добавим следующий код:

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, и добавить в нее такой код:


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 класс и добавим следующий код:

// 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 класс.

// 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():

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
  • Просим вью спрятать индикатор загрузки, так как вы закончили загрузку и парсинг
  • Просим вью отобразить список книг

Вы можете пройтись по массиву книг и распечатать заголовок каждоый книги в консоль, чтобы быть уверенным, что все работает правильно. Чтобы это сделать добавим следующие линии кода после того как книги будут распарсены:

books.forEach { book ->
  println(book.title)
}

Чтобы протетировать код представителя, обновим main() функцию для чтения:

fun main(args: Array<String>) {
  val bookStorePresenter = BookStorePresenter()
  val bookStorePage = BookStorePage(bookStorePresenter)
  bookStorePresenter.attach(bookStorePage)
  bookStorePresenter.loadBooks()
}  

Тут мы создаем новый объект BookStorePresenter, и затем объект BookStorePage, переданные странице от объекта представителя через его конструктор. Теперь вы добавляете страницу в представителя и вызывается loadBooks() непосредственно на представителе.

Сбилдим и запустим проект и обновим index.html. Вы должны увидеть лог как на картинке ниже:

После выполнения этого тестирования, уберите цикл forEach с выражением print внутри loadBooks()

Заметка: Если вы пытаетесь печтать саму книгу(println(book)), это нормально видеть только объект Object повторяющийся снова и снова в выходе. Это потому что вызов JSON.parce создает чистый JavaScript объект вместо вызова конструктора класса Book Kotlin.

Это значит что вы сможете читать его свйоства, но любой метод ожидающий класс будет пропущен - включая автогерированную реализацию toSting(). Если вам необходим более надежный метод анализа решения который сделает это правильно, вы можете изучить подробнее kotlinx.serialization библиотеку.

Создание UI

Файл index.html содержит два div тага с ID, названный loader и content. Для начала загрузочный индикаторый который вы можете показывать пока ваше приложение загружает данные, и прячет когда эта загрузка выполнени. Затем контейнер в который будет добавлены все карточки книг.

Для доступа в DOM элементы в вашем Kotlin коде, добавьте следующие два новых свойства в класс BookStorePage как показано ниже.

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 следующий образом.

override fun showLoader() {
  loader.style.visibility = "visible"
}
override fun hideLoader() {
  loader.style.visibility = "hidden"
}

Снова, вы используете обычную DOM модель для изменения визуальныйх свойств элементов visible и hidden, как и трубется.

Элемент загрузчик видимый по умолчанию, поэтому вы должны видеть его когда открываете страницу index.html.

Проверьте ваш код и спрячьте загрузчик изменим следующим образом функцию main():

fun main(args: Array<String>) {
  val bookStorePresenter = BookStorePresenter()
  val bookStorePage = BookStorePage(bookStorePresenter)
  bookStorePage.hideLoader()
}

Вы обновили main функцию вызывать hideLoader() напрямую чтобы прятать индикатор который был до этого видим.

Сбилдим проект и обновим index.html в вашем браузере. Загрузчик должен теперь исчезнуть.

Создание элементов книги.

Теперь, вы создадие карточку отображения каждой книги, как показано ниже

Создайте новый класс и назовите его CardBuilder. В этом классе вы создадите HTMLElement для представления книги, свяжите детали книги с ним, и примените CSS. Нанчите с класса как показано ниже:

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:

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.

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

private val cardBuilder = CardBuilder()

Теперь идем в showBooks() метод и добавялем следующий код:

books.forEach { book ->
  val card = cardBuilder.build(book)
  content.appendChild(card)
}

Этот код проходит через список книг, и для каждой книги, созадет HTML элемент отображающий её. Теперь, это добавляет элемент к содержанию div который мы наблюдали из DOM ранее. This code iterates through the list of books, and for each book, builds an HTML element representing it. Then, it adds the element to the content div we looked up from the DOM earlier.

Отображение Страницы книжного магазина

Вы почти закончили. Добавим следующий метод в класс BookStorePage.

fun show() {
  presenter.attach(this)
  presenter.loadBooks()
}

Этот код указывает текущий BookStorePage экземпляр как представитель вью, который может получать callback от него. И заптем просит представителя начать загрузку книг.

Идите в main() функцию и обновите, чтобы она вызывала show() метод в bookStorePage. Полный метод main() должен теперь выглядеть таким образом:

fun main(args: Array<String>) {
  val bookStorePresenter = BookStorePresenter()
  val bookStorePage = BookStorePage(bookStorePresenter)
  bookStorePage.show()
}

Сбилдим проект, и обновим index.html.

Вы должны увидеть загрузчик мельком прежде чем приложение закончит загрузку книги. Затем отобразятся книжные каточки. Карточки должны иметь теперь когда вы будете наводить на нее мышку, и кнопка "View Details" должна вас вести на нужную страницу книги.

Ура! Вы воздали ваше первое web приложение на Kotlin :]

Счастливое лицо!