Уважаемое читатели, приветствую!
В этой части статьи мы завершим исследование простого нативного приложения для ведения списка покупок. Хочу напомнить, что в первой части рассмотрена структура проекта, а вторая часть посвящена некоторым стандартным элементам GUI. Сегодня читателя ждет работа с базой данных, организация поиска, а также локализация и прочие «завершающие штрихи». Все коммиты открытого git-репозитория снабжены комментариями, для каждого этапа в тексте указаны соответствующие теги. Добро пожаловать под кат!
Часть вторая
ЧАСТЬ ТРЕТЬЯ
Tizen SDK предлагает ряд встроенных средств для работы с данными. Для простых задач, коей является наша, можно использовать обычные чтение/запись в файл. Работа с файлами подробно, с примерами описана в документации. В случаях, требующих гибкости и больших объемов данных, имеется механизм доступа к встраиваемой базе данных SQLite. Нам она пригодится для осуществления быстрого поиска по записям и удобного переупорядочивания списка покупок.
Для работы с БД имеется целый арсенал средств. Tizen::Io::Database предназначен для доступа к БД: подключение, непосредственное выполнение SQL-запросов и т.д. Tizen::Io::DbStatement позволяет удобно формировать запрос и прикреплять к нему данные: числовые, строковые, бинарные – разумеется, в контексте синтаксиса SQLite. Tizen::Io::DbEnumerator отвечает за извлечение данных из результата запроса. Это минимальный набор для работы с БД. В большинстве случаев я пользуюсь именно этим набором, правда, под дополнительной самописной оболочкой, которая автоматизирует некоторые рутинные операции. Также в Tizen SDK имеются дополнительные инструменты, облегчающие работу, например: SqlStatementBuilder – удобно формирует простые запросы, Data controls – обмен данными между приложениями, Tizen::Io::DataSet, Tizen::Io::DataRow – для работы с таблицами в памяти, и т. д.
Начнем от печки, т.е. непосредственно с файла базы данных SQLite. Его можно создать при первом запуске приложения (и в документации подробно описано, как это сделать), также можно создать таблицу с помощью SQL-запроса. Я предпочитаю использовать уже сформированный файл SQLite, с заранее подготовленными таблицами. Для этих целей удобно воспользоваться SQLite Manager-ом – плагином для Firefox. Инструмент кроссплатформенный и довольно функциональный. Те, кому его возможностей мало, могут воспользоваться консольной утилитой с официального сайта SQLite.
Файл новой БД сохраняем в каталоге data, что корневом каталоге Tizen-проекта (см. первую часть статьи). Нам понадобятся 2 таблицы: в одной будем хранить покупки, а в другой – списки покупок:
Как я уже упоминал, в большинстве проектов для работы с БД я использую собственную оболочку над 3-мя основными классами: Tizen::Io::Database, Tizen::Io::DbStatement и Tizen::Io::DbEnumerator. Классы DataSet и DataRow не упоминаются в документации в контексте работы с базами данных, не считая того, что у них одно корневое пространство имен Tizen::Io. Неудивительно, что они были обнаружены мной уже после того, как я написал собственные реализации (есть оправдание!). Как бы там ни было, они работают, и их можно посмотреть в коммите под меткой v0.3. Хочу подчеркнуть, что я писал оболочку для себя, и никоим образом не претендую на особую изящность кода и архитектуры. Кратко перечислим ее основные модули:
- DbRow – интерфейс, наследники которого инкапсулируют строки таблиц;
- DbDataSet – хранилище для набора DbRow;
- DbAccess – отвечает за подключение/отключение БД, чтение одиночных значений (метод GetValue), чтение/запись целых таблиц (FillDataSet, UploadDataSet) и выполнение запросов-команд с параметрами (PerformRequest);
- DbQuery – объединяет строковый запрос и его параметры в одном объекте;
- DbRowBuilder – интерфейс для создания реализаций DbRow, используется DbDataSet-ом;
- DbValue – универсальное значение поля, тип которого определяется при создании;
- DbRowValue – стандартная реализация DbRow, используется, когда надо прочитать только 1 столбец.
Вернемся теперь к вариации паттерна MVC, рассмотренной в первой части. Полноценная реализация паттерна подразумевает, что все его модули должны между собой как-то общаться. Это можно сделать напрямую через указатели, через посредника или через событийную модель, в общем, способов много, со своими плюсами и минусами. На практике я обычно использую фабрику и диспетчер объектов, которые позволяют объектам безопасно общаться друг с другом через интерфейс сообщений. Разумеется, поскольку объекты модели и контроллера будут присутствовать в единственном экземпляре, первое, что приходит на ум – инстанцировать их через синглтон. Соблазн велик, однако в реальном мире требования к ПО постоянно меняются, а это означает, что любые монолиты в коде в конце концов изживут себя. Думаю, эта тема заслуживает отдельного рассмотрения, а сейчас, не отвлекаясь на вопросы архитектуры, разместим весь код в ShoppingListMainForm. Напоминаю, наш проект учебный!
Форма ShoppingListMainForm загружается один раз при старте приложения, а закрывается при выходе из него, поэтому подключение/отключение базы данных удобно делать в соответствующих обработчиках событий:
result
ShoppingListMainForm::OnInitializing(void)
{
result r = E_SUCCESS;
// прочий код инициализации
…
pDb = new DbAccess();
r = pDb->Construct();
if (IsFailed(r))
{
AppLogDebug("ERROR: cannot construct DbAccess! [%s]", GetErrorMessage(r));
return r;
}
String strDbName = "lists1.sqlite";
r = pDb->Connect(strDbName);
if (IsFailed(r))
{
AppLogDebug("ERROR: cannot connect %S! [%s]", strDbName.GetPointer(), GetErrorMessage(r));
return r;
}
return r;
}
result
ShoppingListMainForm::OnTerminating(void)
{
result r = E_SUCCESS;
r = pDb->Close();
if (IsFailed(r))
{
AppLogDebug("ERROR: cannot close DbAccess! [%s]", GetErrorMessage(r));
return r;
}
delete pDb; pDb = null;
return r;
}
В нативной разработке под Tizen не используются исключения, вместо этого принято возвращать результирующий системный enum и всегда его проверять. В случае ошибки я также добавляю трассировку в лог. Поначалу сильно напрягает, однако это окупается при отладке. Я вообще забыл, когда последний раз пользовался отладчиком – благодаря логам всегда можно узнать, что происходит в программе. Команд вывода сообщений в лог предлагается несколько, но я рекомендую использовать AppLogDebug. Во-первых, она деактивируется в релизной версии, а вывод большого количества логов снижает производительность. Во-вторых, Tizen сам сыпет в лог тонны системных сообщений, и, как правило, эти сообщения имеют тип AppLogException:
В большинстве случаев они бесполезны, поэтому я их отключаю и оставляю только собственные «дебажные» сообщения:
Видим, что я допустил ошибку – указал неправильное имя файла. Заменяем lists1.sqlite на lists.sqlite, и подключение к базе готово!
Теперь извлечем названия списков покупок. Пока извлекать нечего, база пустая, поэтому в SQLite Manager-е выбираем таблицу Lists, переходим на вкладку «Просмотр и поиск» и добавляем несколько тестовых значений:
Для загрузки значений нам понадобятся:
- таблица DbDataSet;
- новый класс List – это структуры для хранения строк таблицы Lists;
- новый класс RowList, наследующий DbRow – обеспечивает доступ к строкам для DbDataSet;
- класс, наследующий DbRowBuilderInterface – он будет генерировать объекты RowList для строки DbDataSet;
- DbQuery – инкапсулирует SQL-запрос к базе DbAccess.
Наследовать DbRowBuilderInterface будет сама форма. В качестве новой строки будем возвращать RowList. Если бы нам был нужен только один столбец, можно было бы воспользоваться готовым классом DbRowValue. При создании таблицы не забываем запомнить ее идентификатор theDataSetIdGetLists – он понадобится, чтобы определять, какие строки какой таблице передавать:
void
ShoppingListMainForm::GetLists()
{
DbDataSet theTable;
theDataSetIdGetLists = theTable.GetId();
theTable.SetRowBulder(this);
String strQueryString = "SELECT * FROM Lists";
DbQuery query;
query.queryString = strQueryString;
pDb->FillDataSet(query, theTable);
int count = theTable.GetRowCount();
int valueInt;
String* pvalueText;
for (int i=0; i<count; i++)
{
DbRow* pRow = theTable.GetRow(i);
if (pRow)
{
pRow->GetInt(0, valueInt);
pRow->GetText(1, pvalueText);
AppLogDebug("%i %S", valueInt, pvalueText->GetPointer());
}
else
{
AppLogDebug("ERROR: pRow is null!");
}
}
return;
}
DbRow*
ShoppingListMainForm::BuildNewRowN(unsigned int tableId, unsigned int rowIndex, void* content) const
{
DbRow* pRow = null;
if (tableId == theDataSetIdGetLists)
{
pRow = new RowList(new Content::List());
}
return pRow;
}
Добавляем вызов метода GetLists в обработчик инициализации формы OnInitializing, запускаем проект и наблюдаем в логе id и названия списков:
> 1 Подарки
> 2 Продукты
> 3 Прогулочная амуниция для кота
Как выглядит проект на данном этапе – см. метку v0.4.
Теперь отобразим названия списков в ListView на вкладке Tab1. Для этого ShoppingListTab1 должен иметь доступ к данным, загружаемым из базы. Будь у нас полноценный MVC, мы бы просто обратились к модели и взяли все необходимое, а так придется лепить костыль:
- Указатель на DbAccess делаем статическим и открытым для доступа извне;
- Механизм извлечения данных переносим из ShoppingListMainForm в ShoppingListTab1;
- Подключение к базе данных переносим из ShoppingListMainForm::OnInitializing в начало ShoppingListMainForm::Initialize, чтобы база была подключена до того, как будет инициализирована Tab1.
- DbDataSet со списками делаем членом класса ShoppingListTab1.
Получилось, мягко говоря, не очень красиво – это результат пренебрежения архитектурой. Продолжим: назначаем DbDataSet в качестве источника данных для ListView и, наконец, наблюдаем названия списков на экране:
Как выглядит проект на данном этапе – см. метку v0.5.
Во второй части статьи было рассмотрено контекстное меню элементов списка, куда мы поместили кнопку Delete. Настало время воспользоваться обработчиком этого события:
void
ShoppingListTab1::OnListViewContextItemStateChanged(Tizen::Ui::Controls::ListView& listView, int index, int elementId, Tizen::Ui::Controls::ListContextItemStatus status)
{
if (status == LIST_CONTEXT_ITEM_STATUS_SELECTED && elementId == ID_CNTX_BTN_DELETE)
{
// удаляем из базы данных
RowList* pRow = dynamic_cast<RowList*>(theTableLists.GetRow(index));
if (pRow)
{
DbQuery theQuery;
theQuery.queryString = "DELETE FROM Lists WHERE id = ?";
theQuery.AddParamInt(pRow->pList->id.value);
ShoppingListMainForm::pDb->PerformRequest(theQuery);
}
if (pRow)
{
DbQuery theQuery;
theQuery.queryString = "DELETE FROM Purchases WHERE list_id = ?";
theQuery.AddParamInt(pRow->pList->id.value);
ShoppingListMainForm::pDb->PerformRequest(theQuery);
}
// удаляем из таблицы в памяти
theTableLists.RemoveRow(index);
// визуализируем изменения
pListview1->UpdateList();
}
}
В базе данных удаляем сам список и все его содержимое (покупки). После изменений в источнике данных нужно также обновить ListView, чтобы изменения отобразились на экране.
Потенциальная ловушка: чтобы файл базы данных при повторном развертывании приложения был перезаписан, его нужно модифицировать (либо удалить приложение с целевого устройства). В противном случае на целевом устройстве останется старый файл. Это сделано для быстрого развертывания приложения, чтобы не нужно было каждый раз пересылать мегабайты ресурсных файлов.
Дальнейшую работу уместнее отслеживать, изучая изменения в коде.
Метка v0.6 – к левой сенсорной кнопке привязано контекстное меню (OptionMenu) с командой «Добавить». Она вызывает диалог, который приглашает ввести имя нового элемента. После подтверждения диалог закрывается, а в базу и таблицу DbDataSet добавляется новая запись.
Метка v0.7 – реализована работа с покупками. Выбор списка на вкладке Tab1 приводит к переходу на вкладку Tab2, где отображены элементы этого списка. Их можно редактировать: добавлять, удалять, отмечать завершенные. Завершенные покупки отображаются в конце списка.
Метка v0.8 – в контекстное меню добавлена команда Search. Поиск осуществляется в отдельной панели, ищет как названия списков, так и названия покупок. Выбор результата поиска приводит к переходу на соответствующую вкладку (и скроллингу до нужного элемента, если необходимо).
Метка v0.9 – наводим красоту: раскрашиваем кнопки, меню, панели используя рекомендуемую цветовую палитру и размеры шрифтов. Добавляем локализацию текстов (русский, английский).
Наконец, v1.0 – завершающий штрих, а именно – сжимаем базу данных командой VACUUM при выходе из приложения.
На этом наш «Hello, world!» можно считать завершенным. В первой части статьи была описана вариация паттерна MVC, однако ее реализация была отложена для облегчения понимания (и изложения) материала. Вещь полезная и в контексте нативных Tizen-приложений нуждается в отдельном рассмотрении. Кроме того, Tizen SDK предлагает удобные встроенные средства для связи GUI и основного кода приложения с данными. Описанию этих механизмов я хочу посвятить следующую статью. Надеюсь, был не особо нудным – буду рад ответить на вопросы :) Да пребудет с вами сила Тай’Дзен!
Автор: IFITOWS