Flex, AMF, PHP and Zend Framework
If you missed the last week announcements coming from Adobe and Zend, then you might want to read this post.
Basically, the two companies announced a join effort to make easier the developing of PHP applications with Flex frontends.The two companies will deliver technologies, content and services to make it easy for enterprise developers to build rich Internet applications (RIAs) using Flex on the client and PHP on the server.
Part of this joined effort, are the support for AMF in the Zend Framework, a better experience for developers when working with Flex Builder and Zend Studio (both these IDEs are build on top of Eclipse).
My recent article from Adobe Developer Connection, Working with Flex and PHP in Eclipse, shows the things and some workarounds you can do today. I expect a much better experience in the near future and I plan to write back and to record some videos on this subject. Here is a link to the original announcement.
A chance to win a trip to Adobe MAX San Francisco 2008
If you want to go to MAX San Francisco and you don’t have a ticket, maybe you can win one. Here is the press release from ZIO Pro:
“”NEW YORK, New York – September 11th, 2008. Adobe Systems, the leading creative software company, and ZIO Pro, the new media network for creative professionals, are offering an exclusive opportunity to send a Creative or IT Director to Adobe’s MAX 2008 conference in San Francisco.
Registrants will be entered for a chance to win a trip to Adobe MAX 2008 in San Francisco. The winner will receive a free all access pass to the even as well as free air travel to San Francisco and free hotel stay. No obligation and no credit card information necessary.
In addition, every registrant for the contest will get access to a 30 Day 10 Seat software training license for their team - absolutely free – including full length training courses on Dreamweaver CS3, Flash CS3, and Photoshop CS3. Every qualified registrant will also receive a free e-Book – ‘Adobe Air with Ajax’ or ‘Presentation Zen’.”
To register for this limited time offer:
IT Directors/Managers can go here.
Creative Directors/Managers can go here.
Good luck
ZuiPrezi, one of the sexiest online application for making presentations
…and is created in Flex, too
If you want to see something cool, I encourage you to take a look at this application: http://test.zuiprezi.com/. I loved it from the first time I saw it. There are probably many presentation apps out there, but this one has a nice and sexy feature: you can zoom-in or out in your presentation, as part of your script.
Let me explain better an interesting use case you can cover with this app. Suppose you have to present a complex concept. And you decide to go from a nice overview, to each component piece. Here comes this tool. You can create a big slide composed out of the small pieces, and then as you presenting, you just move in and out. I think this visual analogy can make your presentation better received and understand. I am planing to use it myself for a presentation, … to test these assumptions
More than a week in a small Greek island
Although the summer has ended, the weather is quite good in Greece. And I hope there are not so many tourist at this time of the year, because I am going there on my vacation. I am leaving this Sunday.
Don’t worry, I will keep publishing some articles next week
AIR: Encrypted local storage and Update framework
In a previous entry I wrote about the different ways you have to store data in an AIR application. One way is to use the encrypted local storage. The signature of the method that you use to place something in “vault” is:
EncryptedLocalStore.setItem(name:String, data:ByteArray, stronglyBound:Boolean = false):void
Usually, you use just the first two arguments: name - the key you use to store and to retrieve the data, and data - what you want to store. However the third argument is very interesting. If you set this to true, then the stored item is strongly bound to the digital signature and bits of the AIR application in addition to the publisher id. This means that if the bits of the installed application are changing, then the previous stored information in the encrypted storage cannot be read anymore.
This is great because it gives you all the security you might need - if somehow, some malicious files are injected into your application, the data are protected and cannot be read.
But in some situations, it is not so great. If you use the update framework, after each update the bits are changed and thus the information is lost. So, I spent some time thinking on this issue and I came to the conclusion that the only solution is this:
- during the update detection read the data from the encrypted store and write back using stronglyBound set to false;
- when the application is re-started, check if you have something in the above store, and if you have write it back to store with stronglyBound set to true and delete the transient data.
So, this is the only solution I came with for this. I am curious; how do you handle this?
Kind of a Data Service implementation for PHP using AMFPHP and RemoteObject
Last week I played a little bit with AMFPHP and Flex and I put together this code. Basically I’ve built a Flex service that mimics (a little bit) the data services from LiveCycle Data Services. From the first time I worked with Data Services, I really liked the simple API you can use to retrieve data from the server and to send it back. In short, the workflow in the client goes like this:
- Create an instance of Data Service, and set the destination for i.
- Call the method fill() on this instance, and provide an ArrayCollection to be filled with what the server sends back to the client.
- Any changes you might make on this ArrayCollection (for example you might add or delete an item) are automatically sent to the server by the Data Service instance if you set the auto-commit property to true. Or, if you set auto-commit to false you can just call the commit() method and you are done.
- If you want to revert all the changes that were not committed yet, just call revertChanges().
Of course, this is not the complete story behind data services. They can do a lot more:
- synchronization (automatically push the changes that one client makes to all the other clients)
- lazy loading
- paging (if the collection is really big, you can retrieve just a few rows and as the UI of your application needs more, the data are retrieved automatically for you)
- conflict resolution (if someone else modified the same piece of information as you, you can choose what to do: overwrite their change or accept it)
- handle complex domain objects (hierarchies and different kind of associations).
And finally, if you want to create items using atomic operations, you can use: createItem(item), deleteItem(item).
My code doesn’t try to replicate all these features (that would be be a huge task). Instead, I’ve just tried to leverage the RemoteObject and AMFPHP, so as a Flex developer I have a smoother way to manage a collection of data. And because I used RemoteObject, I can use Value-Objects as a data model for an item and these objects are translated automatically for me by the framework (so, on the server I get arrays of VOs of PHP classes, and on the client I get VOs of ActionScript classes). Click here for a taste of what I hacked together (I made some restrictions so you cannot change the existent four records, but you can mess around with new ones). You can view the source code, just right click and choose view source.
For me, the beauty is that most of the time I can concentrate on my data model (add/delete/change items) and when I’m done, just call the commit() method on the PHPDataService, as opposed to using a HTTPService, which would require multiple calls on the service object, in addition to work on the data model.
Flex code
Below is a snippet of code that shows how to use my service (PHPDataService):
<mx:Script> <![CDATA[ [Bindable] private var authorsCollection:ArrayCollection = new ArrayCollection(); private function getData():void { myService.fill(authorsCollection); } private function save():void { myService.commit(); } private function addNewItem():void { authorsCollection.addItem(new YourValueObject()); myService.commit(); } private function deleteItem():void { authorsCollection.removeItemAt(1); myService.commit(); } private function faultListener(event:FaultEvent):void { Alert.show(event.fault.message, event.fault.name);} ]]> </mx:Script> <mx:Button label="Revert" click="myService.revertChanges()"/> <PHPDataService id="myService" destination="Author" source="Author" endpoint="http://localhost/amfphp/gateway.php" fault="faultListener(event)"/>
When you create the service you need to set the name of the remote class for destination and source properties, and the set the endpoint value as the URL to the amfphp/gateway.php file. Once you set these values, you are ready to call the methods on the object:
- fill(arrayCollectionToBeFilled) - retrieves all the records from the server
- commit() - send all the changes to the server (if there were deletes/updates/inserts all these are committed by this method)
- revertChanges() - all uncommitted changes are reverted to the original values
- getItem(filterCondition) - return one item
If you want to add a new item, you create a new item and you add it to the ArrayCollection that is managed by the service. The same goes when you want to delete something.
As I said before, by using AMFPHP I am able to pass around the data serialized using a VO. So this is the code in Flex for the VO:
package org.corlan {
[RemoteClass(alias="org.corlan.VOAuthor")]
[Bindable]
public class VOAuthor {
public var id_aut:int;
public var fname_aut:String;
public var lname_aut:String;
}
}
In case you’re wondering how i implemented the PHPDataService, here is the code:
package org.corlan {
import flash.events.EventDispatcher;
import mx.collections.ArrayCollection;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.events.PropertyChangeEvent;
import mx.rpc.AsyncToken;
import mx.rpc.Responder;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.rpc.remoting.mxml.RemoteObject;
import mx.utils.ObjectUtil;
[Event(name="result", type="mx.rpc.events.ResultEvent")]
[Event(name="getItem", type="org.corlan.ItemEvent")]
[Event(name="fault", type="mx.rpc.events.FaultEvent")]
/**
* This service wraps an RemoteObject to connect to the server.
* Although it is created and tested with AMFPHP, it should work with no or minimum
* changes with other server side technologies.
*
* Imlements simplistic behaviour of Flex Data Services, in that this service can manage an
* ArrayCollection items and save/read/delete items.
*
* If you configured the VO like you would do for any remote object, then you will get an
* array collection of VOs.
*
* You set the collection you want to be managed by calling the <code>fill()</code> method and
* you save the changes by calling the <code>commit</code> method. When the fill method returns the result,
* an ResultEvent is dispatched.
*
* You can retrieve one item by calling the <code>getItem</code> and sending the value you want
* to be used as a filter.
*
* No changes are committed until you call commit() method on the service.
* When you call commit method, all the deleted items are committed, then the updated items
* and finally the inserts (in that order). The calls are queued.
*
* You can call <code>revertChanges()</code> and all the uncommitted changes are reverted (delete/create/update).
* So this call don't change anything on the server.
*
* You need to register a listener for fault if you want to get error messages from
* the server.
*
* On the server side, this component expects to find this methods implemented:
* - fill() - returns all the records from the table
* - getItem(condition) - returns one item or none
* - saveItem(item) - saves one item (either insert or update)
* - saveCollection(collection) - saves an array of items (insert or update)
* - deleteItem(item) - delete an item
* - deleteCollection(collection) - deletes a collection of items
*/
public class PHPDataService extends EventDispatcher {
private var _collection:ArrayCollection;
private var _revert:ArrayCollection = new ArrayCollection();
private var _isChanged:Boolean = false;
private var _isWriting:Boolean = false;
private var _endpoint:String;
private var _source:String;
private var _destination:String;
private var _remote:RemoteObject;
private var _insert:ArrayCollection = new ArrayCollection();
private var _update:ArrayCollection = new ArrayCollection();
private var _delete:ArrayCollection = new ArrayCollection();
public function fill(collection:ArrayCollection = null):void {
if (collection != null) {
_collection = collection;
_collection.addEventListener(CollectionEvent.COLLECTION_CHANGE, onChangeCollection);
}
initializeRemote();
var token:AsyncToken = _remote.fill();
var responder:Responder = new Responder(onFill, onFault);
token.addResponder(responder);
}
public function getItem(id:Object):void {
initializeRemote();
var token:AsyncToken = _remote.getItem(id);
var responder:Responder = new Responder(onGetItemResult, onFault);
token.addResponder(responder);
}
public function commit():void {
if (!_isChanged)
return;
initializeRemote();
var responder:Responder;
var token:AsyncToken;
if (_delete.length > 0) {
token = _remote.deleteCollection(_delete);
responder = new Responder(onDelete, onFault);
token.addResponder(responder);
} else if (_update.length > 0) {
token = _remote.saveCollection(_update);
responder = new Responder(onUpdate, onFault);
token.addResponder(responder);
} else if (_insert.length > 0) {
token = _remote.saveCollection(_insert);
responder = new Responder(onInsert, onFault);
token.addResponder(responder);
}
}
public function revertChanges():void {
if (!_isChanged)
return;
if (_collection.length > 0)
_collection.removeAll();
for (var i:int = 0; i <_revert.length; i++) {
_collection.addItem(_revert.getItemAt(i));
}
}
private function initializeRemote():void {
if (_remote != null)
return;
_remote = new RemoteObject(destination);
_remote.source = source;
_remote.endpoint = endpoint;
_remote.showBusyCursor = true;
}
private function onDelete(event:ResultEvent):void {
var responder:Responder;
var token:AsyncToken;
_delete.removeAll();
if (_update.length > 0) {
token = _remote.saveCollection(_update);
responder = new Responder(onUpdate, onFault);
token.addResponder(responder);
} else if (_insert.length > 0) {
token = _remote.saveCollection(_insert);
responder = new Responder(onInsert, onFault);
token.addResponder(responder);
} else {
_isChanged = false;
fill();
}
}
private function onUpdate(event:ResultEvent):void {
_update.removeAll();
if (_insert.length > 0) {
var responder:Responder;
var token:AsyncToken = _remote.saveCollection(_insert);
responder = new Responder(onInsert, onFault);
token.addResponder(responder);
} else {
_isChanged = false;
fill();
}
}
private function onInsert(event:ResultEvent):void {
_insert.removeAll();
_isChanged = false;
fill();
}
private function onFill(event:ResultEvent):void {
var arr:Array = event.result as Array;
_isWriting = true;
if (_collection.length > 0)
_collection.removeAll();
if (_revert.length > 0)
_revert.removeAll();
for (var i:int; i<arr.length; i++) {
_collection.addItem(arr[i]);
_revert.addItem(ObjectUtil.copy(arr[i]));
}
_isWriting = false;
dispatchEvent(event);
}
private function onGetItemResult(event:ResultEvent):void {
var ev:ItemEvent = new ItemEvent(ItemEvent.GET_ITEM, event.bubbles, event.cancelable, event.result, event.token, event.message);
dispatchEvent(ev);
}
private function onFault(event:FaultEvent):void {
trace(event.fault.faultString);
dispatchEvent(event);
}
private function onChangeCollection(event:CollectionEvent):void {
if (_isWriting)
return;
trace(event.kind);
_isChanged = true;
switch (event.kind) {
case CollectionEventKind.ADD:
addFor(_insert, event.items, CollectionEventKind.ADD);
break;
case CollectionEventKind.UPDATE:
addFor(_update, event.items, CollectionEventKind.UPDATE);
break;
case CollectionEventKind.REMOVE:
addFor(_delete, event.items, CollectionEventKind.REMOVE);
break;
}
}
private function addFor(collection:ArrayCollection, items:Array, eventType:String):void {
for (var i:int; i<items.length; i++) {
var obj:Object;
if (items[i] is PropertyChangeEvent)
obj = (items[i] as PropertyChangeEvent).source;
else
obj = items[i];
if (collection.contains(obj))
return;
switch (eventType) {
case CollectionEventKind.REMOVE:
//remove the item from update or insert
if (_insert.contains(obj)) {
_insert.removeItemAt(
_insert.getItemIndex(obj));
} else if (_update.contains(obj)) {
_update.removeItemAt(
_update.getItemIndex(obj));
}
collection.addItem(obj);
break;
case CollectionEventKind.UPDATE:
//if the item is already on insert, don' add
if (!_insert.contains(obj))
collection.addItem(obj);
break;
case CollectionEventKind.ADD:
collection.addItem(obj);
break;
}
}
}
public function set endpoint(value:String):void {
_endpoint = value;
}
public function get endpoint():String {
return _endpoint;
}
public function set source(value:String):void {
_source = value;
}
public function get source():String {
return _source;
}
public function set destination(value:String):void {
_destination = value;
}
public function get destination():String {
return _destination;
}
}
}
PHP code
The Flex code uses a RemoteObject that expects to find these methods defined on the server object:
- fill() - should return an array of VOs
- getItem($condition) - returns one item
- saveItem($item) - saves (update/insert) one item
- saveCollection($array) - saves (update/insert) all the items from the given array
- deleteItem($item) - deletes the given item
- deleteCollection($array) - deletes all the items from the given array
To formalize that contract between the client and the server, I’ve created an abstract PHP class. And all the PHP classes I want to use with the Flex PHPDataService need to extend this class. The code for the abstract class is:
//connection info define( "DATABASE_SERVER", "localhost"); define( "DATABASE_USERNAME", "mihai"); define( "DATABASE_PASSWORD", "mihai"); define( "DATABASE_NAME", "flex360"); abstract class BasePHPService { protected $_connection; private $_errorMessage; public function BasePHPService() { $this->_connection = mysql_connect(DATABASE_SERVER, DATABASE_USERNAME, DATABASE_PASSWORD); mysql_select_db(DATABASE_NAME); } protected function isError() { $message = mysql_error($this->_connection); if ($message != '') { $this->_errorMessage = mysql_errno($this->_connection) . ': ' . $message; return true; } else { $this->_errorMessage = ''; return false; } } protected function getError() { return $this->_errorMessage; } /** * Escape the given value for SQL * * @param string $value * @return escaped value */ protected function escapeForSql($value) { if (is_null($value) || $value === '') { $tmValue = '\'' . $value . '\''; } else { $tmValue = '\'' . str_replace(array("'", "\\"), array("''", "\\\\"), $value) . '\''; } return $tmValue; } abstract function fill(); abstract function getItem($id); abstract function saveItem($item); abstract function saveCollection($collection); abstract function deleteItem($item); abstract function deleteCollection($collection); }
On the PHP side I have this VO defined in amfphp/services/vo/org/corlan/VOAuthor.php. As long as you put the VO under the amfphp/vo/ and then add the rest of the folders from the package name of the ActionScript VO class (thus the org/corlan/ folders), the AMFPHP and Flex RemoteObject can automatically de-serialize the message to the right VO type.
<?php class VOAuthor { public $id_aut; public $fname_aut; public $lname_aut; // explicit actionscript class var $_explicitType = "org.corlan.VOAuthor"; } ?>
If you want to throw an error from the server side that will be caught by the fault listener of the PHPDataService, you just use: throw new Exception(”error message you want to be consumed by the Flex client”);
For example the table might be setup not to accept empty values for a field. If the client sends such values, you can use this feature to send an error back to the client.
Here is the implementation for the Author PHP class that manages a simple table:
require_once ('../BasePHPService.php'); require_once ('./vo/org/corlan/VOAuthor.php'); /** * This class manages the table author_aut. * It is intended to be used as RemoteObject with AMFPHP. * To model the one row from the table, uses VOAuthor class. * * When you want to throw an error up to the Flex calling code * (where you can catch with the listener registered for fault) * you need to use: * <code>throw new Error("error message to be displayed in Flex application");</code> */ class Author extends BasePHPService { public function Author() { parent::BasePHPService(); } public function fill() { //retrieve all rows $query = 'SELECT id_aut, fname_aut, lname_aut FROM authors_aut ORDER BY fname_aut'; $result = mysql_query($query, $this->_connection); if ($this->isError()) throw new Exception($this->getError()); $ret = array(); while ($row = mysql_fetch_object($result)) { $tmp = new VOAuthor(); $tmp->id_aut = $row->id_aut; $tmp->fname_aut = $row->fname_aut; $tmp->lname_aut = $row->lname_aut; $ret[] = $tmp; } mysql_free_result($result); return $ret; } public function getItem($id) { $query = 'SELECT id_aut, fname_aut, lname_aut FROM authors_aut WHERE id_aut = '. $this->escapeForSql($id); $result = mysql_query($query, $this->_connection); if ($this->isError()) throw new Exception($this->getError()); $ret = array(); $row = mysql_fetch_object($result); $item = new VOAuthor(); $item->id_aut = $row->id_aut; $item->fname_aut = $row->fname_aut; $item->lname_aut = $row->lname_aut; mysql_free_result($result); return $item; } public function saveItem($author) { if ($author == NULL) return NULL; if ($author->id_aut > 0) { $query = 'UPDATE authors_aut SET fname_aut='.$this->escapeForSql($author->fname_aut).', lname_aut='.$this->escapeForSql($author->lname_aut).' WHERE id_aut='. $this->escapeForSql($author->id_aut); } else { $query = 'INSERT INTO authors_aut (fname_aut, lname_aut) VALUES ('.$this->escapeForSql($author->fname_aut).', '.$this->escapeForSql($author->lname_aut).')'; } $result = mysql_query($query, $this->_connection); if ($this->isError()) throw new Exception($this->getError()); return NULL; } public function saveCollection($collection) { for ($i=0; $i<count($collection); $i++) { $this->saveItem($collection[$i]); } } public function deleteItem($author) { if ($author == NULL || !($author->id_aut > 0)) return NULL; $query = 'DELETE FROM authors_aut WHERE id_aut = '. $this->escapeForSql($author->id_aut); mysql_query($query, $this->_connection); if ($this->isError()) throw new Exception($this->getError()); } public function deleteCollection($collection) { for ($i=0; $i<count($collection); $i++) { $this->deleteItem($collection[$i]); } } }
Final words
I wrote the class PHPDataServices with PHP and AMFPHP in mind, and that’s how I’ve tested the code. But with minimal changes it should work with ColdFusion, Java or other server side technologies that offer RemoteObject (Python, Ruby). You can download the project from here - you will find a readme.txt file inside of the archive explaining how to set up the project.
BarCamp Brighton - the third edition
Last weekend in Brighton (University of Sussex) it was the third edition of BarCamp. As Adobe was one of the sponsors, I was there presenting a session on Flex. My friend, Andrew was there too, and he presented a session on AIR. His session was much better than mine, and the public was applauding. But I think he cheated a little bit, as he presented 10 AIR applications you can’t live without and the last one was Desktop Keeley. I mean, come on, show me a Flex app that can compete with this one
The concept is interesting, everyone is expected to speak (present something). There were more than 100 people over there, so at any given time you had 5 or 6 sessions going. Thank you again guys for inviting me!
PS As usual, my luck goes berserk when I am on the road. This time the Heathrow Express was evacuated (so I spent an extra 40 minutes waiting) and Saturday, during lunch time, the fire alarm sounded and we were evacuated for about 30 minutes. Rock&Roll baby!
Storing data locally in AIR
If someone asked me how to store data locally in AIR (on the user’s machine), I think this would be my answer in short:
- Store data in the SQLite database
- Store ActionScript objects in files
- Store assets in files
- Store ActionScript objects in the encrypted local store
Let’s talk about each one and see how you can use it.
Store data in the SQLite database
Every AIR application has access to an embedded SQL database (SQLite to be precise). This database supports (almost :D) ACID transactions, implements most of SQL-92, and more importantly, you don’t have to configure a thing to use it. It is there and if you need it, you can use it. All the database information is stored in a single file.
If you are not sure why you would use this feature, I’ll give you an example. Suppose your application has a feedback form, and because it is an AIR application the user can use it whether he has Internet connectivity or not. Imagine that the user is offline and he submits the form. What should the application do? I think one reasonable answer would be to save the data locally and then send it to the server the next time when the application is online. In this situation SQLite could be the local data store.
Below is sample code that let’s you write some data to a table and read the data. Basically when you want to work with SQLite, you need to create a connection (SQLConnection) and open the connection pointing to the file that stores the database. After this, you use a SQLStatement yo create queries and execute them. Thus, you can create tables, insert/update/delete/read data.
//create the database and table private function createDb():void { //create the database file in the application storage folder var file:File = File.applicationStorageDirectory.resolvePath("my.db"); sqlConnectionSync = new SQLConnection();//create a connection object sqlConnectionSync.open(file, SQLMode.CREATE);//create the database if(file.exists) return; //create a statement var createDb:SQLStatement = new SQLStatement(); //create a table createDb.text = "CREATE TABLE messages (id INTEGER PRIMARY KEY AUTOINCREMENT, subject VARCHAR(64), message VARCHAR(255))"; createDb.sqlConnection = sqlConnectionSync; //set the connection that will be used createDb.execute();//execute the statement } //write the data private function writeData():void { var insert:SQLStatement = new SQLStatement(); //create the insert statement insert.sqlConnection = sqlConnectionSync; //set the connection insert.text = "INSERT INTO messages (subject, message) VALUES (?, ?)"; insert.parameters[0] = subject.text; insert.parameters[1] = message.text; insert.execute(); Alert.show("The data was saved into the table!"); } //read the data private function readData():void { var read:SQLStatement = new SQLStatement(); //create the read statemen read.sqlConnection = sqlConnectionSync; //set the connection read.text = "SELECT id, subject, message FROM messages ORDER BY id"; read.execute(); var result:SQLResult = read.getResult(); //retrieve the result of the query myDatagrid.dataProvider = result.data; //display the array of objects into the data grid }
This example uses synchronous calls. You can use asynchronous calls. See the documentation for SQLConnection. Also, you can execute the queries in transactions if you want.
Store ActionScript objects in files
You might have objects that you want to save locally and you don’t want to use the SQLite for this. With AIR you can save any ActionScript object to a file. You can save anything from simple objects to arrays of value objects. However there is a catch: when the object you stored has a type (let’s say for example VOPerson), then when you retrieve it is safe to cast to the original type. But if you save an ArrayCollection of VOPerson, then when you retrieve them you will have an ArrayCollection of Objects. More on this here.
Below is an example of code that writes/reads an Object to and from a file. You use the File class to create a file under the application storage folder, and then you use a FileStream to write the Object to this file. When you read, you use a File to get the file and a FileStream to read the contents of the file.
//write an Object to a file private function writeObject():void { var object:Object = new Object();//create an object to store object.value = asObject.text; //set the text field value to the value property //create a file under the application storage folder var file:File = File.applicationStorageDirectory.resolvePath("myobject.file"); if (file.exists) file.deleteFile(); var fileStream:FileStream = new FileStream(); //create a file stream fileStream.open(file, FileMode.WRITE);// and open the file for write fileStream.writeObject(object);//write the object to the file fileStream.close(); } //read an object stored into a file private function readObject():void { //read the file var file:File = File.applicationStorageDirectory.resolvePath("myobject.file"); if (!file.exists) { Alert.show("There is no object saved!"); return; } //create a file stream and open it for reading var fileStream:FileStream = new FileStream(); fileStream.open(file, FileMode.READ); var object:Object = fileStream.readObject(); //read the object Alert.show("The text member has this value: " + object.value); }
Store assets in files
If you have an AIR application that frequently uses the same pictures, sounds, movies, or other SWF files you might want to save these assets locally, and thus you the overhead of downloading these resources each time. The workflow is almost the same as the previous one for storing ActionScript objects in files. Only this time you don’t write an ActionScript object, you write the bytes to the disk. So instead of fileStream.writeObject(myObject), I use fileStream.writeBytes(bytes, 0, bytes.length). Below is some sample code that asks for a resource URL (such as an JPG file for example) and using an URLLoader, downloads the resource and saves it locally.
<mx:Script> /** * loader object used to load content from web */ private var loader:URLLoader = new URLLoader(); /** * This is the click handler for the button that save the web resource locally. * Just trigger the load and disable the button so no other * request can be sent while it is already running one. */ private function saveResource():void { loader.load(new URLRequest(resURL.text)); btn.enabled = false; } /** * This function gets called when the resource was loaded. * We just extract the name of the resource and call the saveLocally() function * to write the object on the local machine. */ private function onLoad(event:Event):void { btn.enabled = true; //enable the button so we can send another request. //get the file name of the resource var fileName:String = new String(resURL.text).split("/").pop(); //call the saveLocally function; loader.data has the bytes of the loaded resource saveLocally(fileName, loader.data); } /** * Error handler for loader object. * I just display the error message and enable the button for new requests. */ private function onError(event:ErrorEvent):void { btn.enabled = true; Alert.show(event.text, "Error"); } /** * This function does the actual saving. * @param file name to be used for saving * @param bytes to be written */ private function saveLocally(fileName:String, data:ByteArray):void { //create a file under the application storage directory using the filename argument var file:File = File.applicationStorageDirectory.resolvePath(fileName); if (file.exists) file.deleteFile(); //delete it if exists //create a file stream to be able to write the content of the file var fileStream:FileStream = new FileStream(); //open the file stream and set for Write fileStream.open(file, FileMode.WRITE); //writes the bytes fileStream.writeBytes(data, 0, data.length); //close the stream fileStream.close(); //display the path of the saved resources showLocalPath.text = file.nativePath; } </mx:Script> <mx:VBox left="10"> <mx:Label text="Store assets in files" fontSize="20"/> <mx:Label text="Enter a resource URL such as http://mysite/image.jpg"/> <mx:HBox> <mx:TextInput id="resURL" text="" width="600"/> <mx:Button id="btn" click="saveResource()" label="Save locally" /> </mx:HBox> <mx:Label id="showLocalPath" /></mx:VBox>
You can read more on this here.
Store ActionScript objects in the encrypted local store
Sometimes you have sensitive data, such as personal information or passwords and you want to save them locally. But you don’t want this data to be read by other users or applications. The solution for this problem is to use the encrypted local store. This is a built in functionality in AIR, and each AIR application has its own encrypted local store. Thus you can very safely store any sensitive information you might have. You have access to this store using EncryptedLocalStore object. You have setItem(), getItem() and removeItem() methods. The data saved and read are ByteArray, and when you store or retrieve something you use a key. Below is a code sample:
<mx:Script> //store the text in the encrypted local store private function encrypt():void { var data:ByteArray = new ByteArray(); data.writeUTFBytes(encryptText.text);//create a byte array out of the text we want to encrypt EncryptedLocalStore.setItem("myEncryptedData", data); //store the byte array into the encrypted local storage } //read the data from the encrypted local store private function decrypt():void { //using the same key I used when I wrote the data, I reead the bytes back var bytes:ByteArray = EncryptedLocalStore.getItem("myEncryptedData"); if(bytes) Alert.show(bytes.readUTFBytes(bytes.bytesAvailable)); else Alert.show("There was no value to be read!"); } </mx:Script> <mx:VBox left="10"> <mx:Label text="Store ActionScript objects in the encrypted local store" fontSize="20"/> <mx:TextInput id="encryptText" /> <mx:Button label="Encrypt" click="encrypt()" /> <mx:Button label="Decrypt" click="decrypt()" /> </mx:VBox>
You can read more on this subject here.
Final words
You can download the exported project from here. You can import the project in Flex Builder using Import > Flex Builder > Flex Project.