Part 1. TMA on KMP. Writing a clicker in Kotlin
In this article, we will consider the launch of the project as a regular web application with minimal functionality. The remaining functions will be tied to the Telegram API and the web application will be able to be launched from Telegram.
Navigating the article series
Part 1. Writing a clicker web app in Kotlin – current article
Topics covered in the series
Web Application in Kotlin – Part 1
Integrating the App with Telegram Mini Apps – Part 2
Working with the interface elements of the TMA application. Theme,
MainButton
,BackButton
– part 2Share a link to an application via Telegram. Transferring data via a link – part 2
Authentication via TMA application – part 2 and 2.5
Telegram Payments API – Part 3
Technical task. Briefly
Develop Telegram Mini Apps – a clicker app with the main mechanics:
Tap on the coin and increase the counter
Save the number of taps for each user
Invite friends via a link from the app
View your friends list.
Payment for premium status via Telegram
In the future, we will transfer it to Android/IOS
Why Kotlin is needed in Web. Another .js framework?
Kotlin Multiplaform announced back in 2017 with support for JVM, Native and JS targets. And it is the JS target that is most underestimated by both JetBrains itself and companies using the Kotlin language in development.
Only in mid-2023, JetBrains announced the change of the abbreviation from KMM (Kotlin Multiplatform Mobile) to KMP (Kotlin Multiplatform), which speaks about the development prospects of all platforms.
Why does a business need Kotlin Multiplatform, especially in web development? Mobile applications have long been integrating common code between platforms, but JS target, although it has been presented for a long time, not all developers see the point in using it. However, the reason to write web applications in Kotlin is the same as for mobile platforms – code sharing between platforms. Already having an application on KMP for two platforms – Android and iOS – you can add a third browser and develop a web application without rewriting the business logic, although of course web applications have less access to the system, which is why you will have to reduce the number of supported features.
The Kotlin/JS target is capable of compiling into JavaScript code, calling methods from JS modules or being called itself, which significantly expands the capabilities of Kotlin/JS applications and makes it possible to write in Kotlin with the code base of existing JS applications.
The main difficulty is writing the UI. There are currently four implementations to choose from.
Write UI in HTML and CSS. And work with DOM tree as in good old days – get element by id and change it
Use React in Kotlin ( kotlin-wrappers/kotlin-rea… @GitHub) from the kotlin-wrappers module. However, this approach seems unnatural for the Kotlin language. The widespread use of external for props, inconsistent styles with CSS in Kotlin
Use Compose for Web (currently in Alpha status) from Compose Multiplatform. Can be compiled for WASM (which uses wasm gc, which is not yet supported in Safari/WebKit) and JS, where there are already bugs in interaction with scrolling and unstable operation.
The most promising option Compose HTML library.
Choosing what to write UI in
More about Compose HTML library. The code is compiled to JS and uses compose runtime and html tags with css in kotlin styles, similar in ideology to JSX.
Available basic functionality of compose runtime: Composable annotation, Side-effects, states, etc. from the compose-runtime package.
Not available other packages like compose-ui, compose-material, which is why you can't use the usual ones Column
, Row
, Box
, Scaffold
, Button
(composable ui functions), MaterialTheme
and etc.
Functions are used to write UI Div
, Button
, Span
, TextArea
and other browser tags, only called as functions with a capital letter. Styles are applied via CSS in Kotlin via a separate StyleSheet
or block style
V attrs
any tag.
Project start
To make the start easier, you can use samplebut we will still look at the settings features.
The file structure is typical for a project for kotlin multiplatform.
IN build.gradle.kts
composeApp module we connect plugins (also need to declare in the root module build.gradle.kts
)
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("org.jetbrains.kotlin.plugin.compose") // компилятор, необходимый для сборки модуля с compose на Kotlin 2.X
id("org.jetbrains.compose") // работа с compose multiplatform
}
Let's set the JS target and dependencies in the block kotlin{}
module composeApp
kotlin {
js(IR) {
browser()
binaries.executable()
}
sourceSets {
jsMain.dependencies {
implementation(compose.runtime)
implementation(compose.html.core)
}
}
}
We create directories for our sources commonMain
And jsMain
In resources jsMain
need to create index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EllowBurgerBot</title>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>
<body id="app" class="app">
<script src="https://habr.com/ru/articles/830120/app.js"> </script>
</body>
</html>
Next, we will define the entry point to the application. Create main.kt
with function main
which will be the entry point to our application
fun main() {
renderComposable(rootElementId = "app") {
/* Весь интерфейс здесь */
}
}
Let's write a simple screen that will tell you that this is a web application written in Kotlin
Important: UI is in
jsMain
since this is the browser's native display method.
@Composable
fun App() {
Div {
H2 {
Text("This is my Kotlin web application")
}
}
}
Let's call the function with our application in the block renderComposable
renderComposable(rootElementId = "app") {
App()
}
The initial setup is complete, now you can launch the application and check that everything works as it should.
./gradlew composeApp:jsRun
But the page looks like just text, Compose HTML library can inject its styles, which are written in Kotlin, into the browser. Let's add a style sheet to our application
object AppStyles: StyleSheet() {
val MainContainer by style {
display(DisplayStyle.Flex)
width(100.vw)
height(100.vh)
textAlign("center")
alignItems(AlignItems.Center)
justifyContent(JustifyContent.Center)
}
}
It is important that everyone StyleSheet
must be explicitly defined, it is at this point that it is injected into the browser page. We apply the style sheet.
renderComposable(rootElementId = "app") {
Style(AppStyles)
App()
}
Further through the fields from AppStyles
let's apply the style to ours Div
.
@Composable
fun App() {
Div(
attrs = {
classes(AppStyles.MainContainer)
}
) {
H2 {
Text("This is my Kotlin web application")
}
}
}
Let's run it again and see how our styles have been applied and the application looks the way we want it to.
Adding resources
Now we move on to creating a full-fledged web application. Each stage will not be described, but the main points will be described. Some of the available, albeit limited, capabilities of the Compose runtime and Compose HTML library, when working with KMP
For this we use the library moko-resources
. Connected together with the gradle plugin in build.gradle.kts
module composeApp
plugins {
// ...
id("dev.icerock.mobile.multiplatform-resources")
}
kotlin {
//...
sourceSets {
commonMain.dependencies {
implementation("dev.icerock.moko:resources:0.24.1")
}
//...
}
}
multiplatformResources {
resourcesPackage.set("your.package.name")
resourcesClassName.set("Res")
}
Let's add our picture, which we will tap on commonMain
module.
And we get url
to the image via the generated class Res
and the field Res.images.click_item
.
@Composable
fun App() {
Div(
attrs = {
classes(AppStyles.MainContainer)
}
) {
Img(
src = Res.images.click_item.fileUrl,
attrs = {
classes(AppStyles.MainImage) // стиль с указанием размера картинки
}
)
}
}
Let's create the UI of our clicker
Since this is a clicker, we definitely need to add a click counter to our “insert who we will click on”.
Let's create a state, the value of which will be increased by clicking on the picture.
@Composable
fun App() {
var score by remember { mutableStateOf(0) }
Div(
attrs = {
classes(AppStyles.MainContainer)
}
) {
H2 {
Text("Score: \$score")
}
Img(
src = Res.images.click_item.fileUrl,
attrs = {
classes(AppStyles.ClickImage)
onClick {
score++
}
}
)
}
}
Results
In this article, we learned how to develop web applications on a rather non-standard stack.
List of libraries currently used:
Compose Runtime
Moko-resources
Further morein the following articles this simple web application with one button will become a full-fledged Telegram clicker with its own referral system.