February 22, 2025
As an experiment, I decided to set up a repo that uses kotlin/js to fetch a list of todos completely clientside.
Here is a live demo so you can see what it does; a simple fetch of todos and ability to paginate through them. Also I've added a button to filter out all of the unfinished items.
First, we make a fetch with axios. Axios integration with Kotlin is daunting at first but they've made it pretty easy.
The only thing I needed to use axios was a helper class and to add the dependency to my gradle file
implementation(npm("axios", "1.7.9"))
@JsModule("axios")
@JsNonModule
@JsName("axios")
external class Axios {
companion object {
@JsName("get")
fun get(endPoint: String): Axios
}
@JsName("then")
fun then(unt: (Axios) -> Unit)
}
Then to actually use axios it's as you'd expect:
Axios.get(endPoint = urlEndpoint)
.then {
val result = it.asDynamic().request.responseText as? String ?: "Get Request String Issue"
val todoList = Json.decodeFromString<List<TodoResponseItem>>(result)
todoListManager = TodoListManager(fullTodoList = todoList)
setResultList()
}
An interesting line of code here is the call to Json.decodeFromString
, what that's actually doing is allowing me to deserialize the request dynamically into a data class.
This means I'm proccessing my code type safely, and don't need to use .asDynamic()
past this point.
This repo uses Kotlinx.serialization to deserialize the json dynamically into a data class, it's easy to add to gradle:
plugins {
...
kotlin("plugin.serialization") version "2.1.10"
}
dependencies {
...
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
}
Now my data class for each Json typicode todo object looks as expected, just needs @Serializable annotation.
@Serializable
data class TodoResponseItem (
val userId: Int,
val id: Int,
val title: String,
val completed: Boolean
)
We can use TodoResponseItem
s anywhere in code with type-safety!
My favorite part of this mini project is using Kotlinx.html Kotlin's html templating. This allows us to write tags enclosed in brackets, giving us a much more code like feel. For instance, our main list looks like this:
document.getElementById("mainBody")
?.apply {
innerHTML = ""
append {
ul {
todoTemplate(todoList = resultsByPage)
}
}
}
Writing html this way is a breeze and way less prone to errors. Since the todo template is effectively a list of li items, we know it will work ahead of time at compile.
fun UL.todoTemplate (todoList: List<TodoResponseItem>)
= todoList.forEach { todo ->
li {
b { + todo.title }
+ if(todo.completed) "✅" else "❌"
}
}
This gives us succinct code that is also flexible and readable. I prefer this to jsx and think it's far superior.
Writing the todolist manager was also a breeze. The core of my pagination logic lies with adjusting the list with a call to js .slice(). This is a nice idiomatic approach illustrating how we can still use JS api's where we want. This snippet is nearly 1:1 with pure js.
fun resultsByPage(filteredList: List<TodoResponseItem>? = null, adjustedInterval: Int? = null)
: List<TodoResponseItem> {
val todoList = filteredList ?: fullTodoList
val pageNum = if(currentPageNumber < endPage) currentPageNumber else endPage
val interval = adjustedInterval ?: currentInterval
//1: 0..9, 2: 10..19, 3: 20..29 etc
val rangeStart = (pageNum - 1) * interval
val rangeEnd = rangeStart + (interval-1)
return todoList.slice(indices = IntRange(start = rangeStart, endInclusive = rangeEnd))
}
//A second version of this method written in more idiomatic kotlin
//the main differences being `coerceAtMost` and `subList` which I think
//make this code a bit more readable
fun resultsByPage(filteredList: List<TodoResponseItem>? = null, adjustedInterval: Int? = null)
: List<TodoResponseItem> {
val todoList = filteredList ?: fullTodoList
val pageNum = currentPageNumber.coerceAtMost(endPage)
val interval = adjustedInterval ?: currentInterval
val rangeStart = (pageNum - 1) * interval
val rangeEnd = (rangeStart + interval).coerceAtMost(todoList.size)
return if (rangeStart in todoList.indices)
todoList.subList(rangeStart, rangeEnd)
else emptyList()
}
Further down I decided to use kotlin's api's a bit more. Kotlin's api makes doing things like sorting and filtering much easier to understand than with JS clunky closure syntax.
While the difference above is arguable, writing this same snippet in JS would be ugly, Kotlin's {}
bracket syntax makes it much easier on the eyes.
fun onlyUnfinishedTasks(): List<TodoResponseItem> {
currentPageNumber = 1
onlyUnfinished = !onlyUnfinished
return if (onlyUnfinished)
fullTodoList.filterNot{ it.completed }.let {
resultsByPage(filteredList = it, adjustedInterval = it.size)
} else resultsByPage()
}
fun sortByName() {
fullTodoList = fullTodoList.sortedBy { it.title.uppercase() }
}
I do think Kotlin has a well thought-out approach to client side js. I'd love to see more examples of clientside js but it's hard to find online (hence why I've written this article).
I encourage anyone reading to try it out! It's easy, just checkout the project and use:
./gradlew browserDevelopmentRun
And you should see it live in you browser.
Checkout the code on Github.