Веб приложение с 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()
функцию, чтобы подготовить её к тому, что будет происходить дальше.
Чтобы получить данные от сервера их отобразить их в 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 ранее.
Отображение Страницы книжного магазина
Вы почти закончили. Добавим следующий метод в класс 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 :]
Счастливое лицо!