Мы — команда ellow. Разрабатываем мобильные приложения. В серии статей поделимся нашим опытом написания тестового Telegram Mini App.

В этой статье рассмотрим старт проекта как обычное веб-приложение с минимальным функционалом. Остальные функции будут завязаны на Telegram API и веб-приложение сможет запускаться из Telegram.
Навигация по циклу статей
Часть 1. Пишем кликер на Kotlin/JS — текущая статья
Часть 2. Пишем кликер для Telegram на Kotlin — в разработке
Часть 2.5. Аутентификация пользователя с rest‑framework. TMA на KMP — в разработке
Часть 3. Добавляем оплату через Telegram Mini Apps — в разработке
Раскрытые темы в цикле
- Web приложение на Kotlin — часть 1 
- Интеграция приложения с Telegram Mini Apps — часть 2 
- Работа с элементами интерфейса TMA приложения. Тема, - MainButton,- BackButton— часть 2
- Поделиться ссылкой на приложение через Telegram. Передача данных через ссылку — часть 2 
- Аутентификации через TMA приложение — часть 2 и 2.5 
- Telegram Payments API — часть 3 
Техническое задание. Кратко
Разработать Telegram Mini Apps приложение-кликер с основными механиками:
- Тапать на коин и увеличивать счётчик 
- Сохранять количество тапов для каждого пользователя 
- Приглашать друзей через ссылку из приложения 
- Просматривать список друзей. 
- Оплата премиум статуса через Telegram 
- В будущем перенести на Android/IOS 
Зачем нужен Kotlin в Web. Ещё один .js фреймворк?
Kotlin Multiplaform анонсирован ещё в 2017 с поддержкой JVM, Native и JS таргетов. И именно JS тартет наиболее недооценён как самой JetBrains, так и компаниями, применяющими в разработке язык Kotlin.
Только в середине 2023 года JetBrains заявила о смене аббревиатуры с KMM (Kotlin Multiplatform Mobile) на KMP (Kotlin Multiplatform), что говорит о перспективах развития всех тартетов.
Зачем бизнесу нужен Kotlin Multiplatform, тем более в веб‑разработке? В мобильные приложения уже давно интегрируют общий код между платформами, однако JS таргет, хоть и представлен давно, не все разработчики видят в его использовании смысл. Однако причина писать веб‑приложения на Kotlin такая же как и для мобильных платформ — это шаринг кода между платформами. Уже имея приложение на KMP под две платформы — Android и iOS — можно третьей добавить браузер и разработать веб приложение, не переписывая бизнес логику, хотя конечно же доступа к системе у веб‑приложений меньше, из‑за чего придётся уменьшить количество поддерживаемых фичей.
Kotlin/JS тартет способен компилироваться в JavaScript код, вызывать методы из JS модулей или самим быть вызываем, что значительно расширяет возможности приложений на Kotlin/JS и даёт возможность писать на Kotlin с кодовой базовой уже существующих JS приложений.
Главная трудность — это написание UI. Сейчас есть выбор между четырьмя реализациями
- Написать UI на HTML и CSS. И работать с DOM деревом как в старые добрые — получать элемент по id и изменять его 
- Использовать React in Kotlin из модуля kotlin‑wrappers. Однако такой подход выглядит неестественным для языка Kotlin. Повсеместное использование external для props, неоднородные стили с CSS in Kotlin 
- Использовать Compose for Web (пока статус Alpha) из Compose Multiplatform. Можно компилировать под WASM (который использует wasm gc, поддержки которого пока нет в Safari/WebKit) и JS, где уже встречаются баги взаимодействия со скроллом и нестабильной работой. 
- Наиболее перспективный вариант Compose HTML library. 
Выбор на чем писать UI
Подробнее о Compose HTML library. Код компилируется в JS и использует compose runtime и html теги с css in kotlin стилями, по идеологии схож с JSX.
Доступен базовый функционал compose runtime: Composable аннотация, Side-effects, states и д.р. из пакета compose-runtime.
Не доступны другие пакеты, как compose-ui, compose-material, из-за чего нельзя использовать привычные Column, Row, Box, Scaffold, Button (composable ui функции), MaterialTheme и д.р.
Для написания UI используются функции Div, Button, Span, TextArea и д.р. браузерные теги, только вызываемые как функции с заглавной буквы. Стили применяются через СSS in Kotlin через отдельный StyleSheet или блок style в attrs любого тега.
Старт проекта
Для упрощения старта можно использовать шаблон, однако ещё разберём особенности настройки.
Файловая структура — типичная для проекта для kotlin multiplatform.

В build.gradle.kts composeApp модуля подключим плагины (также нужно объявить в корневом модуле 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
}Зададим JS таргет и зависимости в блоке kotlin{} модуля composeApp
kotlin {
    js(IR) {
        browser()
        binaries.executable()
    }
    sourceSets {
        jsMain.dependencies {
            implementation(compose.runtime)
            implementation(compose.html.core)
        }
    }
}Создаём директории для наших исходников commonMain и jsMain

В resources jsMain нужно создать 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="app.js"> </script>
</body>
</html>Далее определим точку входа в приложение. Создаём main.kt с функцией main, которая и будет точкой входа в наше приложение
fun main() {
    renderComposable(rootElementId = "app") {
       /* Весь интерфейс здесь */
    }
}Напишем простой экран, который будет сообщать, что это веб-приложение, написанное на Kotlin
Важно: UI находится в
jsMain, поскольку это нативный для браузера способа отображения
@Composable
fun App() {
    Div {
        H2 {
            Text("This is my Kotlin web application")
        }
    }
}Вызовем функцию с нашим приложением в блоке renderComposable
renderComposable(rootElementId = "app") {
    App()
}Первоначальная настройка закончена, теперь можно запустить приложение и проверить, что всё работает как надо
./gradlew composeApp:jsRunНо страница выглядит просто как текст, Compose HTML library может инжектить в браузер свои стили, которые пишутся на Kotlin. Добавим таблицу стилей в наше приложение
object AppStyles: StyleSheet() {
    val MainContainer by style {
        display(DisplayStyle.Flex)
        width(100.vw)
        height(100.vh)
        textAlign("center")
        alignItems(AlignItems.Center)
        justifyContent(JustifyContent.Center)
    }
}Важно, что каждый StyleSheet должен быть явно определён, именно в этот момент он инжектится в браузерную страницу. Применяем таблицу стилей.
renderComposable(rootElementId = "app") {
    Style(AppStyles)
    App()
}Далее через поля из AppStyles применим стиль к нашему Div.
@Composable
fun App() {
    Div(
        attrs = {
            classes(AppStyles.MainContainer)
        }
    ) {
        H2 {
            Text("This is my Kotlin web application")
        }
    }
}Запускаем ещё раз и наблюдаем, как наши стили применились и приложение выглядит, как нам нужно.
Добавляем ресурсы
Теперь переходим к созданию полноценного веб-приложения. Каждый этап описан не будет, однако основные моменты будут описаны. Некоторые из доступных, хоть и ограниченных, возможностей Compose runtime и Compose HTML library, при работе с KMP
Для этого используем библиотеку moko-resources. Подключается вместе с gradle плагином в build.gradle.kts модуля 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")
}Добавим нашу картинку, по которой будем тапать в commonMain модуль.

И получим url до картинки через сгенерированный класс Res и поле Res.images.click_item.
@Composable
fun App() {
    Div(
        attrs = {
            classes(AppStyles.MainContainer)
        }
    ) {
        Img(
            src = Res.images.click_item.fileUrl,
            attrs = {
                classes(AppStyles.MainImage) // стиль с указанием размера картинки
            }
        )
    }
}Создаём UI нашего кликера
Поскольку это кликер, обязательно нужно добавить счётчик нажатий на наш «подставить на кого будем кликать».
Создадим состояние, значение которого будем увеличивать по клику на картинку.
@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++
                }
            }
        )
    }
}Итоги
В данной статье мы научились разрабатывать веб-приложения на довольно нестандартном стеке.
Список используемых библиотек на данный момент:
- Compose Runtime 
- Compose HTML library 
- Moko-resources 
Дальше – больше, в следующих статьях это просто веб-приложение с одной кнопкой станет полноценным Telegram-кликером со своей реферальной системой.
Мы не только делимся своим опытом в статьях, но и используем его во благо бизнеса. Возможно, именно вашего! Обращайтесь к нам для разработки мобильных приложений под ключ. Работаем на подряде, субподряде, предоставляем аутстафф. 
 
           
 
MEJIOMAH
https://github.com/ellow-tech/kmp-compose-html-template - 404
https://github.com/JetBrains/compose-multiplatform/tree/master/tutorials/HTML/Getting%5C_Started%5C - 404
Ellow_Tech Автор
Первая ссылка – репозитория был закрыт, открыли. Вторая ссылка оказалась неправильно вставлена, исправили.
Спасибо за обратную связь!