Наше перше додаток для SailfishOS

Всім доброго часу доби! У даній статті хотілося б розповісти, як ми розробили свій перший додаток для платформи SailfishOS (про розробку під яку вже був ряд статей).



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

Оскільки словник являє собою список термінів, для кожного з яких дано визначення, було вирішено виділити три типи питань для тестів. У першому типі користувачеві показується сам термін і чотири варіанти визначень термінів з яких він повинен вибрати одну вірну. Другий тип питань еквівалентний першого, з тією лише різницею, що користувачеві вже показано одне визначення і чотири терміна. Користувач повинен вибрати термін, що відповідає цьому визначенню. Нарешті, останній тип питань — з відкритою відповіддю. Користувачеві показується визначення і йому необхідно самому написати слово, що відповідає цьому визначенню.

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

Особливості програми
Власне сам словник був люб'язно наданий нам співробітниками кафедри іноземних мов нашого Вузу (ЯрГУ їм. П. Р. Демидова). Він був у звичайному текстовому вигляді, тому для зручності використання ми перенесли його в формат 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 загалом, так і для SailfishOS зокрема, те і загострювати увагу на цьому моменті не будемо.

При натисканні на елемент списку відкривається сторінка з докладним описом терміна. Оскільки ми для програми вирішили використовувати архітектуру 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.словник.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-х сотень користувачів:


Автори: Максим Костерін, Микита Романов
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.