Здравствуйте! Данная статья является продолжением цикла статей, посвященных разработке для мобильной платформы Sailfish OS. В этот раз мы решили рассказать о приложении для контроля финансов, позволяющее пользователю вести журнал доходов и расходов, а также откладывать средства для осуществления целей. Стоит упомянуть, что данное приложение является одним из победителей хакатона по Sailfish OS в Ярославле, организованного компанией «Открытая Мобильная Платформа» и ассоциацией FRUCT.

Описание


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

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



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

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



Второй модуль приложения предоставляет возможность создавать задачи для накопления денежных средств на какие-либо цели. Пользователь может фиксировать в приложении информацию о том, сколько он откладывает средств на ту или иную цель,
и отслеживать прогресс, тем самым дополнительно мотивируя себя на выполнение своих задач:

Работа с базой данных


В данной статье было решено акцентировать внимание на работе с базой данных непосредственно из QML файлов. Для реализации этой задачи была использована библиотека LocalStorage, позволяющая организовывать доступ к базам данных SQLite, хранящимся на устройстве.

Для отделения логики работы с БД от элементов вида был создан QML объект для доступа к данным (data access object или попросту DAO), управляющий всеми соединениями с базой и предоставляющий более удобный интерфейс для работы с данными. Соединения с базой данных открываются с помощью глобального объекта синглтона LocalStorage. На нем вызывается метод openDatabaseSync(), который непосредственно открывает соединение или создает базу, если она не была создана ранее. Все соединения автоматически закрываются при сборке мусора. Ниже приведена часть кода файла Dao.qml:

import QtQuick 2.0
import QtQuick.LocalStorage 2.0

Item {
   Component.onCompleted: {
       database = LocalStorage.openDatabaseSync("SaveYourMoneyDatabase", "1.0")
   }
  //...
}

Транзакции и запросы при работе с БД оформлены как функции JS: для получения
результатов требуются callback-функции, вызываемые по окончании выполнения операций. На полученном объекте соединения можно вызывать методы readTransaction() и transaction(), которые создают транзакцию на чтение или изменение данных и передают ее callback-функции, указываемой в качестве аргумента этих методов. Внутри этих функций могут быть вызваны методы executeSql(), содержащие SQL-запросы.

В нашем приложении потребовалось создать базу данных с тремя таблицами: TransactionsTable для хранения операций, GoalTable для целей и CategoriesTable для категорий операций:

Component.onCompleted: {
       database = LocalStorage.openDatabaseSync("SaveYourMoneyDatabase", "1.0")
       database.transaction(function(tx) {
           tx.executeSql("CREATE TABLE IF NOT EXISTS TransactionsTable(
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                date TEXT,
                sum INTEGER,
                category_id INTEGER,
                type INTEGER,
                goal_id INTEGER,
                description TEXT)");
           tx.executeSql("CREATE TABLE IF NOT EXISTS CategoriesTable(
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT,
                type INTEGER)");
           tx.executeSql("CREATE TABLE IF NOT EXISTS GoalTable(
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT,
                sum INTEGER,
                isFinished INTEGER)");
	  // ...
           }
       });
   }

Стоит упомянуть, что операции по созданию БД и ее инициализации необходимо помещать внутри обработчика сигнала Component.onCompleted для выполнения в момент создания объекта Dao.

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

function createTransaction(date, sum, category_id, type, description) {
       database.transaction(function(tx) {
           tx.executeSql("INSERT INTO TransactionsTable(date, sum, category_id, type, description) VALUES(?, ?, ?, ?, ?)", [date, sum, category_id, type, description]);
       });
}

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

function retrieveGoals(isFinished, callback) {
       database = LocalStorage.openDatabaseSync("SaveYourMoneyDatabase", "1.0");
       database.readTransaction(function(tx) {
           var result = tx.executeSql("SELECT * FROM GoalTable WHERE isFinished = ? ORDER BY id ASC", [isFinished]);
           callback(result.rows)
       });
}

Здесь результатом выполнения executeSql() является объект, содержащий свойство rows со списком всех результирующих записей. Чтобы получить i-ый элемент, достаточно вызвать метод rows.item(i). Количество элементов доступно по свойству rows.length. Пример использования описанного метода:

Dao { id: dao }

SilicaListView {
    id: listView
    model: GoalListModel { id: goalsListModel }
    delegate: ListItem {
        // ...
    }
    // ...
}

function displayGoals() {
    listView.model.clear();
    dao.retrieveGoals(true, function(goals) {
        for (var i = 0; i < goals.length; i++) {
            var goal = goals.item(i);
            listView.model.addGoal(goal.id, goal.name, goal.sum);
        }
    });
}
Component.onCompleted: displayGoals();

В данном коде видно, что значения полей записи из БД, доступны в качестве свойств объекта var goal = goals.item(i). Учитывая это, запрос с вычисляемым полем будет выглядеть:

function retrieveGoalStatistics(callback) {
    database = LocalStorage.openDatabaseSync("SaveYourMoneyDatabase", "1.0");
    database.readTransaction(function(tx) {
        var result = tx.executeSql("SELECT SUM(sum) AS goalSum FROM GoalTable WHERE isFinished = 0");
        callback(result.rows.item(0).goalSum)
    });
}

Также в нашем приложении добавлена фильтрация записей по датам. Как известно, SQLite не имеет стандартного типа для работы с датами. Вместо этого SQLite поддерживает пять функций для работы с датой и временем. Все даты внутри БД мы храним в виде строки в формате ISO8601. Пример запроса на извлечение даты самой ранней операции выглядит следующим образом:

function retrieveDateOfFirstTransaction(callback) {
    database = LocalStorage.openDatabaseSync("SaveYourMoneyDatabase", "1.0");
    database.readTransaction(function(tx) {
        var result = tx.executeSql("SELECT MIN(date(date)) as minDate FROM TransactionsTable");
        callback(result.rows.item(0).minDate)
    });
}

На стороне QML при работе с датами используется QML класс Date, наследуемый от класса Date из Javascript. Чтобы продолжить работу с результатом запроса необходимо выполнить:

dao.retrieveDateOfFirstTransaction(function(result){
    startDate =  new Date(result);
});

Чтобы преобразовать объект обратно к строке в формате ISO8601, необходимо использовать метод toISOString().

Заключение


В результате было создано приложение, позволяющее хранить информацию о финансах пользователя. Приложение было опубликовано в магазине приложений Jolla Harbour под названием Save Your money и доступно для скачивания всем желающим. Исходники приложения доступны на Bitbucket.

Технические вопросы можно также обсудить на канале русскоязычного сообщества Sailfish OS в Telegram или группе ВКонтакте.

Автор: Дарья Ройчикова
Поделиться с друзьями
-->

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


  1. lani
    13.12.2016 13:14

    > исходники доступны все желающим
    А у меня: «You do not have access to this repository.»


    1. FRUCT
      13.12.2016 13:24

      Спасибо за комментарий, исправили, теперь должно быть доступно.


  1. lani
    13.12.2016 13:39

    То что все приложение написано на JS (qml) — это идеоматичный подход Sailfish?
    И есть ли преимущества у нативных приложение перед Android?


    1. kirikch
      13.12.2016 13:42

      То что все приложение написано на JS (qml) — это идеоматичный подход Sailfish?

      На QML должен быть написан интерфейс пользователя. И это архитектура приложений.
      Более сложную или более производительную логику можно реализовывать на Qt/C++. Связка с QML делается очень органично.

      И есть ли преимущества у нативных приложение перед Android?

      Конечно. Они требуют меньше ресурсов и выглядят и ведут себя «нативно».


  1. navrocky
    15.12.2016 08:36

    Было бы не плохо увидеть мотивирующую статью — «Зачем писать под Sailfish». Телефонов то на нашем рынке нет. Было бы интересно почитать про перспективы этой ОС на данный момент.


    1. kirikch
      18.12.2016 22:16

      То есть, помимо того, что это единственная мобильная ОС, к слову доступная в РФ так или так, которая предоставляет функционал полноценного Linux?

      Например, такое одобрение, как мне думается, тоже важно.