Créer une application multiplateforme AIR utilisant SQLite

Julien Fray

Dans ce tutoriel nous allons créer avec Flex Builder 3 un exemple d'application AIR utilisant SQLite.
Cette application doit permettre de gérer une liste de contacts stockée dans une base de données.

Consultez aussi l'article sur l'utilisation de SQLite dans AIR.

Commentez ce tutoriel : Commentez

- Titre : Créer une application multiplateforme AIR utilisant SQLite

- Auteur : Julien Fray

- Parution : 2 septembre 2010

- Mise à jour : 30 avril 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 2. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Étape 1 : création d'un nouveau projet AIR

- Étape 2 : définition de l'objet Contact

- Étape 3 : initialisation des classes d'accès aux données

- Étape 4 : initialisation de la base de données

- Étape 5 : création de l'interface pour l'ajout d'un contact

- Étape 6 : insérer un nouveau contact

- Étape 7 : afficher la liste des contacts

- Étape 8 : modification d'un contact

- Étape 9 : suppression d'un contact

- Étape 10 : créer une pagination pour la liste des contacts

- Conclusion

Étape 1 : création d'un nouveau projet AIR

La première chose à faire est d'initialiser un nouveau projet AIR en utilisant le menu suivant :
File > New > Flex Project

Sélectionnez ensuite l'option application de bureau AIR (Desktop application) puis validez.

Le dossier source de l'application créée contient le fichier mxml avec la définition de l'interface principale de l'application :

Interface par défaut

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical">
</mx:WindowedApplication>

Le projet sera organisé avec les trois packages suivants : BO pour les objets métiers (ici une seule classe Contact), DB pour les classes d'accès aux données de la base et GUI qui contiendra les interfaces de l'application.

Étape 2 : définition de l'objet Contact

Pour commencer il faut définir notre contact. Les propriétés d'un contact sont un pseudonyme, une date de naissance, une adresse mail et un avatar.
Pour stocker l'avatar on utilise une variable de type ByteArray. La propriété ID correspondra à un identifiant auto-incrémenté dans la base de données.

classe Contact

package BO
{
import flash.utils.ByteArray;

public class Contact
{
public var id:int;
public var pseudo:String;
public var dateNaissance:Date;
public var mail:String;
public var avatar:ByteArray;

public function Contact()
{
ID = -1;
Pseudo = "";
Mail = "";
DateNaissance = null;
Avatar = null;
}

public function get ID():int
{
return id;
}
public function set ID(pValue:int):void
{
id = pValue;
}

public function get Pseudo():String
{
return pseudo;
}
public function set Pseudo(pValue:String):void
{
pseudo = pValue;
}

public function get Mail():String
{
return mail;
}
public function set Mail(pValue:String):void
{
mail = pValue;
}

public function get Avatar():ByteArray
{
return avatar;
}
public function set Avatar(pValue:ByteArray):void
{
avatar = pValue;
}

public function get DateNaissance():Date
{
return dateNaissance;
}
public function set DateNaissance(pDate:Date):void
{
dateNaissance = pDate;
}
}
}

Étape 3 : initialisation des classes d'accès aux données

Il faut définir une interface d'accès à notre base de contacts qui fournira les fonctionnalités suivantes : la sélection des contacts de la base, l'insertion, la mise à jour et la suppression de contacts.

Nous allons créer une classe Database dans le package DB pour effectuer l'exécution des requêtes.
Nous devrons donc gérer au sein de cette classe le fichier de la base, la connexion à la base, l'initialisation et l'exécution des objets SQLStatement.

Il est aussi nécessaire de gérer les erreurs lors de l'exécution des requêtes et de permettre de récupérer la description d'une éventuelle erreur.
Afin d'alléger un peu le code, la classe Database héritera d'une classe Logger, qui permettra de stocker et de mettre à disposition la description des erreurs survenues.

La classe Logger :

Classe Logger

package DB
{
import flash.events.EventDispatcher;

public class Logger extends EventDispatcher
{
private var errorMessageList:Array;

public function get LastErrorMessage():String
{
return errorMessageList.length > 0 ? errorMessageList[errorMessageList.length-1] : "No error";
}

public function Logger()
{
errorMessageList = new Array();
}

protected function logError(pErrorMessage:String):void
{
errorMessageList.push(pErrorMessage);
trace(pErrorMessage);
}
}
}

La classe Database est uniquement initialisée (voir ci-desous), les différentes méthodes nécessaires seront rajoutées au moment voulu.
La classe Database possède donc une référence vers le fichier de la base qui est initialisée dans le constructeur et qui ne changera pas.

classe Database

package DB
{
import flash.filesystem.File;
import flash.data.SQLConnection;
import flash.data.SQLStatement;

public class Database extends Logger
{
private var databaseFile:File;

public function Database()
{
databaseFile = File.applicationStorageDirectory.resolvePath("database.sqlite");
}
}
}

Pour pouvoir accéder facilement à la base de contacts dans le reste de l'application, la classe Database sera un Singleton (une instance unique accessible de façon statique).
L'instance est accessible avec la propriété Database.Instance.

classe Database

package DB
{
import flash.filesystem.File;

public class Database extends Logger
{
private static var constructorKey:Object = {};
private static var instance:Database = null;

private var databaseFile:File;

public function Database(pConstructorKey:Object)
{
if(pConstructorKey != constructorKey)
{
throw new Error("Illegal instanciation (private constructor)");
}

databaseFile = File.applicationStorageDirectory.resolvePath("database.sqlite");
}

public static function get Instance():Database
{
if(instance == null)
{
instance = new Database(constructorKey);
}
return instance;
}
}
}

Étape 4 : initialisation de la base de données

Lors de l'initialisation de l'application il faut vérifier si la base existe et la créer dans le cas contraire. Nous créons donc une méthode createDatabase qui retourne un booléen pour indiquer si la création s'est bien déroulée.

Méthode de création de la base

public function createDatabase():Boolean
{
if(databaseFile.exists)
{
return true;
}
// la suite ici
}

Premièrement il faut construire la requête de création de la table Contact. Il faut s'assurer que les noms des champs de la table correspondent aux noms des propriétés de l'objet Contact créé précédemment. La requête est la suivante :

Requête de création

CREATE TABLE Contact (ID INTEGER PRIMARY KEY, Pseudo TEXT, Mail TEXT, DateNaissance Date, Avatar OBJECT )

Nous devons maintenant créer une méthode pour exécuter la requête, méthode qui sera privée car uniquement à utiliser dans cette classe. Elle assure l'ouverture et la fermeture de la connexion, la définition d'une variable SQLStatement et son exécution puis retourne le résultat de la requête. Si une erreur se produit, la valeur null est retournée.

Méthode pour l'exécution des requêtes

private function executeQuery(pMode:SQLMode, pQuery:String):SQLResult
{
try
{
var Connection:SQLConnection = new SQLConnection();
var Statement:SQLStatement = new SQLStatement();

Connection.open(databaseFile, pMode);

Statement.sqlConnection = Connection;

Statement.text = pQuery;
Statement.execute();

return Statement.getResult();
}
catch(error:SQLError)
{
this.logError("Requête : <" + pQuery + ">\n" + error.toString());
}
finally
{
Connection.close();
}
return null;
}

La méthode de création est donc la suivante :

Méthode de création de la base

public function createDatabase():Boolean
{
if(databaseFile.exists)
{
return true;
}

var sQuery:String = "CREATE TABLE Contact (";

sQuery += "ID INTEGER PRIMARY KEY, "
sQuery += "Pseudo TEXT, "
sQuery += "Mail TEXT, "
sQuery += "DateNaissance Date, "
sQuery += "Avatar OBJECT "
sQuery += ")";

var result:SQLResult = this.executeCreateQuery(SQLMode.CREATE, sQuery);

return result != null;
}

Il reste à appeler cette méthode lors de l'initialisation de l'interface principale, pour cela il faut définir l'évènement creationComplete de l'application dans le fichier principal (TutoDVP.mxml) :

Interface principale

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="InitApplication()"
width="500" height="470" title="Tutoriel AIR SQLite developpez" viewSourceURL="srcview/index.html">

<mx:Script>
<![CDATA[
import mx.controls.Alert;
import DB.Database;

private function initApplication():void
{
if(!Database.Instance.createDatabase())
{
Alert.show("Erreur lors de la création de la base :\n" + Database.Instance.LastErrorMessage, "Tutoriel SQLite developpez");
}else{
Alert.show("Base créée.", "Tutoriel SQLite developpez");
}
}

] ]>
</mx:Script>

</mx:WindowedApplication>

Vous pouvez déjà lancer l'application pour créer la base !

On peut ensuite vérifier que la table est bien créée à l'aide d'un utilitaire qui permet d'explorer les bases SQLite :

Étape 5 : création de l'interface pour l'ajout d'un contact

Maintenant que notre base de données est créée il faut y insérer des données. Nous allons créer une fenêtre contenant un formulaire pour la création d'un nouveau contact. Cette fenêtre sera un composant basé sur un objet TitleWindow. Pour le créer, sélectionnez le menu correspondant, puis donnez un nom et une classe de base comme ci-dessous :

Cette fenêtre va contenir un formulaire comprenant trois champs de saisie pour le pseudonyme, l'adresse mail et la date de naissance, puis une image avec un bouton pour sélectionner l'avatar.
Lorsque le formulaire est rempli et validé, un objet Contact est créé à partir des différents éléments.

Fenêtre avec formulaire de création

<?xml version="1.0" encoding="utf-8"?>
<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" width="450" height="350" title="Nouveau contact">

<mx:Script>
<![CDATA[
import mx.controls.Alert;
import mx.managers.DragManager;
import mx.graphics.codec.JPEGEncoder;
import mx.validators.Validator;
import mx.managers.PopUpManager;

import BO.Contact;

public function closeWindow():void
{
PopUpManager.removePopUp(this);
}

public function validateForm():void
{
var validators:Array = Validator.validateAll([validMail, validPseudo, validDate]);

if(!validators.length)
{
// Formulaire valide
var co:Contact = new Contact();

co.Pseudo = inputPseudo.text;
co.Mail = inputMail.text;
co.DateNaissance = inputDate.selectedDate;
co.Avatar = imageToByteArray(inputAvatar);

trace(co);
}
}

private function imageToByteArray(img:Image):ByteArray
{
var bmd:BitmapData = new BitmapData(img.width, img.height, true);
bmd.draw(img, new Matrix());
var bm:Bitmap = new Bitmap(bmd);
var jpg:JPEGEncoder = new JPEGEncoder();
return jpg.encode(bmd);
}

private function selectImage():void
{
var imagesFilter:FileFilter = new FileFilter("Images", "*.jpg;*.gif;*.png;*.swf;");
var imageFile:File = new File();
imageFile.browseForOpen("Selectionnez une image", [imagesFilter]);
imageFile.addEventListener(Event.SELECT, imageSelected);
}

private function imageSelected(pEvent:Event):void
{
inputAvatar.source = (pEvent.currentTarget as File).url;
}
] ]>
</mx:Script>

<mx:StringValidator id="validPseudo" source="{inputPseudo}" property="text" trigger="{btnValidate}"
triggerEvent="click" minLength="2" maxLength="20"
tooShortError="2 caractères minimum !" tooLongError="Pseudonyme trop long !"
requiredFieldError="Veuillez indiquer un Pseudonyme." />

<mx:EmailValidator id="validMail" source="{inputMail}" property="text" trigger="{btnValidate}"
triggerEvent="click"
requiredFieldError="Veuillez indiquer une adresse mail."
missingAtSignError="Le charactere @ est manquant dans votre adresse email."
missingPeriodInDomainError="Le domaine est manquant dans votre adresse email."
missingUsernameError="Le nom d'utilisateur est manquant dans votre adresse email." />

<mx:DateValidator id="validDate" source="{inputDate}" property="text" trigger="{btnValidate}"
triggerEvent="click" allowedFormatChars="/" inputFormat="DD/MM/YYYY"
requiredFieldError="Entrez la date de naissance" />

<mx:Form width="100%">

<mx:FormItem label="Pseudo">
<mx:TextInput id="inputPseudo" />
</mx:FormItem>

<mx:FormItem label="Adresse mail">
<mx:TextInput id="inputMail" />
</mx:FormItem>

<mx:FormItem label="Date de naissance">
<mx:DateField id="inputDate" editable="true" formatString="DD/MM/YYYY" />
</mx:FormItem>

<mx:FormItem label="Avatar">
<mx:HBox id="boxAvatar">
<mx:Canvas borderThickness="2" borderStyle="solid">
<mx:Image id="inputAvatar" scaleContent="{ckScale.selected}" maintainAspectRatio="true" width="170" height="130" />
</mx:Canvas>
<mx:VBox>
<mx:Button label="Parcourir" click="selectImage()" />
<mx:CheckBox id="ckScale" selected="true" label="Ajuster" />
</mx:VBox>
</mx:HBox>
</mx:FormItem>

</mx:Form>

<mx:HBox width="100%" horizontalAlign="center">

<mx:Spacer width="100%" />
<mx:Button label="Annuler" click="closeWindow()" />

<mx:Spacer width="30%" />

<mx:Button id="btnValidate" label="Valider" click="validateForm()" />
<mx:Spacer width="100%" />

</mx:HBox>

</mx:TitleWindow>

Dans l'interface principale on ajoute un bouton "Nouveau contact" qui va ouvrir la fenêtre de création de contacts :

<mx:HBox>
<mx:Spacer width="100%" />
<mx:Button label="Nouveau contact" horizontalCenter="true" click="addContact()" />
<mx:Spacer width="100%" />
</mx:HBox>

avec la fonction addContact qui utilise la classe PopUpManager :

private function addContact():void
{
var window:IFlexDisplayObject = PopUpManager.createPopUp(this, ContactWindow , true);
var CreationWindow:ContactWindow = ContactWindow(window);
PopUpManager.centerPopUp(CreationWindow);
}

L'interface est maintenant créée, lancez l'application pour vérifier que la fenêtre réagit bien.

Bien sûr il faut maintenant créer la fonction d'insertion du contact dans la base de données.

Étape 6 : insérer un nouveau contact

Pour l'insertion du nouveau contact nous allons créer la méthode correspondante dans la classe Database. Comme pour la création de la base il faut d'abord construire la requête puis l'exécuter. Nous devons en plus récupérer le nouvel identifiant automatique généré.

L'exécution de cette requête nécessite la définition de paramètres, il faut donc enrichir la méthode executeQuery avec un nouveau paramètre pParameters de type Object. Ce paramètre aura la valeur null par défaut pour ne pas modifier le comportement initial de la méthode.

Méthode pour l'exécution des requêtes

private function executeQuery(pMode:String, pQuery:String, pParameters:Object = null):SQLResult
{
try
{
var Connection:SQLConnection = new SQLConnection();
var Statement:SQLStatement = new SQLStatement();

Connection.open(databaseFile, pMode);

Statement.sqlConnection = Connection;

Statement.text = pQuery;

if(pParameters != null)
{
for(var id:String in pParameters)
{
Statement.parameters[id] = pParameters[id];
}
}

Statement.execute();
return Statement.getResult();
}
catch(error:SQLError)
{
this.logError("Requête : <" + pQuery + ">\n" + error.toString());
}
finally
{
Connection.close();
}
return null;
}

La requête à exécuter est la suivante, les paramètres sont préfixés par le caractère @ :

Requête d'insertion

INSERT INTO Contact (Pseudo, Mail, DateNaissance, Avatar) VALUES (@Pseudo, @Mail, @DateNaissance, @Avatar)

La méthode insertContact lance l'exécution en spécifiant la requête ainsi que les paramètres puis récupère le nouvel identifiant du contact :

Méthode pour l'insertion d'un contact dans la base

public function insertContact(pContact:Contact):Boolean
{
var sQuery:String = "INSERT INTO Contact (Pseudo, Mail, DateNaissance, Avatar) VALUES (@Pseudo, @Mail, @DateNaissance, @Avatar)";
var params:Object = {"@Pseudo" : pContact.Pseudo, "@Mail" : pContact.Mail, "@DateNaissance" : pContact.DateNaissance, "@Avatar" : pContact.Avatar};

var result:SQLResult = this.executeQuery(SQLMode.UPDATE, sQuery, params);

if(result != null)
{
pContact.ID = result.lastInsertRowID;
return true;
}

return false;
}

Retournons dans la fenêtre de création pour appeler la méthode insertContact :

var co:Contact = new Contact();
co.Pseudo = inputPseudo.text;
co.Mail = inputMail.text;
co.DateNaissance = inputDate.selectedDate;
co.Avatar = imageToByteArray(inputAvatar);

if(Database.Instance.insertContact(co))
{
Close();
}
else
{
Alert.show("Erreur lors de l'enregistrement\n\n" + Database.Instance.LastErrorMessage, "Tutoriel SQLite developpez");
}

Lancez l'application, l'interface est fonctionnelle. Vous pouvez maintenant ajouter des contacts dans la base.

Étape 7 : afficher la liste des contacts

Premièrement, nous allons récupérer la liste des contacts de la base, puis nous verrons le moyen de l'afficher.

Nous allons utiliser notre méthode executeQuery mais il faut y ajouter la gestion du type des résultats en ajoutant un nouveau paramètre pClass.

Méthode pour l'exécution des requêtes

private function executeQuery(pMode:String, pQuery:String, pParameters:Object = null, pClass:Class=null):SQLResult
{
try
{
var Connection:SQLConnection = new SQLConnection();
var Statement:SQLStatement = new SQLStatement();
Connection.open(databaseFile, pMode);

Statement.sqlConnection = Connection;

Statement.text = pQuery;

if(pParameters != null)
{
for(var id:String in pParameters)
{
Statement.parameters[id] = pParameters[id];
}
}
if(pClass != null)
{
Statement.itemClass = pClass;
}

Statement.execute();
return Statement.getResult();
}
catch(error:SQLError)
{
this.logError("Requête : <" + pQuery + ">\n" + error.toString());
}
finally
{
Connection.close();
}
return null;
}

La méthode de récupération des contacts est ensuite très simple :

public function getContactList():Array
{
var result:SQLResult = this.executeQuery(SQLMode.READ, "SELECT * FROM Contact", null, Contact);

if(result != null)
{
return result.data == null ? new Array() : result.data;
}
return null;
}

Nous allons initialiser une liste de contacts dans l'interface principale et lancer le chargement lors de l'initialisation :

private var contactList:ArrayCollection;

private function initApplication():void
{
if(!Database.Instance.createDatabase())
{
Alert.show("Erreur lors de la création de la base :\n" + Database.Instance.LastErrorMessage, "Tutoriel SQLite developpez");
}else{
var ReturnedList:Array = Database.Instance.getContactList();

if(ReturnedList == null)
{
Alert.show("Erreur lors de la récupération des contacts de la base :\n" + Database.Instance.LastErrorMessage, "Tutoriel SQLite developpez");
}else{
contactList = new ArrayCollection(ReturnedList);
}
}
}

La variable contactList contient donc les contacts de la base. Les éléments de cette liste seront affichés à l'aide d'un composant TileList.
Deux propriétés principales sont à définir pour utiliser le composant :

- la première est la propriété dataProvider, elle permet de définir la source de données du composant, ici la liste des contacts ;

- la seconde itemRenderer définit le composant utilisé pour afficher un élément de la liste.

Nous allons ensuite créer ce composant.

L'objet TileList ajouté dans l'interface principale :

<mx:TileList dataProvider="{ContactList}" itemRenderer="GUI.ContactRenderer"
width="440" height="100%" maxColumns="1" columnWidth="420" />

Nous allons créer le composant ContactRenderer qui définit le rendu d'un contact de la liste à partir d'un simple composant HBox.

Dans le composant, l'objet data est utilisé pour accéder à l'élément courant. Il faut ensuite associer les propriétés de cet objet aux éléments de l'interface. Par exemple, pour afficher l'avatar du contact on associe data.Avatar à la source d'un objet Image :

<mx:Image source="{data.Avatar}" />

De même pour la description du contact :

<mx:Label text="{data.Pseudo + ' (' + data.Age + ' ans)'}" />

Cette technique est appelée Binding et permet de mettre à jour l'interface automatiquement lorsque la propriété est modifiée. Pour pouvoir lier les attributs de la classe Contact il faut les définir comme étant Bindable avec la syntaxe suivante :

[Bindable]
public function get Pseudo():String
{
return pseudo;
}

Dans la classe Contact une propriété (getter) Age a été ajoutée pour afficher l'âge à partir de la date de naissance. Avec une propriété en lecture seule le changement de valeur ne peut être détecté pour le Binding avec l'interface. Il faut donc spécifier un événement particulier (ici "agePropertyChanged") :

[Bindable(event='agePropertyChanged')]
public function get Age():uint
{
var dtNow:Date = new Date();
var currentMonth:Number = dtNow.getMonth();
var currentDay:Number = dtNow.getDay();
var currentYear:Number = dtNow.getFullYear();
var bdMonth:Number = DateNaissance.getMonth();
var bdDay:Number = DateNaissance.getDay();
var bdYear:Number = DateNaissance.getFullYear();
var years:uint = dtNow.getFullYear() - DateNaissance.getFullYear();

if (currentMonth < bdMonth || (currentMonth == bdMonth && currentDay < bdDay))
{
years--;
}
return years;
}

Il est nécessaire de déclencher cet évènement lorsque la date de naissance est modifiée :

public function set DateNaissance(pDate:Date):void
{
dateNaissance = pDate;
dispatchEvent(new Event('agePropertyChanged'));
}

Le code complet du composant ContactRenderer est le suivant :

Composant pour l'affichage d'un contact

<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" height="160" width="100%" paddingTop="5" paddingBottom="5" paddingLeft="5" paddingRight="5">

<mx:HBox height="150" width="100%" borderStyle="solid" borderThickness="2" paddingTop="5" paddingBottom="5" paddingLeft="5" paddingRight="5">

<mx:Image source="{data.Avatar}" scaleContent="true" width="170" height="130" />

<mx:VBox height="100%" verticalAlign="middle" paddingLeft="10" width="100%" horizontalScrollPolicy="auto">

<mx:Label text="{data.Pseudo + ' (' + data.Age + ' ans)'}" />
<mx:Label text="{data.Mail}" />

</mx:VBox>

<mx:Spacer width="20" />

</mx:HBox>
</mx:HBox>

Vous pouvez lancer l'application et visualiser les contacts créés auparavant :

L'application permet maintenant de créer et visualiser les contacts, par contre lorsqu'un contact est ajouté, la liste ne se met pas à jour. Pour remédier à ce problème il faut que la classe Database émette un évènement lorsqu'un contact est ajouté. Ainsi l'interface pourra mettre à jour la liste lorsque l'évènement est détecté.

Définition du nom de l'évènement dans la classe Database :

public static const DATABASE_CHANGE_EVENT_NAME:String = "DATABASE_CHANGE_EVENT_NAME";

Émission de l'évènement dans la méthode insertContact :

dispatchEvent(new Event(DATABASE_CHANGE_EVENT_NAME));

Pour disposer de la méthode dispatchEvent, la classe doit hériter de EventDispatcher. C'est la classe de base Logger qui va en hériter.

L'interface principale écoute l'évènement et lance la mise à jour de la liste lorsqu'il est détecté :

Database.Instance.addEventListener(Database.DATABASE_CHANGE_EVENT_NAME, function(event:Event):void
{
updateList();
});

Étape 8 : modification d'un contact

Pour implémenter l'édition d'un contact dans notre application nous allons utiliser la fenêtre existante. Il faut simplement ajouter une méthode pour initialiser l'interface avec un contact particulier et ensuite modifier le comportement lors de la validation de la fenêtre pour ajouter ou modifier le contact suivant le cas d'utilisation. Une propriété editedContact est ajoutée pour mémoriser le contact édité.

private var editedContact:Contact;

La fonction d'initialisation lors de l'édition :

public function initEdition(pContact:Contact):void
{
editedContact = pContact;
inputPseudo.text = pContact.Pseudo;
inputMail.text = pContact.Mail;
inputDate.selectedDate = pContact.DateNaissance;
inputAvatar.source = pContact.Avatar;
}

Validation de la fenêtre :

// Formulaire valide

var Edition:Boolean = editedContact != null;

if(editedContact == null)
{
editedContact = new Contact();
}

editedContact.Pseudo = inputPseudo.text;
editedContact.Mail = inputMail.text;
editedContact.DateNaissance = inputDate.selectedDate;
editedContact.Avatar = ImageToByteArray(inputAvatar);

var result:Boolean = false;
if(!Edition)
{
result = Database.Instance.InsertContact(editedContact);
}
else
{
result = Database.Instance.UpdateContact(editedContact);
}
if(result)
{
Close();
}
else
{
Alert.show("Erreur lors de l'enregistrement\n\n" + Database.Instance.LastErrorMessage, "Tutoriel SQLite developpez");
}

La méthode updateContact de la classe Database est créée à partir de la méthode insertContact :

Méthode de mise à jour d'un contact

public function updateContact(pContact:Contact):Boolean
{
var sQuery:String = "UPDATE Contact SET Pseudo=@Pseudo, Mail=@Mail, DateNaissance=@DateNaissance, Avatar=@Avatar WHERE ID=@ID";
var params:Object = {"@Pseudo" : pContact.Pseudo, "@Mail" : pContact.Mail, "@DateNaissance" : pContact.DateNaissance, "@Avatar" : pContact.Avatar, "@ID" : pContact.ID};

var result:SQLResult = this.executeQuery(SQLMode.UPDATE, sQuery, params);

return result != null;
}

Dans le composant de rendu du contact dans la liste nous ajoutons un bouton "Éditer" qui va ouvrir la fenêtre d'édition du contact :

<mx:LinkButton label="Editer" click="editContact()" />

Avec la fonction editContact qui initialise la fenêtre :

private function editContact():void
{
var window:IFlexDisplayObject = PopUpManager.createPopUp(this.parent.parent.parent, ContactWindow , true);
var EditionWindow:ContactWindow = ContactWindow(window);
EditionWindow.initEdition(data as Contact);
PopUpManager.centerPopUp(EditionWindow);
}

Grâce au Binding des éléments de la liste, une fois le contact édité, la mise à jour de l'interface est automatique.

Étape 9 : suppression d'un contact

Pour la suppression d'un contact un autre bouton "Supprimer" est ajouté :

<mx:LinkButton label="Supprimer" click="delete()" />

Avant de supprimer le contact, une confirmation de la part de l'utilisateur n'est jamais de trop.
La méthode confirm ci-dessous permet de l'effectuer plus simplement :

private function confirm(pMessage:String, pThisObject:Object, pHandler:Function):void
{
Alert.show(pMessage,"Confirmation",Alert.OK|Alert.CANCEL,null,function(evt:CloseEvent):void
{
pHandler.apply(pThisObject, [evt.detail == Alert.OK]);
},null,Alert.OK);
}

Elle est utilisée de la façon suivante avant d'appeler la méthode de la classe Database :

private function delete():void
{
confirm("Supprimer le contact ?", this, deleteContact);
}

private function deleteContact(pResult:Boolean):void
{
if(pResult)
{
Database.Instance.deleteContact(data as Contact);
}
}

La méthode deleteContact de la classe Database est presque similaire à la méthode de mise à jour :

Méthode de suppréssion d'un contact

public function deleteContact(pContact:Contact):Boolean
{
var sQuery:String = "DELETE FROM Contact WHERE ID=@ID";
var params:Object = {"@ID" : pContact.ID};

var result:SQLResult = this.executeQuery(SQLMode.UPDATE, sQuery, params);
dispatchEvent(new Event(DATABASE_CHANGE_EVENT_NAME));

return result != null;
}

Étape 10 : créer une pagination pour la liste des contacts

Nous allons effectuer une pagination sur la liste de contacts, ce qui revient à limiter le nombre d'éléments de la liste et à placer des boutons pour parcourir la liste.

La pagination reste indépendante du composant utilisé pour afficher la liste de contact.

Quatre boutons sont nécessaires : page suivante, page précédente, première page et dernière page.
Un label doit indiquer les contacts affichés ainsi que le nombre total de contacts.

L'interface est donc la suivante (le bouton "Nouveau contact" a été déplacé) :

Pour implémenter cette pagination nous avons besoin de connaître le nombre total d'éléments.
Dans la classe Database la méthode getContactCount récupère cette valeur qui est stockée dans une variable contactCount :

public function getContactCount():uint
{
var result:SQLResult = this.executeQuery(SQLMode.READ, "SELECT COUNT(ID) as RecordCount FROM Contact");

if(result != null && result.data != null)
{
return result.data[0].RecordCount;
}
return null;
}

Une variable pageItemCount indique le nombre d'éléments dans une page.
Ce nombre est défini à deux dans l'exemple mais peut donc être changé simplement si besoin.

Pour pouvoir griser les boutons suivant les éléments affichés nous utilisons deux autres variables qui indiquent si il y a des éléments précédents ou suivants.

Deux dernières variables mémorisent les index du premier et du dernier élément de la liste.
Elles sont utilisées pour pouvoir récupérer les contacts correspondants dans la base de données.

Pour récupérer seulement une partie des contacts, il faut modifier la fonction getContactList de la classe Database, en y ajoutant deux paramètres pour le nombre d'éléments à sélectionner ainsi que l'index du premier élément :

public function getContactList(pStartIndex:uint, pCount:uint):Array
{
var sQuery:String = "SELECT * FROM Contact LIMIT @count OFFSET @start";
var params:Object = {"@start" : pStartIndex, "@count" : pCount};

var result:SQLResult = this.executeQuery(SQLMode.READ, sQuery, params, Contact);

if(result != null)
{
return result.data == null ? new Array() : result.data;
}
return null;
}

Dans l'interface principale les quatre méthodes de navigation sont les suivantes :

private function firstPage():void
{
firstItemIndex = 0;
updateList();
}

private function nextPage():void
{
firstItemIndex += pageItemCount;
updateList();
}

private function previousPage():void
{
firstItemIndex -= pageItemCount;
updateList();
}

private function lastPage():void
{
firstItemIndex = contactCount - contactCount % pageItemCount;
updateList();
}

Pour naviguer dans la liste, les méthodes mettent à jour l'index du premier élément à afficher, puis lancer la méthode de mise jour de la liste.
Lorsqu'un contact est inséré, supprimé ou encore lors de l'initialisation cette mise à jour doit aussi récupérer le nombre de contacts de la base.
La méthode updateList modifiée est la suivante :

private function updateList(pUpdateCount:Boolean=false):void
{
if(pUpdateCount)
{
contactCount = Database.Instance.getContactCount();
firstItemIndex = 0;
}

var ReturnedList:Array = Database.Instance.getContactList(firstItemIndex, pageItemCount);

if(ReturnedList == null)
{
Alert.show("Erreur lors de la récupération des contacts de la base :\n" + Database.Instance.LastErrorMessage, "Tutoriel SQLite developpez");
}else{
contactList = new ArrayCollection(ReturnedList);

lastItemIndex = firstItemIndex + contactList.length;
hasNextItemToDisplay = lastItemIndex < contactCount;
hasPreviousItemToDisplay = firstItemIndex > 0;
}
}

La pagination est terminée, les boutons peuvent être utilisés pour afficher les différents contacts de la base :

Conclusion

Les sources de l'application sont disponibles ICI.

L'application à installer est disponible ICI.
(clic droit pour afficher le code source à partir de l'application)

Liens utiles:

- présentation AIR [lien]

- le forum SQLite [lien]

- le forum Flex [lien]

Consultez aussi l'article sur l'utilisation de SQLite dans AIR.

Un grand merci à ellene pour son aide et à jacques_jean et Kerod pour leur relecture.