Development for Sailfish OS: FLUX architecture in QML using an example application for memorizing literary terms

Good day to all! In this article I would like to tell how we developed our first application for the Sailfish OS platform (there were already a number of articles about the development for which).



The task was to write an application with which it would be possible to study and memorize literary terms. Since it is too simple and boring to implement a regular dictionary with the interpretation of words, a decision was made: to organize the learning process through interaction with the user. Having considered all the available options for building user interaction, it was decided to do the training in the form of tests.

Since the dictionary is a list of terms for each of which a definition is given, it was decided to identify three types of questions for the tests. In the first type, the user is shown the term itself and four variants of definitions of terms from which he must choose the right one. The second type of questions is equivalent to the first, with the only difference being that the user has already been shown one definition and four terms. The user must select a term that matches this definition. Finally, the last type of question is an open answer. The user is shown the definition and he needs to write the word himself, corresponding to this definition.

In addition to the learning process described above, it was necessary to implement viewing a list of terms that we will teach, so that the user can know what will be asked from him. It was also planned to track and maintain progress in learning, in order to show the user what successes he achieved in learning words.

Application features


Actually, the dictionary itself was kindly provided to us by the staff of the department of foreign languages ​​of our university (Yaroslavl State University named after P. G. Demidov). It was in plain text, so for ease of use, we moved it to xml format. The result is an xml document consisting of elements of the form:

EpenalepsisPolysyndeton[ˌpɒlɪˈsɪndɪtən]Use of several conjunctionsHe thought, and thought, and thought…

Downloading such a dictionary is very easy - using the standard XmlListModel component .

Facebook’s Flux architecture was chosen as the application architecture. A lot of articles have already been written about architecture itself. Quite interesting and understandable translations are available on Habré: here and here . Also, during the development, we were guided by an article on the use of Flux when writing QML applications . We recommend the article to everyone who writes applications in QML (not necessarily even mobile). It is unnecessary to describe all these points here, since all the information is available at the above links and it is described there very well. Therefore, we will only write how the Flux architecture was used in our application.

With View, everything is clear - each page of the application is part of the View. The transition between pages is carried out using Actions. In our case, the action navigateTo is responsible for the transition .

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

Two stores are used to store values, as well as to implement functions. One (we called it TermInformationStore ) is responsible for a separate current term. It contains information about the term: the word itself, its transcription, meaning, usage example and synonyms for it. In the same Store, the properties containing the above information are populated.

The second Store - TestStore - is responsible for the testing process and progress in learning words. It contains information about the current test question. Accordingly, these questions are compiled here, and here progress is calculated.

In order to separate the work with data and the organization of the relationship between the parts of the application, a Script element was created that is responsible for receiving signals from View and calling functions from the Store in the correct order, which solves the problem of calling new actions when the old ones have not yet completed. Also, this element contains all the logic for moving between different screens of the application.

Implemented Functionality


Since this was our first application for this platform, and in QML in general, at first we of course took up the simplest - a list of terms. The list itself is implemented using SilicaListView , into which a list of terms from XmlListModel is loaded (as described just above). In general, this is the most common list, and since creating lists is one of the most basic and common examples for QML in general, and for Sailfish OS in particular, we will not focus on this point at this point.

Clicking on a list item opens a page with a detailed description of the term. Since we decided to use the Flux architecture for the application, the process of opening this page looks somewhat unusual compared to MVC or MVVM. When you click on a list item, an Action is created with information about the index of the clicked item. This Action provokes TermInformationStore to change the information about the current term depending on the selected index of the list item, and then open the description page. It looks quite simple:


Testing can be started from the main screen. There are 20 questions in the test on non-repeating terms selected randomly. The type of question itself (as described at the beginning - we have three of them) and the wrong answers (if they should be in this type of question) are also randomly selected. As already mentioned above, TestStore is responsible for the entire logic of writing questions . The question is created as follows:

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);
            }
        }
    }
}

The index index of the term in the dictionary and the type of question are passed to the function. Depending on these parameters, the TestStore properties that are responsible for the current question ( question , options , rightAnswer and others) are filled . They will then be used by the view to display the question to the user. Each type of question has its own page:




Here is a sample code for a page asking where the user needs to select a term by definition:

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

As you can see, the information on the page is filled in very easily simply by accessing the properties of TestStore .

After each question that occurred during testing, the application displays the correctness of this answer, as well as the word itself, its meaning and application. This allows you to once again consolidate the user's knowledge or, if the wrong answer was given, makes it possible to find out and remember the correct one:


This recounts the user's progress. The recount itself is associated with the application settings and will be shown below.

The results of the user in the study of words are displayed both for the entire dictionary, and for each term separately. For individual terms, the result is calculated when you select one of the answer options.

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

For the entire dictionary, progress is displayed in the form of a scale reflecting the total degree of “knowledge” of all the terms present. The application also keeps statistics on how many words from the dictionary have already been successfully studied by the user. This progress is displayed both on the main page of the application and on its cover:



Since the application is designed for long-term use, it was necessary to implement the storage of user results so that the entire accumulated result is not lost between application launches. To maintain progress, it was decided to use the Qt class QSettings provided . It provides the ability to permanently store settings and application data. For Salifish OS, all data is stored in an ini file, respectively, the format of the stored data is a string. Since QSettings is still a class from Qt, it was necessary to import it as a module in QML. This is done in the body of the main function as follows:

qmlRegisterType("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();

The study progress in the file will be saved in the form of “dictionary name / term number” - “degree of knowledge”. The name of the dictionary here is not accidental, in the future we plan to add more dictionaries, and it is also possible to implement the addition of custom dictionaries. When the application starts, the degrees of knowledge of the terms are read from the file and summed up to calculate the overall progress, the number of words that are “learned” by the user is also read:

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)
    }
}

Recording / updating the degree of knowledge of the term occurs at the time of its change, i.e., at the time of selecting an answer in the test. It happens this way:

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);
}

Total


As a result, we managed to implement all the planned functionality and our first application for Sailfish OS was successfully created. And more recently, we published its Jolla Store, where it is available for download and already has about 2 hundred users:


Authors: Maxim Kosterin, Nikita Romanov

Also popular now: