Тай’Дзен: первые шаги (часть 3)

в 6:00, , рубрики: kama games, kamagames, mobile development, native development, tizen, tizen sdk, tizen ui builder, Блог компании KamaGames Studio, метки: , , , ,

Уважаемое читатели, приветствую!
В этой части статьи мы завершим исследование простого нативного приложения для ведения списка покупок. Хочу напомнить, что в первой части рассмотрена структура проекта, а вторая часть посвящена некоторым стандартным элементам GUI. Сегодня читателя ждет работа с базой данных, организация поиска, а также локализация и прочие «завершающие штрихи». Все коммиты открытого git-репозитория снабжены комментариями, для каждого этапа в тексте указаны соответствующие теги. Добро пожаловать под кат!

Часть вторая

ЧАСТЬ ТРЕТЬЯ

Tizen SDK предлагает ряд встроенных средств для работы с данными. Для простых задач, коей является наша, можно использовать обычные чтение/запись в файл. Работа с файлами подробно, с примерами описана в документации. В случаях, требующих гибкости и больших объемов данных, имеется механизм доступа к встраиваемой базе данных SQLite. Нам она пригодится для осуществления быстрого поиска по записям и удобного переупорядочивания списка покупок.
ТайДзен: первые шаги (часть 3)
Для работы с БД имеется целый арсенал средств. 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)

ТайДзен: первые шаги (часть 3)

Как я уже упоминал, в большинстве проектов для работы с БД я использую собственную оболочку над 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:

ТайДзен: первые шаги (часть 3)

В большинстве случаев они бесполезны, поэтому я их отключаю и оставляю только собственные «дебажные» сообщения:

ТайДзен: первые шаги (часть 3)

Видим, что я допустил ошибку – указал неправильное имя файла. Заменяем lists1.sqlite на lists.sqlite, и подключение к базе готово!

Теперь извлечем названия списков покупок. Пока извлекать нечего, база пустая, поэтому в SQLite Manager-е выбираем таблицу Lists, переходим на вкладку «Просмотр и поиск» и добавляем несколько тестовых значений:

ТайДзен: первые шаги (часть 3)

Для загрузки значений нам понадобятся:

  1. таблица DbDataSet;
  2. новый класс List – это структуры для хранения строк таблицы Lists;
  3. новый класс RowList, наследующий DbRow – обеспечивает доступ к строкам для DbDataSet;
  4. класс, наследующий DbRowBuilderInterface – он будет генерировать объекты RowList для строки DbDataSet;
  5. 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, мы бы просто обратились к модели и взяли все необходимое, а так придется лепить костыль:

  1. Указатель на DbAccess делаем статическим и открытым для доступа извне;
  2. Механизм извлечения данных переносим из ShoppingListMainForm в ShoppingListTab1;
  3. Подключение к базе данных переносим из ShoppingListMainForm::OnInitializing в начало ShoppingListMainForm::Initialize, чтобы база была подключена до того, как будет инициализирована Tab1.
  4. DbDataSet со списками делаем членом класса ShoppingListTab1.

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

ТайДзен: первые шаги (часть 3)

Как выглядит проект на данном этапе – см. метку 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 – наводим красоту: раскрашиваем кнопки, меню, панели используя рекомендуемую цветовую палитру и размеры шрифтов. Добавляем локализацию текстов (русский, английский).

ТайДзен: первые шаги (часть 3)

Наконец, v1.0 – завершающий штрих, а именно – сжимаем базу данных командой VACUUM при выходе из приложения.

На этом наш «Hello, world!» можно считать завершенным. В первой части статьи была описана вариация паттерна MVC, однако ее реализация была отложена для облегчения понимания (и изложения) материала. Вещь полезная и в контексте нативных Tizen-приложений нуждается в отдельном рассмотрении. Кроме того, Tizen SDK предлагает удобные встроенные средства для связи GUI и основного кода приложения с данными. Описанию этих механизмов я хочу посвятить следующую статью. Надеюсь, был не особо нудным – буду рад ответить на вопросы :) Да пребудет с вами сила Тай’Дзен!

Автор: IFITOWS

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js