Saving and loading complex objects from a file with FlashPlayer

January 19th, 2012    by sigman    3580
  Tips   actionscript, programming

When building Flash apps or games there is very often a need to save some user data. It might be a player's progress in a game or some work done in an application. Usually online application data is being kept in a database and desktop apps are saving to a file on a hard drive. Flash also allows saving to shared objects, so called Flash's cookie files but there is a limit of size set by an operating system and some browsers or anti-viruses may clear them periodically effectively erasing users saved data. While for desktop apps developers choose Adobe AIR, which allows deeper integration with operating systems, not many people know that since Flash Player 10 there is also an option of saving and loading data from a file. In this quick article I will give you a code example of how this feature could be use along with JSON and ByteArrays to effectively save your data to a file.

Article imported from http://sierakowski.eu/list-of-tips/110-saving-and-loading-complex-objects-from-a-file-with-flashplayer.html

So I was working on an educational application for teachers and one of the requirements was that they should have an option to load and save data to a file what would allow them exchanging data files between themselves. Amount of data was rather huge as among other data it consisted of x and y coordinates representing drawings done on a special canvas. I created a class containing public properties representing these x and y values stored in an array and other information like color and thickness of a stroke. Every time a user creates a new line, a new object of that class is being created and stored in an array, let's call it main array. When pressed a save button, that main array is serialised using JSON library, this means it is represented in a form of string. I also create another string containing information about the version of the application, date when the data is being exported and title of the export that a teacher provides when clicking the save button. Next using these strings and their lengths values I'm creating and populating a byte array. Before saving I'm also compressing it to minimise a file size and finally whole content of the byte array is being saved to a file. Loading a file is a very similar operation with all the steps reversed ;). Enough talking, let's have a look to the code, all important things are commented to give you a better explanation. Bear in mind that this is not a complete code to copy and paste as I just provide here pieces illustrating the whole idea.

This is a class that stores information about each drawing. I put it here to show you what sort of objects you could serialise and save to a file.

//note that all properties are public allowing JSON class to get access to them
internal class ActionPen
{
  public var actionId:uint; //each drawing has unique action id
  public var toolType:int;
  public var color:int;
  public var thicness:int;
  public var points:Array;
 
  public function ActionPen(actionId:uint, color:int, thickness:int):void
  {
    this.actionId = actionId;
    this.toolType = ActionsRecorder.ATOOL_PEN;
    this.color = color;
    this.thicness = thickness;
    points = [];
  }
 
  public function addAction(x:int, y:int):void
  {
    points.push(new Point(x, y));
  }
}
 
internal class Point
{
  public var x:int, y:int;
  public function Point(x:int, y:int):void {this.x = x; this.y = y;};
}

 

Then another class calles ActionsRecorder has a property mActionsArray containing instances of the ActionPen class and methods to serialise and de-serialise this data. To keep things simple I'm not providing the classes to create or recreate the ActionPen objects with drawing data here. As for JSON, if you are going to use build methods to the FP11 (and you should as there are more opitimal than as3corelib's JSON), use JSON.parse() instead of decode and JSON.stringify() instead of encode.

//if you use fp11 you don't need to use as3core lib
//as JSON is built in already.
//if you are targetting for fp10 then use this import
//and get http://code.google.com/p/as3corelib/
import com.adobe.serialization.json.JSON;
 
public class ActionsRecorder
{
   //this array keeps instances of the ActionPen class
   private var mActionsArray:Array;
 
   //using JSON to encode array of ActionPen objects
   //along with all properties
   public function actionsExport():String
   {
      return JSON.encode(mActionsArray);
   }
 
   //again using JSON to decode string read from a file
   //and cast is as an array. This way we are rebuilding
   //also all the object it contained.
   public function actionsImport(data:String):void
   {
      mActionsArray = null;
      mActionsArray = JSON.decode(data) as Array;
      mRepeatIndex = 0;
   }
}

 

JSON encoded version of the mActionsArray would look like this:

[{"thicness":5,"actionId":0,"points":[{"y":112,"x":226},{"y":113,"x":226},
{"y":116,"x":226},{"y":131,"x":226},{"y":149,"x":226}],
"toolType":0,"color":3355443}]

 

Next in the other part of the application I keep method to use export string from ActionsRecorder to save it to a file and another method to read it from a file and send it to ActionsRecorder to import it. There are two important things to note here, one is that Flash Player can't automatically save or read from a file. Methods that are creating file reference objects are required to receive the MouseEvent in order to get access to a file system. The second restriction is that you can't specify any folder where that file is to be saved, it will be defaulted to a desktop and user will need to manually select a location in a browse window. Both are caused by security reasons, just imagine what would happen if your app created an exe file and saved it in the startup folder or accessed operating system configuration files without letting the user know about it. Forcing to use MouseEvent, the user has control that there is something being saved or opened for some reason.

So somewhere on the stage there are two buttons for import and export and here are the methods that are event handlers for them. First let's have a look on saving:

//for both saving and loading you are going to need this import:
import flash.net.FileFilter;
import flash.utils.ByteArray;
 
function saveToFile(e:MouseEvent):void
{
   //here I'm getting a string which is a json encoded array of ActionPen objects
   var exportString:String = ActionsRecorder.getInstance().actionsExport();
 
   //this is some additional data for my application in case in the future 
   //we release a never version with more type export data 
   var fileHeader:String = APP_NAME + " v" + APP_VER;
 
   //also saving export data, which will be shown in the import window later on
   var exportDate:String = getDateString();
 
   //here I'm creating a string for a file name
   var fileName:String = "export_" + getDateString() + ".dat";
 
   var ba:ByteArray = new ByteArray();
 
   //for each piece of separate data I'm saving first a number representing
   //length of a string using writeShort where Short is a 16-bit integer
   //and then an actual string with writeUTFBytes and passing length as an argument..
   //This way I will be able to read correct data later on when importing it.
   ba.writeShort(fileHeader.length);
   ba.writeUTFBytes(fileHeader);
 
   ba.writeShort(exportDate.length);
   ba.writeUTFBytes(exportDate);
 
   ba.writeShort(exportString.length);
   ba.writeUTFBytes(exportString);
 
   //once all data is in byte array it's time to compress it
   ba.compress()
 
   //creating file reference and calling save method which opens browse dialog
   //where user can select a destination for a file.
   var f:FileReference = new FileReference();
   f.save(ba, fileName);
}

 

And for loading in addition for a mouse handler method for import button we also have another one for loading data:

//additional import to filter out files in open dialog box
import flash.net.FileFilter;
 
function openFromFile(e:MouseEvent):void
{
   var f:FileReference = new FileReference();
 
   //when user completes selecting a file to load we call file load
   f.addEventListener(Event.SELECT, function(e:Event):void{f.load();});
 
   //when file loading is complete...
   f.addEventListener(Event.COMPLETE, onFileLoadComplete);
 
   //creating file filter for open dialog box with description "MyApp export files"
   //and default extension of .dat
   var fFilter:FileFilter = new FileFilter("MyApp export files", "*.dat");
 
   //actual call to a browse method invoking browse window with array of file filters
   f.browse([fFilter]);
}
 
function onFileLoadComplete(e:Event):void
{
   //file loaded so let's parse the data
   var ba:ByteArray = e.target.data;
 
   //first we need to uncompress it
   ba.uncompress();
 
   //and read in reverse order we saved them.
   //For each piece of data we first read Short which is a number representing
   //a length of a string and then an actual string (UTFBytes)
   var fileHeader:String = ba.readUTFBytes(ba.readShort());
   var exportDate:String = ba.readUTFBytes(ba.readShort());
   var exportString:String = ba.readUTFBytes(ba.readShort());
 
   //exportString is what we send back to the ActionsRecorder to 
   //recreate the ActionPen objects
   ActionsRecorder.getInstance().actionsImport(exportString);
}

 

So that's it. Of course the final code would have some data validation to make sure that data imported is correct. It could be an additional integer in byte array with a total number of bytes. If these two values didn't match we would know that something wrong happened to the file.

And here is a simple example how this looks in practice. First populate the array (represented by the list component) and export to a file. Next you can refresh the page and import the file to see the list populated again. Also open the saved file in a file editor to have a look at the binary content.

 

Comments imported from the original article @sierakowski.eu

 
+- 0 #2 Fabian 2014-02-02 00:35
2 years ago....super tutorial. thanks for the post. I´m using it now, but i encounter a problem.
My exportString is too long. I get an "Error: Error #2030: End of file was encountered. at flash.utils::ByteArra y/readUTFBytes()"

How can i fix this?
Quote
 
 
 
+- +2 #1 Junior dev 2012-06-17 13:16
Awesome tutorial, thanks for sharing it.
Quote