📜 ⬆️ ⬇️

Development for Sailfish OS: FLUX architecture in QML by the example of an application for memorizing literary terms

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



The task was to write an application with which one could study and memorize literary terms. Since it is too simple and boring to implement a conventional dictionary with the interpretation of words, it was decided: to organize the learning process through user interaction. 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, each of which is defined, it was decided to identify three types of questions for 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 one true. The second type of questions is equivalent to the first, with the only difference 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 questions - with an open answer. The definition is shown to the user and he needs to write the word himself that corresponds to the definition.
')
In addition to the learning process described above, it was necessary to implement a review of the list of terms that we will teach, so that the user can know what they will be asked to do. It was also planned to track and preserve the progress in the study, in order to show the user how much progress he had achieved in the study of words.

Application Features


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

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

Such a dictionary is very easy to load - using the standard XmlListModel component.

The architecture of the application was chosen by the Facebook architecture promoted by Flux. About the architecture itself was already a lot of written articles. Quite interesting and understandable translations are available on Habré: here and here . Also, when developing, we were guided by an article on using 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 on the links above and it is described there very well. Therefore, we will write only 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 done with the help of Actions. In our case, the navigateTo Action 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 the functions. One (we called it TermInformationStore ) is responsible for a particular current term. It contains information about the term: the word itself, its transcription, meaning, usage example and synonyms for it. In the same Store is filling properties that contain the above information.

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

To separate the work with data and the organization of the interconnection of parts of the application, a Script element was created that is responsible for receiving signals from the View and calling functions from the Store in the correct order, which solves the problem of calling new actions when the old ones are 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 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 the list of terms from the XmlListModel is loaded (as described above). In general, this is the most common list, and since the creation of 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 moment.

Clicking on the 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. Clicking on an item in the list creates an Action with information about the index of the clicked item. This Action provokes TermInformationStore to change information about the current term depending on the selected index of the list item, and then open a page with a description. It looks quite simple:


Testing can start from the main screen. There are a total of 20 questions on non-recurring terms, selected randomly in the test. The type of the 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 chosen randomly. As mentioned above, TestStore is responsible for all the 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 function is passed the index of the term in the dictionary and the type of the question. 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. For each type of question there is a page:




Here is a sample code for a page asking where the user needs to choose 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 very easily by simply referring to the properties of the TestStore .

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


When this happens, the user's progress is recalculated. The recalculation itself is related to the application settings and will be shown below.

User results 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 choosing one of the answers.

 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 continuous use, it was necessary to implement the storage of user results so that all accumulated results are not lost between application launches. To save progress, it was decided to use the QS class QSettings provided. It provides the ability to permanently store settings and application data. For Salifish OS, all data is saved in the 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 into QML. This is done in the body of the main function as follows:

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

The progress of the study in the file will be saved in the form of "the name of the dictionary / term number" - "the degree of knowledge." The name of the dictionary is not accidental here, in the future we plan to add more dictionaries, as well as, perhaps, to implement the addition of user dictionaries. When you start the application, the degree of knowledge of the terms is read from the file and summarized 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) } } 

Record / update of the degree of knowledge of the term occurs at the moment of its change, that is, at the moment of selecting the 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 under Sailfish OS was successfully created. And most recently we published it on the Jolla Store, where it is available for download and already has about 2 hundred users:


Authors: Maxim Kosterin, Nikita Romanov

Source: https://habr.com/ru/post/312418/


All Articles