Ускоряем отладку и прототипирование мобильных QML-приложений на живом устройстве

    Привет.

    Хочу поделиться простым способом оптимизации своего рабочего времени при разработке QML приложений под Android/iOS/Embedded.

    Возможно, сказанное мной кому-то покажется бояном, но пока про такой элементарный метод нигде не читал.

    Суть проблемы — при разработке, отладке или прототипировании любого мобильного приложения на любом языке мы как правило всегда проходим одни и те же этапы: правка кода, деплой, запуск. И так до бесконечности. В случае с мобильной разработкой, этап деплоя кода может растянуться на внушительное время — от 2 до 10 минут, в течение которого заниматься в общем-то нечем. Возможно для кого-то это и хорошо, но точно не для тех, кто ценит своё время. В общем, думаю, не только меня выбешивает такое положение вещей ;-)

    Ситуация усугубляется для нативных средств разработки, к примеру, под Android, где нам всегда, без вариантов нужно перекомпилировать Java-код перед следующим запуском на устройстве.

    Qt на первый взгляд обладает той же проблемой — каждый новый билд проекта также деплоится на устройство продолжительное время. Вот только есть одна особенность — мы ведь можем писать приложения не на Qt/C++, а на чистом QML. В этом случае, если мы не меняем логику в C++ части приложения, нам ничего не нужно компилировать под целевую платформу. А значит — было бы круто просто обновлять набор qml файлов приложения и перезапускать приложение на устройстве. Ведь экономия времени на 10 запусков составила бы не менее часа!

    Ну что ж, раз есть потенциальная возможность — грех ею не воспользоваться. Читайте под катом, что у меня получилось.

    Начав рыть информацию на тему, я во первых наткнулся на когда-то подававший надежды проект QML Live Preview (статья на хабре), который позволял разработчикам запускать режим отладки в Qt Creator, на лету изменять код и видеть свои правки на устройстве. Это было волшебство и то что мне нужно, вот только данный модуль Qt Creator-а исключили из общей кодовой базы (ссылка на коммит). Может быть Digia что-то мутит и перенесла его в коммерческую версию, а может он был нестабилен. Не знаю. Главное — что его больше нет.

    В общем пришлось искать обходной путь. Он нашёлся в на странице документации Qt, где рассказывается о такой теме, как сетевая прозрачность при загрузке ресурсов. Идея в том, что Qt и QML приложение пофигу, откуда загружать QML файлы и ресурсы типа шрифтов, изображений и прочего. Источником может быть локальный диск, ресурсы Qt (qrc), smb-шара или http-сервер.

    Идея реализации возникла мгновенно! Разворачиваем на локальной машине HTTP-сервер, корневой директорий которого будет наш проект, меняем сценарий загрузки ресурсов в C++ части приложения, проверяем работоспособность на десктопе, собирает под мобилку, проверяем работоспособность на мобилке, радуемся!

    Полное описание алгоритма:
    1) Убеждаемся что наш ПК и устройство находятся в одной локальной сети.

    2) Приводим проект к требованиям Qt Network Transparent и определённому шаблону:
    — все ресурсы в каталоге /assets;
    — все qml файлы в каталоге /qml;
    — внутри каталога qml структура файлов каталогов должна быть максимум двухуровневой;
    — все включения типа import «dir» приводим к виду import «dir» as Dir;
    — все обращения к компонентам в поддиректориях приводим от вида MyComp {… } к виду Dir.MyComp {… };

    3) Скачиваем, настраиваем и запускаем nginx. В настройках по умолчанию нужно поменять всего два параметра — порт и root каталог. Затем запускаем nginx. Пример:
    server {
            listen       8085;
    ...
    location / {
                root   D:/Dropbox/work/anyway;
    

    4) В файле main.cpp в месте загрузке qml-файлов меняем логику на следующую:
        bool qmlDebug = true;
    
        QString url = "http://192.168.1.22:8085/";
        QString qmlLoadPrefix;
        QString assetsLoadPrefix;
    
        QNetworkAccessManager NAManager;
        QUrl qurl (url+"qml/main.qml");
        QNetworkRequest request(qurl);
        QNetworkReply *reply = NAManager.get(request);
        QEventLoop eventLoop;
        QObject::connect(reply, SIGNAL(finished()), &eventLoop, SLOT(quit()));
        eventLoop.exec();
        if (reply->error() != QNetworkReply::NoError) {
            qmlDebug = false;
            qDebug() << "Error loading qml by network. Load from resources!";
        }
    
        if (qmlDebug) {
            qmlLoadPrefix = url+"qml/";
            assetsLoadPrefix = url+"assets/";
        } else {
            qmlLoadPrefix = "qrc:/";
            assetsLoadPrefix = "qrc:/";
        }
    
        engine.rootContext()->setContextProperty("qmlLoadPrefix", qmlLoadPrefix);
        engine.rootContext()->setContextProperty("assetsLoadPrefix", assetsLoadPrefix);
    
        engine.load(QUrl(qmlLoadPrefix+"main.qml"));
    

    Здесь мы задаём URL нашего ПК в локальной сети, а также две переменные qmlLoadPrefix и assetsLoadPrefix. Как не сложно догадаться это просто префиксы для доступа к ресурсам и qml-файлам приложения в зависимости от текущей конфигурации приложения. Так во время активной фазы разработки мы сможем быстро загружать требуемые ресурсы через http, а при выпуске приложения — загружать всё из файлов ресурсов самого приложения.

    5) Ну и последний шаг — меняем все вхождения типа «qrc://myImg.png» на assetsLoadPrefix+«myImg.png» во всех qml файлах проекта.

    Вот собственно и всё. Осталось отметить, что теперь запускать приложение для отладки нужно не с помощью большой зелёной кнопки «Play» на левой панели инструментов Qt Creator, а используя маленькую кнопку «Play» на нижней панели инструментов в разделе «Вывод приложения». Правда, эта кнопка будет недоступна сразу после запуска Qt Creator и нужно будет запустить приложение впервые по старинке, но затем достаточно пользоваться только ею. При этом в лог Qt Creator всё также будут сыпаться все необходимые debug сообщения, а если Вам понадобится полноценная отладка, то и она будет работать в штатном режиме.

    Конечно, это не ахти какой хак и он всё равно не спасёт от пересборки приложения при написании C++ кода, но даже в таком виде мы получаем гораздо больше удобства при разработке приложений под любую из платформ.

    Предлагаю в комментариях поделиться своими мыслями и предложениями по поводу ещё большей оптимизации процесса разработки Qt-приложений и их преимущества перед нативными приложениями для iOS и Android.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 15
    • 0
      А почему, собственно, не отлаживать чисто QML код на десктопе, вообще без заливки куда-либо?
      • +1
        Есть ряд задач, решать которые можно только на устройстве:
        — позиционирование и масштабирование элементов на нескольких устройствах с разными разрешениями и размерами экранов;
        — прототипирование пользовательского интерфейса в нескольких вариантах для сравнения юзабилити на реальных устройствах;
        — вызов платформозависимого кода, недоступного в Windows. Например то, что раньше называлось Qt Mobility — доступ к местоположению, сенсорам (гироскоп, акселерометр и другие), блютуз, NFC.
        — тестирование своих библиотек. Например я сейчас пилю новую версию своей библиотеки AdCtl (https://github.com/kafeg/adctl) где будет возможность авторизации через GPlay и привязка к рейтингам игроков и ачивкам.

        Это то что вспомнилось сразу. Наверняка у каждого девелопера есть ещё куча причин тестировать функционал на конечном устройстве.
        • 0
          Ещё ссылочка в тему сетевой прозрачности: www.slideshare.net/jeremylaine/serving-qml-applications-over-the-network
          • 0
            — позиционирование и масштабирование элементов на нескольких устройствах с разными разрешениями и размерами экранов;
            Доступно на десктопе
            — вызов платформозависимого кода, недоступного в Windows
            Отлаживаем платформозависимый код — значит нужна перекомпиляция. Если он уже отлажен, значит интерфейс пишем на десктопе.

            2 пункт спорный, 4 схож с третьим.

            Но, тем не менее, статья полезная, наверняка кому-нибудь пригодится.
            • 0
              — позиционирование и масштабирование элементов на нескольких устройствах с разными разрешениями и размерами экранов;

              К сожалениею не всё так хорошо в Qt с масштабированием, чтобы не заморачиваться тестированием UI на конечном устройстве. К тому же я например не знаю как на десктопе симулировать различные dpi/ppi.

              — вызов платформозависимого кода, недоступного в Windows

              Пример — библиотека использующая платформозависимый код уже готова и представлена QML-плагином. В этом случае перекомпиляция не нужна, но чтобы настроить связку с библиотекой может потребоваться несколько перезапусков приложения.
              • 0
                К тому же я например не знаю как на десктопе симулировать различные dpi/ppi.

                Симулировать нельзя, зато можно обеспечить одинаковый размер элементов на всех девайсах вне зависимости от dpi/ppi — хоть в миллиметрах, хоть в пикселах.
                • 0
                  Научите как правильно, перепробовал кучу способов =)
                  • 0
                    Если детально, то читать вот по этой ссылке статьи снизу-вверх.

                    Если кратко, то вот код:
                    #ifdef Q_OS_ANDROID
                        //  BUG with dpi on some androids: https://bugreports.qt-project.org/browse/QTBUG-35701
                        //  Workaround:
                        QAndroidJniObject qtActivity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative", "activity", "()Landroid/app/Activity;");
                        QAndroidJniObject resources = qtActivity.callObjectMethod("getResources", "()Landroid/content/res/Resources;");
                        QAndroidJniObject displayMetrics = resources.callObjectMethod("getDisplayMetrics", "()Landroid/util/DisplayMetrics;");
                        int density = displayMetrics.getField<int>("densityDpi");
                    #else
                        QScreen *screen = qApp->primaryScreen();
                        float density = screen->physicalDotsPerInch();
                    #endif
                    
                        engine.rootContext()->setContextProperty("mm",density / 25.4);
                        engine.rootContext()->setContextProperty("pt", 1);
                    
                        double scale = density < 180 ? 1 :
                                       density < 270 ? 1.5 :
                                       density < 360 ? 2 : 3;
                        engine.rootContext()->setContextProperty("dp", scale);
                    

                    Тут размер в миллиметрах полностью надежен, в девайсонезависимых пикселах приблизителен, как и на Android (но надо бы потестить на соответствующих девайсах), pt для единообразия при указании размеров шрифта.

                    Использвать очень просто — умножаем размеры в любом месте QML кода:
                    Rectangle {
                        height: 20*mm;
                        width: 40*dp;
                    }
                    

                    Все собираюсь написать статью по таким базовым вещам, но руки пока не доходят :/
      • 0
        Заодно это выявит все места неправильного использования createComponent (где не учитывается что объект может быть не доступен сразу после вызова).
        Единственное — полные имена файлов лучше заменить на относительные чтобы не носиться с assetsLoadPrefix+ по всему проекту
        • 0
          Вот кстати да, что касается createComponent, вместо неправильного использования
          if (myComponent.status == Component.Ready) { ... } 
          

          сразу после создания компонента, точно придётся переписывать эту логику на использование асинхронного вызова
          myComponent.statusChanged.connect(function() { ... })
          

          Также выяснится, что вызвать этот метод можно лишь один раз, а дальше от него нужно дёргать createObject необходимое количество раз.
          • 0
            Хм, вроде всё-таки правильное использование это

            var myComponent = Qt.createComponent(url);
            if ( myComponent.status == Component.Ready) {
            callback();
            } else {
            myComponent.statusChanged.connect(callback);
            }
          • 0
            If you are certain the QML file to be loaded is a local file, you could omit the finishCreation() function and call createObject() immediately
            Так что использование вполне себе правильное.
            • 0
              Согласен, здесь не прав я, а делать нужно так как в комментарии выше.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.