Веб приложение с 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
- Просим вью спрятать индикатор загрузки, так как вы закончили загрузку и парсинг
- Просим вью отобразить список книг
MakeВы theможете asynchronousпройтись requestпо toмассиву getкниг theи books’распечатать data.
Parseкаждоый theкниги JSONв responseконсоль, receivedчтобы asбыть anуверенным, arrayчто ofвсе instancesработает ofправильно. theЧтобы Bookэто dataсделать class.
Askследующие theлинии viewкода toпосле hideтого theкак loadingкниги indicator,будут since you’ve finished loading and parsing.
Ask the view to show the list of books.
You can iterate the books array and print each book.title to the console to be sure that everything works correctly. To do this, add the following lines of code after the books have been parsed:распарсены:
books.forEach { book ->
println(book.title)
}
InЧтобы orderпротетировать toкод testпредставителя, outобновим the presenter code, update the main()
functionфункцию toдля read:чтения:
fun main(args: Array<String>) {
val bookStorePresenter = BookStorePresenter()
val bookStorePage = BookStorePage(bookStorePresenter)
bookStorePresenter.attach(bookStorePage)
bookStorePresenter.loadBooks()
}
Here,Тут youмы createсоздаем aновый newобъект instanceBookStorePresenter
, ofи BookStorePresenter,затем andобъект thenBookStorePage
, anпереданные instanceстранице ofот BookStorePage,объекта passingпредставителя theчерез pageего theконструктор. presenterТеперь instanceвы viaдобавляете itsстраницу constructor.в Youпредставителя thenи attachвызывается the page to the presenter and call loadBooks()
onнепосредственно theна presenter directly.
Build and run the project and refresh index.html. You should see a log like the following screenshot:
Books in Dev Console
When done with testing, remove the forEach loop with the print statement inside loadBooks()представителе.
Note:Сбилдим Ifи youзапустим tryпроект printingи theобновим booksindex.html
. themselvesВы должны увидеть лог как на картинке ниже:
После выполнения этого тестирования, уберите цикл forEach
с выражением print
внутри loadBooks()
Заметка: Если вы пытаетесь печтать саму книгу(println(book)
), it’sэто normalнормально toвидеть justтолько seeобъект objectObject
Objectповторяющийся repeatedснова overи andснова overв inвыходе. theЭто output.потому Thisчто isвызов becauseJSON.parce
theсоздает JSON.parse call constructs pureчистый JavaScript objectsобъект insteadвместо ofвызова callingконструктора theкласса constructorBook ofKotlin.
Это значит что вы сможете читать его свйоства, но любой метод ожидающий класс будет пропущен - включая автогерированную реализацию toSting()
. Если вам необходим более надежный метод анализа решения который сделает это правильно, вы можете изучить подробнее kotlinx.serialization
библиотеку.
Создание UI
Файл index.html
содержит два div
тага с ID
, названный loader
и content
. Для начала загрузочный индикаторый который вы можете показывать пока ваше приложение загружает данные, и прячет когда эта загрузка выполнени. Затем контейнер в который будет добавлены все карточки книг.
Для доступа в DOM элементы в вашем Kotlin Bookкоде, class.
Thisследующие meansдва you’llновых beсвойства ableв toкласс readBookStorePage
itsкак properties,показано but any methods you’d expect the class to have will be missing – including the auto-generated toString() implementation. If you need a more robust parsing solution that will do this correctly, you can take a look at the kotlinx.serialization library.
Building the UI
The index.html file contains two div tags with IDs, namely "loader" and "content"ниже. The former is a loading indicator that you can show while your app is loading data, and hide when it’s done loading. The latter one is a container that all of the book cards will be added to.
To access these DOM elements in your Kotlin code, add two new properties to the BookStorePage class as shown below.
private val loader = document.getElementById("loader") as HTMLDivElement
private val content = document.getElementById("content") as HTMLDivElement
YouВы canвсегда alwaysможете getполучить anэлемент elementв inDOM theпо DOMего byID
, itsиспользуя ID,объект usingдокумента theи documentgetElementeById()
objectметод, andтак theже getElementById()как method,это justделается like you would inв JavaScript.
TheМетод getElementById()
methodвозвращает returnsобщий aElement
, genericкоторый Element,вы whichможете youпривести canк castдругому toтипу theэлемента moreесли specificнужно.(похоже elementна typeто ifна youкак needметод (similar to how the findViewById()
methodработает used to work onв Android).
Changing
Меняем theвидимость Loader Visibilityзагручкика
UpdateОбновим theметоды showLoader()
andи hideLoader()
methodsв inBookStorePage
BookStorePageследующий in the following way:образом.
override fun showLoader() {
loader.style.visibility = "visible"
}
override fun hideLoader() {
loader.style.visibility = "hidden"
}
Again, you use the usual DOM APIs to change the visibility property of the elements to either "visible" or "hidden"Снова, asвы required.используете обычную DOM модель для изменения визуальныйх свойств элементов visible
и hidden
, как и трубется.
TheЭлемент loaderзагрузчик elementвидимый isпо visibleумолчанию, byпоэтому default,вы soдолжны youвидеть shouldего seeкогда itоткрываете when you open theстраницу index.html page.html.
Loading
TestПроверьте yourваш codeкод andи hideспрячьте theзагрузчик loaderизменим byследующим addingобразом theфункцию updating the main()
: function to the following:
fun main(args: Array<String>) {
val bookStorePresenter = BookStorePresenter()
val bookStorePage = BookStorePage(bookStorePresenter)
bookStorePage.hideLoader()
}
You’veВы updatedобновили themain
mainфункцию functionвызывать to directly call hideLoader()
toнапрямую hideчтобы theпрятать spinnerиндикатор thatкоторый wasбыл visibleдо before.этого видим.
BuildСбилдим theпроект projectи andобновим refresh index.html inв yourвашем browser.браузере. TheЗагрузчик loaderдолжен shouldтеперь nowисчезнуть.
Создание gone!элементов книги.
Теперь, вы создадие карточку отображения каждой книги, как показано ниже
Building Book Elements
Next, you’ll build cards for each book to display, like this one:
BookСоздайте Card
Createкласс aи newназовите classего andCardBuilder
. nameВ itэтом CardBuilder.классе Inвы thisсоздадите class,HTMLElement
you’llдля buildпредставления anкниги, HTMLElementсвяжите toдетали presentкниги theс book,ним, bindи the books’ details to it, and applyпримените CSS. StartНанчите byс updatingкласса theкак classпоказано to the following:ниже:
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)
}
}
}
There’sНужно aмного lotчего toеще doсделать, here,давайте soпосмотрим let’sна lookшаги atпо theочереди:
- Создть
atновыеaэлементыtime:используяcreateElement()
API барузера, передавая имя HTML тэга для созадния. Для примера, используейтеdiv
для созданияHTMLDivElement()
иimg
чтобы создатьHTMLImageElement
. - Свяжите класс данных
book
c созданнымHTML
элементом. Вы скоро реализуете этот методbind()
- Примените некоторые CSS классы к
HTML
элементам. Так же реализуетеapplyStyle()
метод ниже. - Добавите все отдельные
HTML
элементы в один контейнер. - Вернете контейнер, который является корневым элементов карточек.
- Напишите расширение функции которая позволяет вам добавлять переменнок количество дочрехних к элементы, вместо вызова обычного
appendChild()
метода множество раз.
Создание данных
Заполните элементы данными, добавьте следующий код.
CreateЗаполнетие newэлементы elementsданными, byдобавьте usingслеующие theметод createElement()в browserкласс API, passing in the name of the HTML tag to create. For example, use "div" to create an HTMLDivElement and "img" to create an HTMLImageElement.
Bind the book data to the HTML elements you created. You will implement this bind() method soon.
Apply some CSS classes to the HTML elements you created. You will also implement the applyStyle() method below.
Append all of the individual HTML elements to one container.
Return the container, which is the root element of the card.
Write an extension function that enables you to append a variable number of children to an element, instead of having to call the regular appendChild() method many times.
Binding the Data
To populate the elements with data, add the following method to the CardBuilder
: class.
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)
})
}
InВ thisэтом method,методе, you:вы:
Set the book cover image URL as the source of the image element on the card. Set the text content for the various text elements. Add a click event listener to the button element, which will navigate to the book’s URL if the button is clicked. Applying CSS The other method still missing is applyStyle(), which you should also add to the CardBuilder class.
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")
}
This method adds the proper CSS classes that you need to style the book card with a material design style. You can find these classes already set up in the styles.css file. For example, the card-shadow CSS class gives a material shadow to the card container, and the float-left CSS class aligns the element to the left.
Creating Cards Let’s go back to the BookStorePage class and start using this card creation code. First, add a property to the class, which will store an instance of CardBuilder.
private val cardBuilder = CardBuilder()
Then, go to the showBooks() method and add the following code:
books.forEach { book ->
val card = cardBuilder.build(book)
content.appendChild(card)
}
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.
Showing the Book Store Page You’re almost done now. Add the following method to the BookStorePage class:
fun show() {
presenter.attach(this)
presenter.loadBooks()
}
This code sets the current BookStorePage instance as the presenter’s view so that it can receive callbacks from it, and then it asks the presenter to start loading the books.
Go to the main() function and update it to call this show() method on bookStorePage. The entire main() method should now look like this:
fun main(args: Array<String>) {
val bookStorePresenter = BookStorePresenter()
val bookStorePage = BookStorePage(bookStorePresenter)
bookStorePage.show()
}
Build the project and refresh index.html.
You should see the loader briefly before the app finishes loading the books. Then the book cards will appear. The cards should have a shadow when you hover over them, and the View Details button should navigate you to the appropriate page for the book.
End Product
Hooray! You have created your first web app in Kotlin :]
Happy Face
Where to Go from Here? You can download the files for the completed project (as well as the starter project) by clicking on the Download Materials button at the top or bottom of the tutorial.
We’ve covered the basics of using Kotlin.js to build web apps with Kotlin in the browser, but there’s a lot more to discover on this topic.
If you’re interested in setting up a more advanced project and development environment, you can learn about building Kotlin.js projects with Gradle, unit testing Kotlin code with JavaScript test frameworks, and even about debugging Kotlin in the browser.
Finally, you can take a look at how to call a JavaScript function from Kotlin as if it was a static method.
If you have any questions or comments, join in on the forum discussion below! We are happy to hear from you :]
Tools & Libraries Android & Kotlin Tutorials