12 октября 2016 в 13:22

Разработка для Sailfish OS: архитектура FLUX в QML на примере приложения для запоминания литературных терминов из песочницы

Всем доброго времени суток! В данной статье хотелось бы рассказать, как мы разработали своё первое приложение для платформы Sailfish OS (о разработке под которую уже был ряд статей).



Задачей было написать приложение, с помощью которого можно было бы изучать и запоминать литературные термины. Так как реализовать обычный словарь с толкованием слов слишком просто и скучно, то было принято решение: организовать процесс обучения через взаимодействие с пользователем. Рассмотрев все доступные варианты построения взаимодействия с пользователем, было решено сделать обучение в виде тестов.

Поскольку словарь представляет собой список терминов, для каждого из которых дано определение, было решено выделить три типа вопросов для тестов. В первом типе пользователю показывается сам термин и четыре варианта определений терминов из которых он должен выбрать одно верное. Второй тип вопросов эквивалентен первому, с той лишь разницей, что пользователю уже показано одно определение и четыре термина. Пользователь должен выбрать термин, соответствующий данному определению. Наконец, последний тип вопросов — с открытым ответом. Пользователю показывается определение и ему необходимо самому написать слово, соответствующее данному определению.

Кроме описанного выше процесса обучения, необходимо было реализовать просмотр списка терминов, которым мы будем учить, чтобы пользователь мог знать, что же все-таки с него будут спрашивать. Так же было запланировано отслеживание и сохранение прогресса в изучении, с целью показать пользователю, каких успехов он достиг в изучении слов.

Особенности приложения


Собственно сам словарь был любезно предоставлен нам сотрудниками кафедры иностранных языков нашего ВУЗа (ЯрГУ им. П. Г. Демидова). Он был в обычном текстовом виде, поэтому для удобства использования мы перенесли его в xml формат. Получился xml документ состоящий из элементов вида:

<term>
    <name>
      <text>Epenalepsis</text>
    </name>
    <synonym>
      <text>Polysyndeton</text>
      <transcription>[ˌpɒlɪˈsɪndɪtən]</transcription>
    </synonym>
    <description>Use of several conjunctions</description>
    <context>He thought, and thought, and thought…</context>
</term>

Загружается такой словарь очень легко — с помощью стандартного компонента XmlListModel.

В качестве архитектуры приложения была выбрана продвигаемая корпорацией Facebook архитектура «Flux». Про саму архитектуру было уже много написано статей. Довольно интересные и понятные переводы доступны на Хабре: тут и тут. Так же при разработке мы руководствовались статьей об использовании Flux при написании QML приложений. Рекомендуем статью всем кто пишет приложения на QML (не обязательно даже мобильные). Описывать все эти моменты здесь излишне, поскольку вся информация доступна по приведённым выше ссылкам и описана она там очень хорошо. Поэтому напишем лишь, как архитектура Flux использовалась в нашем приложении.

С View все понятно – каждая страница приложения является частью View. Переход между страницами осуществляется с помощью Actions. В нашем случае за переход отвечает Action navigateTo.

AppListener {
    filter: ActionTypes.navigateTo

    onDispatched: {
            pageStack.push(Qt.resolvedUrl("../sailfish-only/views/pages/" + message.url));
    }
}

Для хранения значений, а также для реализации функций используются два Store. Один (мы назвали его TermInformationStore) отвечает за отдельный текущий термин. В нем содержится информация о термине: само слово, его транскрипция, значение, пример использования и синонимы к нему. В этом же Store происходит заполнение свойств, содержащих вышеперечисленную информацию.

Второй Store — TestStore — отвечает за процесс тестирования и прогресс в изучении слов. В нем содержится информация о текущем вопросе теста. Соответственно, здесь эти вопросы и составляются, и здесь же рассчитывается прогресс.

Чтобы разделить работу с данными и организацию взаимосвязи частей приложения был создан элемент Script, который отвечает за получение сигналов от View и вызов функций из Store в верном порядке, что решает проблему с вызовом новых действий, когда старые еще не завершились. Также этот элемент содержит в себе всю логику по перемещению между различными экранами приложения.

Реализованный функционал


Поскольку это было наше первое приложение для данной платформы, да и на QML вообще, то сначала мы конечно же взялись за самое простое — список терминов. Сам список реализован с помощью SilicaListView, в которую подгружается список терминов из XmlListModel (как было описано чуть выше). Вообще, это самый обычный список, а поскольку создание списков — это один из самых базовых и распространённых примеров для QML в общем, да и для Sailfish OS в частности, то и заострять внимание мы на данном моменте не будем.

При нажатии на элемент списка открывается страница с подробным описанием термина. Поскольку мы для приложения решили использовать архитектуру Flux, то процесс открытия данной страницы выглядит несколько необычно, по сравнению с MVC или MVVM. При нажатии на элемент списка создается Action с информацией об индексе нажатого элемента. Данный Action провоцирует TermInformationStore изменить информацию о текущем термине в зависимости от выбранного индекса элемента списка, а затем открывают страницу с описанием. Выглядит она достаточно просто:


Тестирование можно начать с главного экрана. Всего в тесте 20 вопросов по неповторяющимся терминам выбираемым случайным образом. Сам тип вопроса (как было описано в начале — у нас их три) и неправильные ответы (если они должны быть в данном типе вопроса) так же подбираются случайным образом. Как уже было сказано выше, за всю логику составления вопросов отвечает TestStore. Вопрос создается следующим образом:

function makeQuestion(index, type) {
    options = [];
    var element = dictionary.get(index);
    question = (type === 0) ? element.name : element.description;
    questionIndex = index;
    rightAnswer = (type === 0) ? element.description : element.name;
    alternativeRightAnswer = (element.synonym !== "") ? element.synonym : element.name;
    if(type !== 2) {
        var rightVariantNumber = Math.floor(Math.random() * 4);
        for(var i = 0; i < 4; i++) {
            if(i !== rightVariantNumber) {
                options.push(getWrongOption(index, type));
            } else {
                options.push((type === 0) ? element.description : element.name);
            }
        }
    }
}

В функцию передается индекс термина в словаре и тип вопроса. В зависимости от этих параметров заполняются свойства TestStore, отвечающие за текущий вопрос (question, options, rightAnswer и другие). Они затем будут использованы видом для отображения вопроса пользователю. Для каждого типа вопроса есть своя страница:




Вот пример кода для страницы с вопросом, где пользователю нужно выбрать термин по определению:

Page {
    SilicaFlickable {
        anchors.fill: parent
        contentHeight: column.height + Theme.paddingLarge

        VerticalScrollDecorator {}

        Column {
            id: column
            width: parent.width
            spacing: Theme.paddingLarge

            PageHeader { title: qsTr("Question ") + TestStore.questionNumber }

            Label {
                text: TestStore.question
                font.pixelSize: Theme.fontSizeMedium
                wrapMode: Text.Wrap
                anchors {
                    left: parent.left
                    right: parent.right
                    margins: Theme.paddingLarge
                }
            }

            Button {
                id: option0
                height: Theme.itemSizeMedium
                anchors {
                    left: parent.left
                    right: parent.right
                    margins: Theme.paddingLarge
                }
                text: TestStore.options[0]
                onClicked: {
                    AppActions.submitAnswer(option0.text);
                }
            }

            Button {
                id: option1
                height: Theme.itemSizeMedium
                anchors {
                    left: parent.left
                    right: parent.right
                    margins: Theme.paddingLarge
                }
                text: TestStore.options[1]
                onClicked: {
                    AppActions.submitAnswer(option1.text);
                }
            }

            Button {
                id: option2
                height: Theme.itemSizeMedium
                anchors {
                    left: parent.left
                    right: parent.right
                    margins: Theme.paddingLarge
                }
                text: TestStore.options[2]
                onClicked: {
                    AppActions.submitAnswer(option2.text);
                }
            }

            Button {
                id: option3
                height: Theme.itemSizeMedium
                anchors {
                    left: parent.left
                    right: parent.right
                    margins: Theme.paddingLarge
                }
                text: TestStore.options[3]
                onClicked: {
                    AppActions.submitAnswer(option3.text);
                }
            }

            Button {
                height: Theme.itemSizeLarge
                anchors {
                    left: parent.left
                    right: parent.right
                    margins: Theme.paddingLarge
                }
                text: qsTr("Skip question")
                onClicked: {
                    AppActions.skipQuestion();
                }
            }
        }
    }
}

Как видите, информация на странице заполняется очень легко просто обращаясь к свойствам TestStore.

После каждого вопроса, который встретился во время тестирования, приложение отображает правильность данного ответа, а также само слово, его значение и применение. Это позволяет еще раз закрепить знания пользователя или, если был дан неправильный ответ, дает возможность узнать и запомнить правильный:


При этом происходит пересчет прогресса пользователя. Сам пересчет связан с настройками приложения и будет показан ниже.

Результаты пользователя в изучении слов отображаются как для всего словаря, так и для каждого термина отдельно. Для отдельных терминов результат рассчитывается при выборе одного из вариантов ответа.

AppScript {
    runWhen: ActionTypes.submitAnswer

    script: {
        TestStore.checkResult(message.answer);
        TestStore.updateDictionaryProgress(TestStore.questionIndex);
        TermInformationStore.updateInfo(TestStore.questionIndex);
        AppActions.replacePage("QuestionResult.qml");
    }
}

Для всего словаря прогресс отображается в виде шкалы, отражающей суммарную степень «изученности» всех присутствующих терминов. Также приложение ведет статистику того, сколько слов из словаря уже успешно изучены пользователем. Данный прогресс отображается как на главной странице приложения, так и на его обложке:



Так как приложение рассчитано на продолжительное использование, необходимо было реализовать хранение результатов пользователя, чтобы весь накопленный результат не терялся между запусками приложения. Для сохранения прогресса решено было использовать предоставляемый Qt класс QSettings. Он предоставляет возможность постоянного хранения настроек и данных приложения. Для Salifish OS все данные сохраняются в ini файл, соответственно, формат хранимых данных – строка. Так как QSettings все-таки класс из Qt, необходимо было импортировать его как модуль в QML. Делается это в теле функции main следующим образом:

qmlRegisterType<Settings>("harbour.dictionary.trainer.settings", 1, 0, "Settings");

QQuickView* view = SailfishApp::createView();

QSettings data("FRUCT", "Dictionary Trainer");
data.setPath(QSettings::NativeFormat, QSettings::UserScope,
    QStandardPaths::writableLocation(QStandardPaths::DataLocation));
qmlEngine->rootContext()->setContextProperty("data", &data);
QQmlComponent dataComponent(qmlEngine, QUrl("TestStore"));
dataComponent.create();

Прогресс изучения в файле сохранятся в виде «название словаря/номер термина» — «степень изученности». Название словаря здесь не случайно, в будущем мы планируем добавить больше словарей, а так же, возможно, реализовать добавление пользовательских словарей. При запуске приложения, степени изученности терминов считываются из файла и суммируются для расчета общего прогресса, также считывается число слов, являющихся «изученными» пользователем:

function fillProgress() {
    progress = 0;
    learnedWords = 0;
    if(data.childGroups().indexOf("dictionary") !== -1) {
        for (var i = 0; i < dictionary.count; i++){
            progress += data.valueAsInt("dictionary/" + i.toString());
        }
        learnedWords = data.value("dictionary/learnedWords", 0);
    } else {
        for (var i = 0; i < dictionary.count; i++){
            data.setValue("dictionary/" + i.toString(), 0);
        }
        data.setValue("dictionary/learnedWords", 0)
    }
}

Запись/обновление степени изученности термина происходит в момент ее изменения, т. е. в момент выбора ответа в тесте. Происходит это таким образом:

function updateDictionaryProgress(index) {
    var currentStatus = data.valueAsInt("dictionary/" + index);
    var newStatus;
    if (result === "correct") {
        newStatus = getWordStatus(currentStatus + 1);
    } else {
        newStatus = getWordStatus(currentStatus - 2);
    }
    var statusChange = newStatus - currentStatus;
    calculateLearnedWords(currentStatus, newStatus);
    progress += statusChange;
    data.setValue("dictionary/" + index.toString(), newStatus);
}

Итог


В итоге нам удалось реализовать весь запланированный функционал и наше первое приложение под Sailfish OS было успешно создано. А совсем недавно мы опубликовали его Jolla Store, где оно доступно для скачивания и уже имеет около 2х сотен пользователей:


Авторы: Максим Костерин, Никита Романов
FRUCT @FRUCT
карма
16,0
рейтинг 13,6
Пользователь
Самое читаемое Разработка

Комментарии (36)

  • –1
    Не вижу перспектив этой ОС для рынка России.
    Единственно куда может «стрельнуть» это госсектор, а там разработка уже давно «схвачена нужными людьми».
    Буду рад контраргументам.
    • 0
      Не вижу перспектив этой ОС для рынка России.

      А зачем так селективно, напишите сразу для «мирового рынка»?

      А что касается контраргументов, я бы и сам с удовольствием прочитал. И это не смотря на то, что у меня были Nokia N9, Jolla a сейчас Jolla C. К сожалению, единственное что ей можно поставить в преимущества — это интерфейс. Всё остальное — нишевые плюсы.
      • 0
        Насчет нишевых плюсов не согласен, что работает дольше (по сравнению с андроид устройствами с такой же батареей) — это нишевый плюс?
        • 0
          Да, именно так. У тех кто пользуется смартфонами ежедневная зарядка уже не считается чем-то неприемлемым.
          • 0
            Дожили…
          • +1
            Мне Jolla 1 хватало обычно почти на 2 полных дня. Jolla C в моем случае работает чуть меньше, чем «почти 2 полных дня». Т. е. здесь речи об зарядке телефона раз в днь не идет…
            • 0
              У меня типично с утра час — полтора навигатор из-под андройда, днём звонки, мало смс, вечером полтора — два часа, почитать из-под андройда опять же. Jolla C — держится молодцом, заряжаю только ночью — «умным» кабелем который отрубает заряд когда зарядка окончена. По процентам заряда можно было бы и реже, но тогда посреди дня получится — что неудобно. А на два — боюсь не хватит.

              Нативные приложения меньше потребляют. НО их пока мало, и требуется запускать прожорливые андройдовские. Поэтому по честному это тоже нишевый плюс.
    • +2
      И кем там схваченно все? Sailfish OS`ом? :) Sailfish OS RUS, если мне не изменяет память, единственная мобильная ОС, которая есть в РосРеестре + недавно получила сертификат от ФСБ.
      • 0
        1) Почитайте мой комментарий ниже(https://habrahabr.ru/post/312418/#comment_9857308)
        2) https://geektimes.ru/post/275406/ «Общая сумма такого проекта составляет около 2,2 млрд рублей»
        3) так же где-то здесь проскакивала информация о внедрении этих смартов работникам госсектора, цена около $2000 за аппарат. Естественно всё из бюджета.
        Теперь Вы хоть немного понимаете моё негодование?
        • +1
          Общая сумма такого проекта составляет около 2,2 млрд рублей

          Конечно, проект не дешёвый. Ну и что? Открытая Мобильная Платформа работает не на бюджетные деньги.

          проскакивала информация о внедрении этих смартов работникам госсектора, цена около $2000 за аппарат

          Может, есть ещё и ссылка на первоисточник, а не домыслы не пойми кого?
          • –2
            «Открытая Мобильная Платформа работает не на бюджетные деньги.»
            «Может, есть ещё и ссылка на первоисточник, а не домыслы не пойми кого?»

            За бюджетные деньги Платформа будет продавать аля «андроид» со своим лаунчером по $2000
            https://geektimes.ru/post/277092/

            Вы вообще себе цифру в 2,2 млрд рублей представляете?
            По Пятому каналу регулярно вымаливают деньги очередному ребёнку на операцию,
            средний «ценник» 1,5 млн. -> 2,2 млрд / 1,5 млн = 1466 детей умрёт потому что кто-то решил сделать очередной «андроид» с нескучными обоями.
            • 0
              Дорогой собеседник. Сначала к технической статье Вы примешали политику, потом социальные проблемы.
              При этом оперируете желтой прессой и абсурдным утверждениями, абсолютно игнорируя суть самой статьи, в которой рассматривается, к слову, довольно интересный пример использования Flux.
              Другого от Вас, видимо, не получить. Поэтому я заканчиваю обсуждение со своей стороны.
    • 0
      Лично я считаю, что пути развития следующие: продвигаться в B2B и B2G, «война за сердца гиков», а также рынки развивающихся стран (Индия, ЮАР)
      И если посмотреть на деятельность Jolla + Открытой мобильной платформы, то увидим, что они этими путями и стараются идти…
    • –1
      Не очень люблю подобные ссылки. Но, тем не менее, это именно контраргумент.
      О Sailfish OS и «нужных людях».
      https://twitter.com/nnikiforov/status/728264659996950528
      • –1
        Вы сами твиты читали?

        "@nnikiforov так же как с электронным паспортом будет? 4 года сделать не можем и ещё на 2,5 отодвигаем?"
        Вопрос остался без ответа

        "@nnikiforov Конкурировать с Гугл? Статистика? Кому это нужно? Нет, зачем, давайте просто под это дело попилим бабла..."
        Я придерживаюсь этого же мнения, это и печалит :(

        "@andrey_snegovik @nnikiforov Вспомни Ubuntu Touch! Опытные разрабы, хорошая идея! Большие деньги! В итоге — сколько смартов на этой ОС?"

        "@andrey_snegovik @nnikiforov Идея то хороша, а в итоге получится очередной Bolgin OS"

        "@Billi_Gates @nnikiforov "… От специалистов требуется навык портирования Android на новые платформы". Отечественная ОС говорите ?"
        Вот это особенно понравилось :)

        И еще 100500 насущных вопросов по теме оставленных без ответа.
        • +1
          Да нет, совершенно понятно, почему без ответа. Это же провокации.
          Некоторые, причём, совершенно неграмотные.

          Конкурировать с Гугл?

          Развитие Sailfish OS в РФ не позиционируется, как конкуренция с Google.
          Конечно, не позиционируется. Очевидно, не позиционируется.

          Вспомни Ubuntu Touch!

          При чём здесь Ubuntu Touch? Ubuntu — это целиком управляемая Canonical экосистема. Все вопросы корректно адресовывать именно им.

          Идея то хороша, а в итоге получится очередной Bolgin OS

          «Диванный аналитик» налицо.
          Sailfish OS обладает продуманным дизайном интерфейса, над которым работали те же люди, которые создавали UX для Nikia N9.
          https://www.youtube.com/watch?v=qUBTjJ6_WcI
          Попробуйте, он реально интересный.

          От специалистов требуется навык портирования Android на новые платформы". Отечественная ОС говорите ?"

          От людей, работающих с железом, действительно, требуется. Это должно быть очевидно всем, кто знаком с libhybris.
          Если Вам понравилось, то Вы, очевидно, не знаете, как устроена аппаратная адаптация. Рекомендую ознакомиться, хотя бы, с этим: https://wiki.merproject.org/wiki/Adaptations/libhybris

          Опять же, комментарии в твиттере — это не показатель чего бы то ни было, поскольку могут быть вызваны совершенно другими причинами, не относящимися к основной теме.
          Особенно комментарии к сообщениям политиков любой фракции без исключения.
          Поэтому я и написал выше, что не люблю подобные ссылки.
          Предлагаю исключить из обсуждения все 100500 «насущных» вопросов.
  • 0
    Вопросик такой, для затравки. На каком ЯП пишут под Sailfush?
    • 0
      На QML, который в свою очередь основан на JavaScript.
    • +1
      C++/QML, а также Python, но Python обычно не рекомендуют, т. к. он там какой-то обрезанный (подробностей не знаю)
      • +1
        Можно было сказать проще, то бишь Qt. Ну не знаю, какой-либо обрезанности в PyQt я не заметил.
        • 0
          С PyQt все нормально, но я слышал от активных разработчиков, что с Python есть некоторые сложности. Какие конкретно и насколько большие — оставляю за рамками этого комментария, так как я этим вопросом не интересовался, мне QML + C++ хвататет :)
    • +1
      Не Sailfush, а Sailfish OS.
      К слову, это и FRUCT касается. Не SailfishOS, а Sailfish OS.
      Просто, ради порядка.
      • 0
        Спасибо за комментарий, поправили название платформы в статье. Учтём на будущее правильное написание ;)
  • 0
    Заинтересовало приложение, установил… А русский язык имеется? В словаре все на английском :(
    • 0
      Словарь, к сожалению, пока только англоязычный с англоязычными терминами. Интерфейс на двух языках, зависит от языка устройства.
  • 0
    А поделитесь опытом:
    На сколько удобным оказалось на практике применение Flux в QML?
    Обнаружились ли подводные камни при реализации паттерна или библиотеки QuickFlux?
    • +1
      В принципе, достаточно удобным. На подобном небольшом проекте это, наверное, не так заметно. На больших должно быть более заметно. Из положительных моментов можно отметить, что вся логика работы с данными уходит в Store элементы, что позволяет структурировать код. Если бы мы делали все «стандартно», без централизованного хранилища, описывая логику в обработчиках событий, код бы получился довольно размазанным. Ну и, конечно, положительные момент такой, что состояние данных в любой момент доступно из Store в любой точке приложения и оно в силу реализации (Store реализованы как синглтоны) в один момент времени всегда одинаково для любого другого объекта. Такая централизованность всё таки дает ощущение надежности и единообразии. И опять же, если нам что-то нужно поменять в логике работы с данными, мы меняем в одном месте и оно работает так везде.

      С QuickFlux никаких проблем не возникло, она просто добавляется в проект и «работает как есть» :) Очень упрощает жизнь, если надо реализовать подобную архитектуру. Единственный момент, но он связан скорее с особенностью Sailfish OS, а не QuickFlux. Нам пришлось покопаться в исходниках QuickFlux, чтобы изменить название библиотеки в QML на harbour.dictionary.trainer.QuickFlux. Это было необходимо для публикации приложения (вот тут есть подробное объяснение).

      В целом, основная проблема была, конечно, в понимании архитектуры. В силу того, что у нас не так много опыта разработки вообще, а архитектура заметно отличается от привычного нам MVC, довольно много времени было потрачено на понимание и осознание самого принципа Flux.
      • 0

        Спасибо большое за развернутый ответ.

  • 0
    А что если я не хочу использовать QML, но хочу С\С++ и OpenGL? Вроде там SDL приложения разрешены. Хотелось бы больше примеров в эту сторону.
    • 0
      OoenGL можно. Но встраивать в QML.
      • 0
        Вот не хочется QML, уже есть все готовое, надо только портировать. Если для этого придется отказаться от QT в пользу SDL то это подойдет. Но примеров крайне мало, как я понимаю -lSDL не достаточно, а -lOpenGL там нет, вот и что делать.
        • 0
          От QML отказать не получится, потому что понадобятся ApplicationWindow и Cover.
          Но, по идее, использовать всё готовое на C++ это не должно помешать.
          • 0
            К сожалению мешает. Я не могу откомпилировать сишники с GL кодом, нет на заголовков ни либы с которой линковать. Через strace я уже разобрался как происходит загрузка GL so'шки, теоретически могу загрузить ее самостоятельно, взять стандартные иклуды. Но это же костыли, правильно то как?
            Вот тут пишут что разрешены SDL2 приложения, в QT врапить не обязательно. Только зачем мне SDL без GL?
            • 0
              Там GL или GLES?
              • 0
                Да, неточность получилась, GLES2 конечно же. Но /usr/include/GLES не наблюдается. Если попробовать поставить mesa-llvmpipe-libGLESv2-devel оно предлагает снести пол системы.
  • 0
    Коллеги, а где есть исходный код приложения?

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