Tai'Zen: First Steps (Part 3)

  • Tutorial
Dear habrasociety, welcome!
In this part of the article, we will complete the study of a simple native shopping list application. I want to remind you that in the first part the structure of the project is considered, and the second part is devoted to some standard GUI elements. Today, the reader will find working with a database, organizing a search, as well as localization and other “finishing touches." All the commits of the open git repository are provided with comments, for each stage the corresponding tags are indicated in the text. Welcome to cat!

PART TWO

PART THREE

The Tizen SDK offers a number of built-in data tools. For simple tasks, which is ours, you can use ordinary read / write to a file. Work with files in detail, with examples described in the documentation . In cases requiring flexibility and large amounts of data, there is a mechanism for accessing the embedded SQLite database. It is useful to us for a quick search of records and convenient reordering of your shopping list.

There is a whole arsenal of tools for working with the database . Tizen :: Io :: Database is intended for access to the database: connection, direct execution of SQL queries, etc. Tizen :: Io :: DbStatementallows you to conveniently form a query and attach data to it: numeric, string, binary - of course, in the context of SQLite syntax. Tizen :: Io :: DbEnumerator is responsible for retrieving data from the query result. This is the minimum set for working with the database. In most cases, I use this particular set, however, under an additional self-written shell that automates some routine operations. Also in the Tizen SDK there are additional tools that facilitate work, for example: SqlStatementBuilder - conveniently generates simple queries, Data controls - data exchange between applications, Tizen :: Io :: DataSet , Tizen :: Io :: DataRow - for working with tables in memory , etc.

Let's start from the stove, i.e. directly from the SQLite database file. It can be created at the first launch of the application (and the documentation describes in detail how to do this), you can also create a table using an SQL query. I prefer to use an already generated SQLite file, with pre-prepared tables. For these purposes, it is convenient to use SQLite Manager , the plug-in for Firefox. The tool is cross-platform and quite functional. Those who have few opportunities can use the console utility from the official SQLite website.

The new database file is saved in the data directory, which is the root directory of the Tizen project (see the first part of the article). We need 2 tables: in one we will store purchases, and in the other - shopping lists:





As I already mentioned, in most projects for working with the database I use my own shell over 3 main classes: Tizen :: Io :: Database, Tizen :: Io :: DbStatement and Tizen :: Io :: DbEnumerator. The DataSet and DataRow classes are not mentioned in the documentation in the context of working with databases, except that they have one root namespace Tizen :: Io. It is not surprising that they were discovered by me after I wrote my own implementations (there is an excuse!). Be that as it may, they work, and you can see them in the commit under the v0.3 tag . I want to emphasize that I wrote the shell for myself, and in no way pretend to the special elegance of the code and architecture. Briefly list its main modules:
  • DbRow - an interface whose descendants encapsulate table rows;
  • DbDataSet - storage for the DbRow set;
  • DbAccess - responsible for connecting / disconnecting a database, reading single values ​​(GetValue method), reading / writing entire tables (FillDataSet, UploadDataSet) and executing command requests with parameters (PerformRequest);
  • DbQuery - combines a string query and its parameters in one object;
  • DbRowBuilder - interface for creating DbRow implementations, used by DbDataSet;
  • DbValue - the universal value of the field, the type of which is determined at creation;
  • DbRowValue is a standard implementation of DbRow; it is used when only 1 column needs to be read.

Let us return to the variation of the MVC pattern discussed in the first part. A full-fledged implementation of the pattern implies that all its modules must communicate with each other somehow. This can be done directly through pointers, through an intermediary, or through an event model, in general, there are many ways, with their pros and cons. In practice, I usually use a factory and an object manager that allows objects to communicate securely with each other through a message interface. Of course, since the objects of the model and controller will be present in a single copy, the first thing that comes to mind is to instantiate them through a singleton. The temptation is great, but in the real world the requirements for software are constantly changing, which means that any monoliths in the code will eventually become obsolete. I think this topic deserves a separate consideration, but now, Without being distracted by questions of architecture, we will place all the code in ShoppingListMainForm. I remind you that our project is educational!

The ShoppingListMainForm form is loaded once when the application starts, and closes when it exits, so connecting / disconnecting the database is convenient in the appropriate event handlers:

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

In the native development for Tizen, no exceptions are used; instead, it is customary to return the resulting system enum and always check it. In case of an error, I also add tracing to the log. At first it is very annoying, but it pays off when debugging. I generally forgot the last time I used the debugger - thanks to the logs, you can always find out what is happening in the program. There are several commands for logging messages, but I recommend using AppLogDebug. Firstly, it is deactivated in the release version, and the output of a large number of logs reduces performance. Secondly, Tizen itself pours tons of system messages into the log, and, as a rule, these messages are of the AppLogException type:



In most cases, they are useless, so I turn them off and leave only my own “debug” messages:



We see that I made a mistake - indicated the wrong file name. We replace lists1.sqlite with lists.sqlite, and the connection to the database is ready!

Now retrieve the names of the shopping lists. While there is nothing to extract, the database is empty, so in SQLite Manager we select the Lists table, go to the "Browse and Search" tab and add some test values:



To load the values, we need:

  1. DbDataSet table;
  2. the new List class is structures for storing rows of the Lists table;
  3. new RowList class inheriting from DbRow - provides row access for DbDataSet;
  4. a class that inherits DbRowBuilderInterface - it will generate RowList objects for the DbDataSet string;
  5. DbQuery - encapsulates an SQL query to the DbAccess database.


The form itself will inherit DbRowBuilderInterface. As a new line we will return RowList. If we needed only one column, we could use the ready-made DbRowValue class. When creating a table, do not forget to remember its identifier theDataSetIdGetLists - you will need it to determine which rows to pass to which table:

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; iGetInt(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;
}


We add the GetLists method call to the OnInitializing form initialization handler, start the project and observe the id and list names in the log:

> 1 Gifts
> 2 Products
> 3 Walking ammunition for the cat

How the project looks at this stage - see label v0.4 .

Now display the names of the lists in the ListView on the Tab1 tab. To do this, ShoppingListTab1 must have access to data downloaded from the database. If we had a full-fledged MVC, we would just turn to the model and take everything we needed, otherwise we would have to sculpt a crutch:

  1. We make the pointer to DbAccess static and open for external access;
  2. The data extraction mechanism is transferred from ShoppingListMainForm to ShoppingListTab1;
  3. We transfer the connection to the database from ShoppingListMainForm :: OnInitializing to the beginning of ShoppingListMainForm :: Initialize so that the database is connected before Tab1 is initialized.
  4. We make the DbDataSet with lists a member of the ShoppingListTab1 class.

It turned out, to put it mildly, not very beautiful - this is the result of neglect of architecture. Let's continue: we assign DbDataSet as the data source for the ListView and, finally, observe the names of the lists on the screen:



How the project looks at this stage - see label v0.5 .

In the second part of the article, the context menu of list items was considered, where we placed the Delete button. It is time to use the handler for this event:

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

In the database, delete the list itself and all its contents (purchases). After changes to the data source, you must also update the ListView so that the changes appear on the screen.

Potential trap: in order for the database file to be overwritten when you redeploy the application, you need to modify it (or remove the application from the target device). Otherwise, the old file will remain on the target device. This is done to quickly deploy the application so that you do not have to send megabytes of resource files every time.

Further work is more appropriate to track, studying changes in the code.

Label v0.6- the context menu (OptionMenu) with the Add command is attached to the left touch button. It brings up a dialog that invites you to enter the name of the new element. After confirmation, the dialog is closed, and a new record is added to the database and the DbDataSet table.

Label v0.7 - implemented work with purchases. Selecting a list on the Tab1 tab leads to the Tab2 tab, where the elements of this list are displayed. They can be edited: add, delete, mark completed. Completed purchases are displayed at the end of the list.

Label v0.8- the Search command has been added to the context menu. The search is carried out in a separate panel, it searches for both the names of the lists and the names of purchases. Selecting a search result leads to the transition to the appropriate tab (and scrolling to the desired item, if necessary).

Label v0.9 - we bring beauty: we colorize buttons, menus, panels using the recommended color palette and font sizes . Add localization of texts (Russian, English).



Finally, v1.0 is the final touch, namely, compressing the database with the VACUUM command when exiting the application.

On this our "Hello, world!" Can be considered complete. In the first part of the article, a variation of the MVC pattern was described, however, its implementation was postponed to facilitate understanding (and presentation) of the material. A useful thing in the context of native Tizen applications needs a separate consideration. In addition, the Tizen SDK offers convenient built-in tools for connecting the GUI and the main application code with data. I want to devote the following article to the description of these mechanisms. I hope it wasn’t particularly boring - I will be glad to answer questions :) May the strength of Tai'Zen be with you!

Also popular now: