между ужасом быть человеком и чудом быть человеком.
«Путешествие в Икстлан»
Мой путь воина – брутального frontend-разработчик на «плюсах» – состоял в том, чтобы найти равновесие между разработкой приложения, работающего на Sailfish, и кроссплатформенного приложения.
С недавних пор я тружусь на позиции разработчика в компании Digital Design, и иногда мне приходится разбираться с задачами, с которыми я ранее не встречалась. Это интересно и часто весело. Сейчас, например, я пишу корпоративное приложение под Sailfish OS и хочу поделиться с вами своим опытом – об этом и пойдет речь ниже. Следуйте за мной под кат, если вы начинающий разработчик или, так же, как и я, столкнулись с задачей адаптации корпоративного приложения под ОС Sailfish и не знаете, с чего начать, а также те, кто ещё не слышал о Qt и особенностях Sailfish.
Немного предыстории, или зачем я это сделала
Когда компания 5 лет назад разрабатывала решение ИСКО «Ареопад» под iPad (iOS), никто и подумать не мог, что заказчикам потребуется приложение для работы на устройстве с ОС Sailfish (тогда ОС принадлежала финской компании Jolla, впрочем, сейчас тоже). Но в 2016 году российская компания «Открытая мобильная платформа» приобрела лицензию на её использование, модифицирование, распространение и довела её до ума: приспособила под отечественные мобильные устройства и пользователей (в основном компании и госучреждения) и зарегистрировала Sailfish Mobile OS RUS в реестре. И сейчас это, можно сказать, единственная официальная российская операционная система для мобильных устройств.
Естественно, нам захотелось в ней разобраться – остальные ОС мы уже покорили, а у Sailfish, судя по всему, большое будущее (да, это я про импортозамещение). Из этого всего следует, что приложению для Sailfish быть! В качестве первого испытуемого объекта выступило мобильное приложение «Информационная система для работы коллегиальных органов «Ареопад» (оно как раз находилось на стадии модернизации).
Теперь о Qt
Замечательный кроссплатформенный инструмент, включающий в себя и среду разработки, и тонну полезных и удобных библиотек, и даже свой язык-разметку QML. Я не буду на нём долго останавливаться, потому что об этом чуде написано множество статей на Хабре, а ещё можно почитать документацию, которая читается легко и непринужденно.
На всякий случай уточню, что вычисления и отображение происходят на разных языках в разных модулях (С++ и QML), взаимодействующих между собой специальными хитрыми методами (property, signal-slot).
А ещё Qt – это основной фреймворк, используемый при разработке ПО для Sailfish.
Подходы к решению задачи: налево пойдёшь – костыль наживешь, направо пойдёшь – фичи потеряешь
Поначалу, окрылённая идеей кроссплатформенности и тем, что буду её воплощать, я подумала, что приложение, написанное для Sailfish, не взлетит, а радостно вспорхнёт на любую другую ОС, да хоть на холодильник или чайник. Но счастье моё длилось ровно до того момента, пока я не села запускать его на Android – тут мне стало грустно. Sailfish под это дело ну совсем не приспособлен и менять определённо ничего не намерен.
Если Android или iOS позволяют использовать условную компиляцию и предоставляют для этого какой-то набор инструментов, то Sailfish невозможно отличить от Linux (в рамках Qt, конечно).
Далее возникает проблема совместимости QML: в Sailfish устаревшая версия Qt Quick (2.6 для Sailfish и уже 2.11 для всех остальных), Android не умеет Silica, Sailfish не умеет QtQuick.Controls. Поэтому для разных ОС приходится тянуть одинаковые компоненты из разных библиотек, что плодит почти одинаковые файлы для каждой ОС.
При этом Silica имеет, вероятно, всё те же возможности, что и QtQuick.Controls, но отказывается запускаться на любой другой платформе.
Особенности Sailfish
Будучи
Однако не могу не отметить, что в версии Sailfish определённо есть интересные вещи. Конечно, за такой короткий срок, что я с ним знакома, познать весь богатый внутренний мир Sailfish практически невозможно – наши с ним отношения находятся на стадии «конфетно-букетного периода», и много чего в его сложном характере мною ещё не изучено. К тому же решено было рисовать универсальное приложение для всех ОС, поэтому использовать более замысловатые механизмы попользоваться шанса так и не выпало, но про парочку интересных моментов, пожалуй, расскажу.
Речь пойдет только про одну библиотеку для реализации графического интерфейса Silica, ибо остальное, что я успела узнать и понять, ничем не отличается от стандартного программирования на QML.
- Theme.
Этот инструмент меня заинтересовал больше всего, потому что аналога в других ОС я не встречала, а ещё он несложный. Суть в том, что дизайн приложений не является константой, а зависит от выбранной темы.
Элемент Theme содержит в себе информацию о выбранной теме на данный момент: основные цвета, размеры шрифтов, отступов и компонентов. Таким образом, при написании приложений не нужно хардкодить цвета, подгонять размеры под размер экрана, да и вообще на макет смотреть (да простят меня дизайнеры, у вас и без того работы много), можно просто подтянуть это все из элемента Theme. Ну разве не круто!
- Menu.
Тоже фича Sailfish (хотя, может так и в Android можно, но я не видела).
Есть три вида меню:
PullDownMenu — PushUpMenu — ContextMenu
Что касается Silica…View – они ничем от стандартных …View не отличаются (± пара не особо заметных свойств), а компонент Button заставляет изрядно понервничать – высоту кнопки нельзя изменить, приходится везде писать Rectangle и MouseArea. Если бы я делала приложение только для Sailfish, используя стандартный дизайн этой ОС, менять высоту кнопки не было бы необходимости или можно было бы использовать компонент из Silica. Но поскольку я пишу приложение для всех устройств, то решено было использовать самый массовый дизайн (Android), чтобы всем было привычно и удобно. Многие со мной не согласятся и скажут, что нужно использовать нативные элементы UI для каждой платформы, но это совсем другая тема, она является предметом холиваров уже на протяжении долгих лет, за это время выросло не одно поколение разработчиков.
Ложка дёгтя к мобильной разработке на Qt
- Ассинхронщина
В Qt для общения с сервером вызова одной функции недостаточно, нужно отправить запрос и попросить специальный объект, отвечающий за ответ сервера, вызвать функцию обработки ответа (которую, конечно, пишем сами), когда ответ будет получен:
void foo()
{
QNetworkRequest request("http://www.leningrad.spb.ru");
QNetworkReply *reply = mngr->get(request);
connect(reply, &QNetworkReply::finished, this, &Class:: onReplyFinished);
}
void
onReplyFinished ()
{
QNetworkReply *reply = (QNetworkReply*)sender();
QByteArray ans = reply->readAll();
qDebug() << ans;
}
Функция onReplyFinished будет вызвана в момент получения ответа от сервера, а это может занять неизвестно сколько времени – зависит от качества соединения и серверной части. Теперь представьте, как это все будет организовано при 1500-2000 запросах, которые при этом друг от друга зависят и должны запускаться в порядке строгой очереди. А если вспомнить, что от ответа зависит вся UI часть, то совсем страшно становится.
- Костыльно-ориентированное программирование, или креативный подход к решению проблем
Другая тема – ListView, это совсем локальная проблема Qt, но всё же неприятная.
У любого уважающего себя ListView обязан быть компонент header, который в качестве первого по счёту делегата вставляет какой-то принципиально отличающийся от остальных элемент. Да, он это делает, да, всё прекрасно рисует, если бы не одно «но»: у меня делегат занимает весь экран, и его надо листать, но как только я пытаюсь с нулевого индекса листнуть влево к хедеру – он принципиально отказывается этот самый хэдер показывать. Что с этим делать? Затыкать костылем. Кусок моего кода формирования модели для данного ListView в cpp части, максимально точно описывающий происходящее:
foreach (QVariant quest, QQuestions)
{
Questions.append(new Question(client, quest));
}
kostilQuestions = Questions;
if (Questions.size() > 0)
kostilQuestions.prepend(Questions[0]);
Далее в делегате создаются 2 независимых айтема, видимость которых зависит от индекса.
При индексе равном нулю видим один, иначе – другой.
Итог
В результате, я получила приложение, которое «летает» как на Android и iOS, так и на Sailfish, точнее гибкое приложение, которое под эти ОС адаптируется. И магия тут ни при чем. Я нашла способ избежать конфликта и подружить все ОС c Sailfish путём подстановки некоторых файлов для Sailfish и всех остальных. Это позволило в коде приложения выделить слои пользовательского интерфейса под каждую ОС.
Можно считать, что первый шаг мы сделали, – подружили приложение с Sailfish. Но работа на этом не завершена, мы с Sailfish выходим на новый уровень наших отношений – готовим приложение ИСКО «Ареопад» к регистрации в реестре (как приложение на Sailfish), а это значит, что нам нужно будет адаптировать его ещё и для реестра.
Надеюсь, было интересно. Продолжение следует.
Комментарии (15)
minlexx
20.08.2018 11:08Я нашла способ избежать конфликта и подружить все ОС c Sailfish путём подстановки некоторых файлов для Sailfish и всех остальных. Это позволило в коде приложения выделить слои пользовательского интерфейса под каждую ОС.
Мне не хватает подробностей по этому пункту!nastika Автор
20.08.2018 12:07Разные main.cpp и ApplicationWindow для Sailfish и остальных. При этом ApplicationWindow Практически копипаста, если не считать файлы, которые все-таки пришлось с помощью Silica или QtQuick.Controls писать
dmitry_dvm
20.08.2018 12:14Этот инструмент меня заинтересовал больше всего, потому что аналога в других ОС я не встречала
В UWP темы искаропки.
Taraflex
20.08.2018 21:36+1В моменте про ассинхронность.
Попробуйте библиотеку github.com/simonbrunel/qtpromise
Позволяет реорганизовать код в стиле
Говнокод из реального проектаQPromise<void> IEXApi::post(const QByteArray &payload){ return QPromise<void>([=](auto resolve, auto reject) { closeReply(); QNetworkRequest req("https://ws-api.iextrading.com/socket.io/?EIO=3&transport=polling&sid=" + sid); req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); req.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain;charset=UTF-8"); req.setHeader(QNetworkRequest::ContentLengthHeader, QByteArray::number(payload.size())); req.setRawHeader(":scheme", "https"); req.setRawHeader(":path", ("/socket.io/?EIO=3&transport=polling&sid=" + sid).toLatin1()); req.setRawHeader(":authority", "ws-api.iextrading.com"); req.setRawHeader(":method", "POST"); reply = nmanager->post(req, payload); connect(reply, &QNetworkReply::finished, [=](){ if(reply->error() == QNetworkReply::NoError){ QString data = reply->readAll(); if(data.trimmed() == "ok"){ return resolve(); } } reject(reply->error()); }); }); } QPromise<QByteArray> IEXApi::get(){ return QPromise<QByteArray>([=](auto resolve, auto reject) { closeReply(); QNetworkRequest req; if(sid.isEmpty()){ req.setUrl(QString("https://ws-api.iextrading.com/socket.io/?EIO=3&transport=polling")); }else{ req.setUrl("https://ws-api.iextrading.com/socket.io/?EIO=3&transport=polling&sid=" + sid); } req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); req.setRawHeader(":scheme", "https"); if(sid.isEmpty()){ req.setRawHeader(":path", "/socket.io/?EIO=3&transport=polling"); }else{ req.setRawHeader(":path", ("/socket.io/?EIO=3&transport=polling&sid=" + sid).toLatin1()); } req.setRawHeader(":authority", "ws-api.iextrading.com"); req.setRawHeader(":method", "GET"); reply = nmanager->get(req); connect(reply, &QNetworkReply::finished, [=](){ if(reply->error() == QNetworkReply::NoError){ if(reply->bytesAvailable() > 0){ return resolve(reply->readAll()); } } reject(reply->error()); }); }); } void IEXApi::open(){ close(); if(!nmanager){ nmanager = new QNetworkAccessManager(this); }else if(nmanager->networkAccessible() != QNetworkAccessManager::Accessible){ //если изменились настройки соединения, пересоздадим QNetworkAccessManager nmanager->deleteLater(); //delete nmanager; nmanager = new QNetworkAccessManager(this); } get().then([=](const QByteArray &info){ int i = 0; while(i < info.size() && info.at(i) != '{'){ ++i; } auto doc = QJsonDocument::fromJson(QByteArray::fromRawData(info.constData() + i, info.size() - i)).object(); sid = doc.value("sid").toString(); pingTimer.setInterval(doc.value("pingInterval").toInt()); }).then([=](){ return post(R"(12:40/1.0/tops,)"); }).then([=](){ return get(); }).then([=](const QByteArray &){ closeSocket(); socket.open("wss://ws-api.iextrading.com/socket.io/?EIO=3&transport=websocket&sid=" + sid); /*}).then([=](){ return get(); }).then([=](const QByteArray &){*/ }).fail([=](QNetworkReply::NetworkError v){ iexWarning << "NetworkError: " << v; openDelayed(); }).fail([=](){ iexWarning << "Uncaught Exception"; openDelayed(); }); }
kITerE
20.08.2018 22:24написала свой шедевр на чистой Qt, без использования QtQuick.Controls и Silica
Интересно было бы взглянуть как это выглядит на Sailfish. Может добавите gif-демо такого приложения в статью?
nastika Автор
21.08.2018 12:12Попробую об этом подумать. Но визуально оно совершенно не отличается от стандартных приложений на андроиде, разве что чуть больше косяков в дизайне (но это пока) :)
kITerE
21.08.2018 13:05Эмулируемые Android-приложения на Sailfish получают внизу стандартную кнопку назад. Нативные Sailfish-приложения обходятся без нее: используются swipe или навигацию между последовательными страницами. Интересно насколько удобно для пользователя чистое Qt приложением, если вообще не используется Silica.
Продолжение следует.
Если есть возможность, выложите (на github например), минимальный скелет такого приложения (с разными контроллами, наполненными тестовыми данными), что бы иметь возможность его повертеть в руках.
nastika Автор
21.08.2018 13:19+1Эмулируемые Android-приложения на Sailfish получают внизу стандартную кнопку назад. Нативные Sailfish-приложения обходятся без нее: используются swipe или навигацию между последовательными страницами. Интересно насколько удобно для пользователя чистое Qt приложением, если вообще не используется Silica.
Действительно, с этим есть определенные проблемы. Например, горизонтальное перелистывание айтемов в ListView на всю страницу в Sailfish — не самая удачная идея, хотя на андроиде так делать можно. Все-таки это приложение — прототип, попытки пощупать новую платформу. Естественно, все эти грабли в дальнейшем будут учтены. Другое дело, что нативный интерфейс Sailfish слишком сильно отличается от стандартных Android и ios, так что совместить в себе все три платформы — задача очень нетривиальная :)
Если есть возможность, выложите (на github например), минимальный скелет такого приложения (с разными контроллами, наполненными тестовыми данными), что бы иметь возможность его повертеть в руках.
Да, отличная идея для следующей статьи, думаю, кому-то будет интересно потрогать этого Франкенштейна :)
aa13q
Спасибо за публикацию!
Полностью согласен насчет серьезной проблемы отсутствия QQC2.
Но менять ситуацию намерен: описывал эту же проблему у себя в бложике.
И стараюсь по мере сил добиваться появления QQC2 на любимой платформе.
nastika Автор
Было бы замечательно!
Tantrido
Это ты был в рассылке Qt? :) Без, пока, заинтересованности со стороны Qt — получится?
aa13q
Я :) Пытался в рамках GSoC от Qt получить такую задачу.
Как справедливо заметил Александ Акулич в той же рассылке (который внес пользы гораздо больше, чем я), это задача больше для Sailfish, чем для Qt.
А, ответ от Qt в лице J-P Nurmi был вполне положительный насчет помочь всем необходимым.
Так что нужно решать на стороне Sailifsh, а в Qt помогут.
Tantrido
Ну держи в курсе :)