Web App With Kotlin.js: Getting Started
Why Kotlin.js? So, why would you use Kotlin for web development? Here are a few reasons:
- Familiarity: If you’re coming from a compiled language like Java, Swift or C#, you’ll find Kotlin very easy to learn — and you might already be familiar with it if you’re an Android developer.
- Friendly tools: IDEs can’t help you much with JavaScript, while Kotlin comes with first-class IDE support so that you can spot common errors as you type the code.
- Sharing code between platforms: With Kotlin Multiplatform Projects, you can write the business logic of your app once, and share it across many platforms including your back-end, browser front-end, Android and iOS clients.
- Interoperability: The ability to call JavaScript code from your Kotlin code is very important. It lets you reuse existing JavaScript code you’ve already written with Kotlin. This interop works in the opposite direction as well. You can call your Kotlin code from JavaScript.
- JavaScript woes: Kotlin is a perfect choice if you gave up on web development because of common JavaScript issues, such as having to deal with dynamic typing, weird logical comparisons, or prototypes.
- Prerequisites: For this tutorial, you need a basic knowledge of web programming and a familiarity with Kotlin and IntelliJ IDEA. If you’re completely new to Kotlin, you might want to check out the Kotlin Apprentice book or the Programming in Kotlin video course first.
Getting Started
You’re going to build a bookstore app for raywenderlich.com. The app will fetch data about the books from a web service, display the book covers with titles, descriptions and prices, as well as a button to open a book’s details page on raywenderlich.com. Here’s what the end product will look like:
End Product
To follow along with this tutorial, you’ll need to use IntelliJ IDEA 2018.2.7 or later (the free Community Edition is good enough), and Kotlin 1.2.61 or later.
Start by downloading the materials for this tutorial using the Download Materials button at the top or bottom of this page. Then fire up IntelliJ IDEA and create a new project.
In the New Project window, select Kotlin from the left side panel and Kotlin/JS on the right. Then, click Next.
New Project Step 1
For the project name, use bookstore. Select a project location, or use the default. Then click Finish.
New Project Step 2
Unzip the materials you’ve downloaded, and from the bookstore-starter folder, copy the index.html and styles.css files to your project’s root folder. The project structure should look like the following:
Project Anatomy
Creating the Main Function
Create a new Kotlin file with the name Main.kt in the src directory and add the main() function as shown below:
fun main(args: Array<String>) {
println("Hello world!")
}
Note: The app’s main() function can be in any Kotlin file with any name, so using Main.kt as the filename here is not strictly required, but you should only have one main() function in your project. Next, open the Build menu and click on Build Project to compile your Kotlin code to JavaScript. The IDE will generate a new folder called out in your project’s root folder.
You need to reference two files from this out folder in index.html:
kotlin.js: This is a distribution of the Kotlin standard library, implemented in JavaScript.
bookstore.js: This is the JavaScript that your own Kotlin code was compiled to.
Note: Both files are already referenced for you within script tags in the starter index.html file, but you may need to check their paths in case you entered a different project name than bookstore.
Finally, open index.html in your browser, and then open the developer console. You should see “Hello World!”, like the following screenshot:
Hello World in Dev Console
Note: Every time you change your Kotlin code, you’ll need to build your project and then refresh the HTML page to see the changes.
Calling JavaScript Code From Kotlin
In the index.html file, inside a script tag, you’ll find a JavaScript function called getApiUrl(), which returns the URL you need to fetch the bookstore JSON data from.
<script>
function getApiUrl(){
return "https://gist.githubusercontent.com/tarek360/4578e33621011e18829bad0c8d1c8cdf/raw/06d185bebc3e14a56dfa85f53288daddd4ff6a2b/books.json";
}
</script>
There are many ways to access a JavaScript function or variable from your Kotlin code. One of them is by using the js() function, which allows you to pass native JavaScript code as a string.
Add the following line of code to the Main.kt file, outside of the main() function.
val API_URL = js("getApiUrl()") as String
Here, you pass the string "getApiUrl()" to the js() function. Since the getApiUrl() function always returns a string, you can cast it safely to a Kotlin String, and store it in a Kotlin value.
Now, update the main() function to print the value of the API_URL property instead of “Hello world!”.
fun main(args: Array<String>) {
println(API_URL)
}
Build the project and refresh index.html in your browser. You should see the value of the API_URL variable printed to the console, like the following screenshot:
API URL in Dev Console
Now, you have the URL saved in the API_URL value, which you’ll use later.
Finally, clear the main() function’s body to get ready for what comes next.
Representing Books RW Books
To fetch book data from the server and present it on the UI, you need to create a new Kotlin class to represent a single book. Create a file named Book.kt in the src folder and place this data class in it:
data class Book(val title: String,
val price: String,
val description: String,
val url: String,
val coverUrl: String)
Every book has a title, price, description, a URL for its details page on raywenderlich.com, and a cover image URL.
Architecting the App
You’ll use a basic MVP architecture in this app. A presenter class will contain all of the business logic, while a page class will act as the view. Before you create these classes, you’ll create the contract between them.
Note: If you haven’t seen the MVP pattern before, you can check out the tutorial Getting Started with MVP on Android.
Create a new Kotlin interface called BookStoreContract (as usual, in its own file in the src folder) which defines the connection between the view and the presenter. Add the following code to it:
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
}
}
The view will be able to:
Show a list of books provided to it.
Show a loading indicator while the app is fetching the book data from the server.
Hide the loading indicator.
As for the presenter, it can:
Display results on any view that it’s provided.
Start loading the book data from the data source. In this case, that’s a remote server.
With that done, you can now create a BookStorePage class, and add the following code:
class BookStorePage(private val presenter: BookStoreContract.Presenter) : BookStoreContract.View {
override fun showBooks(books: List<Book>) {
}
override fun showLoader() {
}
override fun hideLoader() {
}
}
This class has a constructor with a BookStoreContract.Presenter parameter. It implements the BookStoreContract.View interface with three required methods (empty, for now).
Create a BookStorePresenter class and add the following code:
// 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() {
}
}
In this class, you:
Implement the BookStoreContract.Presenter interface.
Add a lateinit property to keep a reference to the view.
Implement the attach() method from the BookStoreContract.Presenter interface, and initialize the view property with the received parameter.
Implement the loadBooks() method required by the BookStoreContract.Presenter interface (empty, for now).
Fetching Data From the Server
You need a way to fetch data from the server. To do this, add the following method to the BookStorePresenter class.
// 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()
}
Hit option+return on Mac or Alt+Enter on PC to add in an import for the XMLHttpRequest class.
Let’s go over what you’re doing here, step-by-step.
Create a new method that makes a network request. It takes a URL to fetch from, as well as a function with a String parameter, which it will pass the result of the network call to.
Create a new XMLHttpRequest instance.
Set this request up so that it sends an HTTP GET to the given URL.
Set a callback which will be invoked when the request completes.
Check if the request is in a done (4) state, and if it has an OK (200) status code.
Call the callback function received as a parameter, and pass it the contents of the network response as a single string.
Invoke send() to fire off the HTTP request you’ve set up.
With that done, you can now use this helper method to implement 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())
}
}
In this code, you:
Ask the view to show a loading indicator before you start loading the data.
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 instance of BookStorePresenter, and then 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 books themselves (println(book)), it’s normal to just see object Object repeated over and over in the output. This is because the JSON.parse call constructs pure JavaScript objects instead of calling the constructor of the Kotlin Book class.
This means you’ll be able to read 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 in the DOM by its ID, using the document object and the getElementById() method, just like you would in JavaScript.
The getElementById() method returns a 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 in 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.
The loader element is visible by default, so you should see it when you open the index.html page.
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 the 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 be gone!
Building Book Elements Next, you’ll build cards for each book to display, like this one:
Book Card
Create a new class and name it CardBuilder. In this class, 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 steps one at a time:
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