Kotlin Multiplatform в ОС Аврора
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 представляет возможное завершение (или сбой) асинхронной операции и ее результирующее значение.
Promise
Портируемое приложение будет обращаться в сеть для запроса XML-данных RSS. Так как запросы будут выполняться через WebView, следует отключить CORS.
CORS (Cross-origin resource sharing) — это механизм, который позволяет запрашивать ограниченные ресурсы на веб-странице из другого домена за пределами домена, из которого обслуживался первый ресурс.
Cross-Origin Resource Sharing (CORS)
Далее нужно создать скрипт 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
KMP
Создать в корне проекта директорию для библиотеки 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.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
.
В итоге получится три независимых проекта, объединенных в приложение на платформе ОС Аврора. Полный код проекта доступен по ссылке: KMM RSS Reader.
Оценка производительности
Для оценки производительности написаны одинаковые тесты для Android и ОС Аврора. Код тестов открыт и доступен для самостоятельного выполнения (Android, ОС Аврора). Тесты интеграционные выполняют функцию, доступную KMP-модулю с разным объемом данных. Функция делает запрос в сеть для получения данных и анализа ответа XML в модели, используемые на платформах. Тесты выполнялись по 5 раз, и был взят худший результат.
Test Android / ОС Аврора
Подробный отчет времени выполнения:
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
Xaomi A2
NS220
SM-J106F
INOI R7
Результат тестов показал, что Kotlin Multiplatform JavaScript в данной задаче быстрее, чем собранная JVM-библиотека для Android. Замеры производились на реальном функционале приложения. Kotlin Multiplatform выполняет свою задачу в ОС Аврора на «отлично».
Заключение
Реализация небольшой библиотеки JS позволяет соединить Kotlin Multiplatform с приложением, написанным на Qt/QML для ОС Аврора. Пользователь получает нативный интерфейс ОС Аврора и бизнес логику на Kotlin Multiplatform с минимальным использованием С++. Портировать приложения очень легко и скорость работы такой связки отличная.