Искусство воина состоит в сохранении равновесия
между ужасом быть человеком и чудом быть человеком.
«Путешествие в Икстлан»


Мой путь воина – брутального 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


Будучи ленивой максимально креативной творческой личностью со стремлением оптимизировать трудозатраты, я пошла по пути наименьшего сопротивления функционала и написала свой шедевр на чистой Qt, без использования QtQuick.Controls и Silica. В итоге, на Sailfish всё работает чинно и благородно, а вот на Android половина элементов, несмотря на мой строгий приказ сидеть на месте, разъехались кто куда по своим наверняка важным делам, так что следующим шагом будет всё-таки сделать по-взрослому и развести файлы разметки для каждой ОС по разным углам. [Что я и впоследствии и сделала, пока готовила статью к выходу в свет. Эта история тянет на отдельную статью]. Прошу запомнить и принять: магические константы – зло (например, width:15). И на двух почти идентичных девайсах даже с одной ОС они могут отличаться в 2 раза.

Однако не могу не отметить, что в версии 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)


  1. aa13q
    20.08.2018 11:08

    Спасибо за публикацию!
    Полностью согласен насчет серьезной проблемы отсутствия QQC2.
    Но менять ситуацию намерен: описывал эту же проблему у себя в бложике.
    И стараюсь по мере сил добиваться появления QQC2 на любимой платформе.


    1. nastika Автор
      20.08.2018 12:08

      Было бы замечательно!


    1. Tantrido
      21.08.2018 03:37

      Это ты был в рассылке Qt? :) Без, пока, заинтересованности со стороны Qt — получится?


      1. aa13q
        21.08.2018 10:55

        Я :) Пытался в рамках GSoC от Qt получить такую задачу.
        Как справедливо заметил Александ Акулич в той же рассылке (который внес пользы гораздо больше, чем я), это задача больше для Sailfish, чем для Qt.
        А, ответ от Qt в лице J-P Nurmi был вполне положительный насчет помочь всем необходимым.
        Так что нужно решать на стороне Sailifsh, а в Qt помогут.


        1. Tantrido
          21.08.2018 16:06

          Ну держи в курсе :)


  1. minlexx
    20.08.2018 11:08

    Я нашла способ избежать конфликта и подружить все ОС c Sailfish путём подстановки некоторых файлов для Sailfish и всех остальных. Это позволило в коде приложения выделить слои пользовательского интерфейса под каждую ОС.

    Мне не хватает подробностей по этому пункту!


    1. nastika Автор
      20.08.2018 12:07

      Разные main.cpp и ApplicationWindow для Sailfish и остальных. При этом ApplicationWindow Практически копипаста, если не считать файлы, которые все-таки пришлось с помощью Silica или QtQuick.Controls писать


  1. dmitry_dvm
    20.08.2018 12:14

    Этот инструмент меня заинтересовал больше всего, потому что аналога в других ОС я не встречала

    В UWP темы искаропки.


  1. 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();
        });
    }
    


  1. kITerE
    20.08.2018 22:24

    написала свой шедевр на чистой Qt, без использования QtQuick.Controls и Silica

    Интересно было бы взглянуть как это выглядит на Sailfish. Может добавите gif-демо такого приложения в статью?


    1. nastika Автор
      21.08.2018 12:12

      Попробую об этом подумать. Но визуально оно совершенно не отличается от стандартных приложений на андроиде, разве что чуть больше косяков в дизайне (но это пока) :)


      1. kITerE
        21.08.2018 13:05

        Эмулируемые Android-приложения на Sailfish получают внизу стандартную кнопку назад. Нативные Sailfish-приложения обходятся без нее: используются swipe или навигацию между последовательными страницами. Интересно насколько удобно для пользователя чистое Qt приложением, если вообще не используется Silica.


        Продолжение следует.

        Если есть возможность, выложите (на github например), минимальный скелет такого приложения (с разными контроллами, наполненными тестовыми данными), что бы иметь возможность его повертеть в руках.


        1. nastika Автор
          21.08.2018 13:19
          +1

          Эмулируемые Android-приложения на Sailfish получают внизу стандартную кнопку назад. Нативные Sailfish-приложения обходятся без нее: используются swipe или навигацию между последовательными страницами. Интересно насколько удобно для пользователя чистое Qt приложением, если вообще не используется Silica.


          Действительно, с этим есть определенные проблемы. Например, горизонтальное перелистывание айтемов в ListView на всю страницу в Sailfish — не самая удачная идея, хотя на андроиде так делать можно. Все-таки это приложение — прототип, попытки пощупать новую платформу. Естественно, все эти грабли в дальнейшем будут учтены. Другое дело, что нативный интерфейс Sailfish слишком сильно отличается от стандартных Android и ios, так что совместить в себе все три платформы — задача очень нетривиальная :)

          Если есть возможность, выложите (на github например), минимальный скелет такого приложения (с разными контроллами, наполненными тестовыми данными), что бы иметь возможность его повертеть в руках.

          Да, отличная идея для следующей статьи, думаю, кому-то будет интересно потрогать этого Франкенштейна :)


  1. monah_tuk
    21.08.2018 06:07

    Сразу вопрос: какой аппарат с Sailfish использовали? :)


    1. nastika Автор
      21.08.2018 12:13

      Для Sailfish inoi R7 и T8, еще несколько андроидов, айпад и айфон)