Kotlin Multiplatform — технология, позволяющая объединять бизнес-логику для приложений разных платформ. В ней доступен полный контроль над тем, какие нативные инструменты использовать, а какие вынести в общий модуль (shared). Это позволяет применять данную технологию в уже существующих проектах, что существенно отличает Kotlin Multiplatform от других кроссплатформенных фреймворков таких, как Cordova или Flutter.
Использование приложениями общего модуля Kotlin Multiplatform позволяет:
Дополнить привычный для платформы функционал.
Стандартизировать подходы.
Упорядочить конфигурационные файлы.
Упростить написание приложений.
Приложение на базе ОС Аврора может работать через компонент QML WebView, поставляемый ОС Аврора, на базе Kotlin Multiplatform JS. На данный момент нет нативной поддержки Kotlin Multiplatform, информацию о доступных фреймворках можно найти по ссылке. Доступен также способ разработки кроссплатформенного приложения с использованием Flutter, информацию по статусу развития можно найти здесь.
При наличии у разработчика готового приложения, написанного на Kotlin Multiplatform (KMP), оно может быть портировано на ОС Аврора. Либо можно написать новое приложение с использованием Kotlin Multiplatform JS под ОС Аврора.
В данной статье показаны основные этапы портирования существующего приложения Kotlin Multiplatform на ОС Аврора. Портирование выполняется через цель сборки JS (Target JS). Для демонстрации используется открытое приложение для чтения RSS-новостей KMM RSS Reader. Портированное приложение можно найти по ссылке.
Схема взаимодействия приложения с общим модулем может выглядеть следующим образом:
Компонент Wrapper является связующим модулем между общим модулем Kotlin Multiplatform и приложением Qt/QML. Wrapper нужен для того, чтобы обеспечить работу с асинхронными функциями из QML, не модифицируя общий модуль KMP.
Структура
Приложение будет состоять из трех основных элементов:
Wrapper — библиотека JavaScript, которая связывает общий модуль Kotlin Multiplatform и QML.
QML-компонент (KMMAgent) — QML-обёртка над WebView, которая связывает нативный код QML и Wrapper.
Kotlin Multiplatform — общий модуль, который реализует бизнес-логику.
Примечание.
В связи признанием устаревшим названием продукта "Kotlin Multiplatform Mobile" в статье используется новая аббревиатура Kotlin Multiplatform - KMP.
Название портируемого приложения (KMM RSS Reader) и компонента (KMMAgent) остается прежним.
Схема взаимодействия элементов выглядит следующим образом:
Kotlin Multiplatform собирается в библиотеку Kotlin Multiplatform JS, которую можно подключить к любым проектам JS. Собранная библиотека Kotlin Multiplatform JS подключается к JS-проекту Wrapper, который обеспечивает работу асинхронных функций через Promise
с помощью событий events
. Собранный webpack Wrapper подключается в файле index.html
(файл index.html
подключается локально как точка входа для QML-компонента). Далее QML-компонент вызывает функции Wrapper и слушает события events
с помощью WebView, которые приходят из библиотеки Wrapper. WebView выполняет роль обслуживания бизнес-логики и скрыт от пользователя.
Структура директорий может быть выстроена на усмотрение разработчика с учетом платформы. Важно понимать, что три компонента (Kotlin Multiplatform, Wrapper, приложение Аврора) собираются отдельно, но в конечном итоге JavaScript-библиотека Wrapper должна быть доступна приложению Аврора при сборке, для этого она должна находится в разделе qml
.
Шаги по портированию KMP-приложения на платформу ОС Аврора могут быть следующими:
Подготовить исходный приложения созданного с помощью Kotlin Multiplatform. Например, KMM RSS Reader.
Собрать общий модуль KMP из исходного кода, как NPM-пакет, с помощью Gradle-плагина
npm-publish
.Создать проект приложения Qt/QML для ОС Аврора, используя Aurora SDK.
Добавить в проект QML-компонент на основе WebView для обеспечения работы с асинхронными функциями.
Создать JS-библиотеку Wrapper, которая свяжет KMP и QML-компонент.
Приступить к разработке пользовательского интерфейса приложения под ОС Аврора с использованием бизнес-логики Kotlin Multiplatform.
Сборка общего модуля Kotlin Multiplatform
Для добавления JavaScript в Kotlin Multiplatform нужно собрать модуль как JavaScript-библиотеку. В приведенном примере используется версия 1.8.0
плагина multiplatform
. Подробная информация по интеграции целей сборки JS доступна в документации "Kotlin/JS IR compiler". Сначала требуется добавить секцию js
в kotlin {}
в конфигурационный файл Gradle:
Файл <проект>/shared/build.gradle.kts
js(IR) {
moduleName = "shared"
version = "0.0.1"
nodejs()
binaries.library()
}
Для сборки модуля Kotlin Multiplatform как JS npm-пакета можно использовать Gradle-плагин npm-publish
. Его можно добавить следующим образом:
Файл <проект>/shared/build.gradle.kts
plugins {
id("dev.petuska.npm.publish") version "3.3.1"
}
npmPublish {
packages {
named("js") {
packageJson {
version.set("0.0.1")
}
}
}
}
Плагин npm-publish
добавит метод packJsPackage
. Его можно вызвать из командной строки для сборки npm-пакета:
./gradlew packJsPackage
QML-компонент (KMMAgent)
QML-компонент будет содержать WebView. WebView по умолчанию может работать только с синхронными функциями. Поэтому нужно добавить к WebView поддержку Promise
.
Объект Promise представляет возможное завершение (или сбой) асинхронной операции и ее результирующее значение.
Портируемое приложение будет обращаться в сеть для запроса XML-данных RSS. Так как запросы будут выполняться через WebView, следует отключить CORS.
CORS (Cross-origin resource sharing) — это механизм, который позволяет запрашивать ограниченные ресурсы на веб-странице из другого домена за пределами домена, из которого обслуживался первый ресурс.
Далее нужно создать скрипт framescript.js в приложении ОС Аврора. Он позволит слушать события events
приходящие со стороны библиотеки Wrapper.
Файл <проект>/auroraApp/RSSReader/qml/shared-js/framescript.js
addEventListener("DOMContentLoaded", function (aEvent) {
aEvent.originalTarget.addEventListener("framescript:log",
function (aEvent) {
sendAsyncMessage("webview:action", aEvent.detail)
});
});
Скрипт framescript.js нужно подключить к WebView, на основе которого создается QML-компонент KMMAgent
.
Файл <проект>/auroraApp/RSSReader/qml/pages/KMMAgent.qml
WebView {
id: webview
height: 0
width: 0
url: Qt.resolvedUrl("../shared-js/index.html")
visible: false
onViewInitialized: {
// Подключение слушателя событий
webview.loadFrameScript(Qt.resolvedUrl("../shared-js/framescript.js"));
webview.addMessageListener("webview:action")
}
onRecvAsyncMessage: {
switch (message) {
case "webview:action":
// Получение асинхронных ответов
break
}
}
Component.onCompleted: {
// Отключение CORS
WebEngineSettings.setPreference("security.disable_cors_checks", true, WebEngineSettings.BoolPref)
}
}
В WebView доступен метод runJavaScript
. Метод запускает фрагмент JavaScript в контексте загруженного документа DOM. С его помощью можно добавить функцию в QML-компонент, которая выполнит запрос к JavaScript-функции и подготовит данные для получения ответа со стороны JavaScript-библиотеки.
Файл <проект>/auroraApp/RSSReader/qml/pages/KMMAgent.qml
property var stateResponse: ({})
function run(method, result, error) {
// По ключевому слову функция определит тип запроса: асинхронные данные или нет
if (method.indexOf("return") === -1) {
// Далее будет добавлен функционал, который возвращает ключ асинхронного запроса
webview.runJavaScript("return " + method, function(key) {
// Следует запомнить функции, чтобы выполнять их после ответа
stateResponse[key] = [result, error]
}, error);
} else {
// Запуск по умолчанию, синхронные функции не требуют вмешательства
webview.runJavaScript(method, result, error);
}
}
Получить событие можно в методе WebView onRecvAsyncMessage
. При выполнении асинхронной функции QML-компонент (KMMAgent) выполняет запрос к синхронной функции Wrapper, которая производит запуск асинхронной функции KMP и возвращает уникальный ключ. Сохраняется пара QML-функция и уникальный ключ. При получении события вызывается соответствующая QML-функция по ключу. Данные события можно получить через переменную data
. Обработка в onRecvAsyncMessage
представлена ниже:
Файл <проект>/auroraApp/RSSReader/qml/pages/KMMAgent.qml
onRecvAsyncMessage: {
switch (message) {
case "webview:action":
try {
// Первое событие, которое обозначает готовность компонента
if (data.caller === 'init') {
root.completed()
// Другие события
} else if (root.stateResponse[data.caller] !== undefined) {
if (data.response.hasOwnProperty('stack')) {
// Обработка ошибки
root.stateResponse[data.caller][1](data.response.message)
} else {
// Обработка ответа
root.stateResponse[data.caller][0](data.response)
}
}
} catch (e) {
// Общая ошибка запроса, парсинга данных
root.stateResponse[data.caller][1](e.toString())
}
break
}
}
QML готов к выполнению функций Kotlin Multiplatform. Через событие init
будет получен сигнал от WebView о готовности компонента KMMAgent к работе. Теперь в приложении ОС Аврора есть возможность выполнять запросы к Wrapper. На главной странице приложения в компоненте QML ApplicationWindow нужно инициализировать KMMAgent, тогда его можно будет вызвать на всех дочерних страницах:
Файл <проект>/auroraApp/RSSReader/qml/RSSReader.qml
// Инициализация на странице приложения
KMMAgent {
id: agent
}
// Запуск асинхронной функции
agent.run(
"shared.Service.get.getAllFeeds()",
function(response) {
console.log(response)
},
function(error) {
console.log(error)
}
)
// Запуск синхронной функции
agent.run(
"return shared.Service.get.getAllFeeds()",
function(response) {
console.log(response)
},
function(error) {
console.log(error)
}
)
// Получение данных, которые могут содержать переменные KMP
agent.run(
"return shared.AppConstants.links.API_URL",
function(response) {
console.log(response)
},
function(error) {
console.log(error)
}
)
Wrapper
Взаимодействие Kotlin Multiplatform и QML обеспечивает библиотека Wrapper. В ней будет полный доступ к модулю Kotlin Multiplatform, что позволит подготовить данные для QML. Библиотеку нужно подключить в точку входа — файл index.html
, который будет загружен в QML-компонент KMMAgent
. В index.html
, расположенный в директории qml
приложения ОС Аврора, нужно подключить библиотеку Wrapper и отправить событие с информацией о том, что инициализация WebView успешно завершилась. Это позволит точно определить готовность WebView. Содержимое файла index.html
будет следующим:
Файл <проект>/auroraApp/RSSReader/qml/shared-js/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>KMP</title>
<script src="./dist/shared.js"></script>
</head>
<body>
<p id="demo"></p>
<script type="text/javascript">
function init() {
async function init() {
// Проверка работоспособности библиотеки
document.getElementById("demo").innerHTML = shared.Helper.randomUUID()
// Отправка события готовности
shared.Helper.sendEvent("init")
}
// Здесь можно добавить задержку отправки готовности
setTimeout(init, 0)
}
init()
</script>
</body>
</html>
Создать в корне проекта директорию для библиотеки Wrapper и добавить в неё webpack.config.js
для сборки.
Файл <проект>/auroraApp/wrapper/webpack.config.js
const path = require('path');
module.exports = {
entry: path.resolve(__dirname, 'src/index.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'shared.js',
library: 'shared',
libraryTarget: 'umd',
hashFunction: "sha256"
},
module: {
rules: [
{
test: /\.(js)$/,
exclude: /node_modules/,
use: ['babel-loader'],
},
],
},
resolve: {
extensions: ['.js'],
modules: [
path.resolve(__dirname, 'src'),
path.resolve(__dirname, 'node_modules')
],
},
mode: 'development',
devtool: 'sourceMap',
};
В папку JavaScript-библиотеки добавить package.json
и подключить нужные зависимости.
-
dependencies
- раздел для зависимостей realtime.uuid
- идентификатор для генерации уникальных ключей.shared
— путь к NPM-библиотеке KMP.
devDependencies
- раздел для зависимостей, необходимых для сборки.
В build
нужно добавить webpack с копированием нужных файлов в приложение ОС Аврора после сборки:
Файл <проект>/auroraApp/wrapper/package.json
{
"name": "kmm-wrapper",
"version": "0.0.1",
"scripts": {
"build": "webpack && cp -R dist ../qml/shared-js && cp index.html ../qml/shared-js",
"test": "jest"
},
"dependencies": {
"uuid": "^9.0.0",
"shared": "file:../../shared/build/packages/js"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"babel-eslint": "^10.0.2",
"babel-loader": "^8.0.6",
"eslint": "^6.1.0",
"webpack": "^4.46.0",
"webpack-cli": "^4.0.0-rc.1"
}
}
В директорию библиотеки Wrapper добавить папку src
, в которой будет находиться объединяющий JavaScript-код. В index.js
добавить экспорт нужных компонентов, к которым можно обратиться из QML.
Файл <проект>/auroraApp/wrapper/src/index.js
export * from './Helper';
export * from './Service';
Дальше c Wrapper можно работать как с обычной библиотекой JS, и вызывать все нужные функции в QML.
Объект Helper (вспомогательный класс) поможет создать уникальный ключ и отправит событие после ответа асинхронной функции Kotlin Multiplatform.
Файл <проект>/auroraApp/wrapper/src/Helper.js
import {v4 as uuidv4} from 'uuid';
export const Helper = {
// Создать случайный UUID
randomUUID: function () {
return uuidv4()
},
// Обертка с событием отправки после получения данных
request: function (fun, callback, delay) {
const caller = Helper.randomUUID()
setTimeout(async () => {
try {
Helper.sendEvent(caller, callback(await fun()))
} catch (e) {
Helper.sendEvent(caller, e)
}
}, delay)
return caller
},
// Отправить событие
sendEvent: function (caller, response) {
const customEvent = new CustomEvent("framescript:log", {
detail: {
response: response,
caller: caller
}
});
document.dispatchEvent(customEvent);
}
}
Helper упростит написание обёртки на асинхронные функции Kotlin Multiplatform. Код обёртки будет выглядеть следующим образом:
Файл <проект>/auroraApp/wrapper/src/Service.js
import shared from "shared";
import {Helper} from "./Helper";
const JsRssReader = new shared.com.github.jetbrains.rssreader.core.JsRssReader()
export const Service = {
get: {
getAllFeeds: function (forceUpdate = true) {
return Helper.request(async () => {
return await JsRssReader.getAllFeedsPromise(forceUpdate)
}, (response) => {
return response.toArray();
}, 0 /** задержка, если это необходимо **/)
},
}
}
Выполнить сборку библиотеки Wrapper:
npm run build
Собранную библиотеку Wrapper нужно добавить в папку qml
и подключить её в index.html
.
<head>
<script src="./dist/shared.js"></script>
</head>
В итоге получится три независимых проекта, объединенных в приложение на платформе ОС Аврора. Полный код проекта доступен по ссылке: KMM RSS Reader.
Оценка производительности
Для оценки производительности написаны одинаковые тесты для Android и ОС Аврора. Код тестов открыт и доступен для самостоятельного выполнения (Android, ОС Аврора). Тесты интеграционные выполняют функцию, доступную KMP-модулю с разным объемом данных. Функция делает запрос в сеть для получения данных и анализа ответа XML в модели, используемые на платформах. Тесты выполнялись по 5 раз, и был взят лучший результат.
Подробный отчет времени выполнения:
Size |
Items |
Emulator (Android) |
Emulator (Aurora) |
Xaomi (Android) |
NS220 (Aurora) |
NS220 (Android) |
SM-J106F (Android) |
INOI R7 (Aurora) |
---|---|---|---|---|---|---|---|---|
10M |
4922 |
2687ms |
1148ms |
8500ms |
8171ms |
28279ms |
24057ms |
12609ms |
5M |
2436 |
1556ms |
558ms |
5117ms |
3581ms |
16718ms |
12165ms |
5533ms |
1M |
485 |
846ms |
210ms |
1898ms |
1340ms |
6361ms |
3452ms |
2156ms |
100K |
48 |
127ms |
102ms |
352ms |
407ms |
428ms |
378ms |
514ms |
20K |
9 |
103ms |
78ms |
177ms |
220ms |
229ms |
153ms |
265ms |
PC
OS — Ubuntu 22.04
CPU — AMD 3950x
RAM — DDR4 / 2133 MHz / 56GiB
Xaomi A2
OS — Android 10
CPU — Qualcomm Snapdragon 660
RAM — 4GiB
NS220
OS — ОС Аврора 4.0.2.269 / Android 8.1.0
CPU — MediaTek MT8735A
RAM — 4GiB
SM-J106F
OS — Android 6.0.1
CPU — Spreadtrum SC9830I
RAM — 1GiB
INOI R7
OS — ОС Аврора 4.0.2.269
CPU — Qualcomm Snapdragon 212
RAM — 2GiB
Результат тестов показал, что Kotlin Multiplatform JavaScript в данной задаче быстрее, чем собранная JVM-библиотека для Android. Замеры производились на реальном функционале приложения. Kotlin Multiplatform выполняет свою задачу в ОС Аврора на «отлично».
Заключение
Реализация небольшой библиотеки JS позволяет соединить Kotlin Multiplatform с приложением, написанным на Qt/QML для ОС Аврора. Пользователь получает нативный интерфейс ОС Аврора и бизнес логику на Kotlin Multiplatform с минимальным использованием С++. Портировать приложения очень легко и скорость работы такой связки отличная.
Комментарии (4)
JajaComp
09.08.2023 15:46+1Спасибо за статью. Хотелось бы так же почитать про возможные подводные камни с которыми можно столкнуться. На первый взгляд непонятно как реализовывать кеширование и работу с БД, например SQLDelight.
ChPr
09.08.2023 15:46+1Зачем JS? Можно же собрать нативную либу и юзать ее из QT? Или Аврова не съест Linux либу?
dea
не хватает замеров по потреблению памяти