Using Deferreds With the Cordova File API

Reading and writing files in a Cordova hybrid mobile app is done using the File plugin. This API can be a little difficult to use because it takes multiple function callbacks to get anything done.

Let’s look at an example of how we could save some data to a file.
First you need to request a fileSystem object:

var dataToSave = "save me to a file";

window.requestFileSystem(window.LocalFileSystem.PERSISTENT, dataToSave.length, function (fileSystem) {
  // use file system
}, function (error) {
  // handle error
});

This function call takes in a “success” and a “fail” callback. Next we can use the filesystem object to get access to a file to save the data into:

var dataToSave = "save me to a file";

window.requestFileSystem(window.LocalFileSystem.PERSISTENT, dataToSave.length, function (fileSystem) {
  fileSystem.root.getFile("fileName.txt", { create: true, exclusive: false }, function (file) {
    // use file
  }, function (error) {
    // handle error
  });
}, function (error) {
  // handle error
});

Again here the call to getFile() takes a “success” and “fail” callback function.
Once we have access to the file, we can now request a file writer:

var dataToSave = "save me to a file";

window.requestFileSystem(window.LocalFileSystem.PERSISTENT, dataToSave.length, function (fileSystem) {
  fileSystem.root.getFile("fileName.txt", { create: true, exclusive: false }, function (file) {
    fileEntry.createWriter(function (writer) {
      // use file writer
    }, function (error) {
      // handle error
    });
  }, function (error) {
    // handle error
  });
}, function (error) {
  // handle error
});

Once again, two more callback functions.
Now that we have the file writer, we can finally write our string to the file:

var dataToSave = "save me to a file";

window.requestFileSystem(window.LocalFileSystem.PERSISTENT, dataToSave.length, function (fileSystem) {
  fileSystem.root.getFile("fileName.txt", { create: true, exclusive: false }, function (file) {
    fileEntry.createWriter(function (writer) {
      writer.onwrite = function () {
        // success!
      };
      writer.onerror = function () {
        // handle error
      };
      writer.write(data);
    }, function (error) {
      // handle error
    });
  }, function (error) {
    // handle error
  });
}, function (error) {
  // handle error
});

The code syntax changes here, since onwrite and onerror are assigned functions before the call to write(), but the idea is the same; it gets “success” and “error” callbacks.

Remove Anonymous Functions

This code is awful! We have to go 4 asynchronous callback functions deep before we can report success to the user. There are also 4 places where an error function is specified. This code can be cleaned up significantly by not using anonymous functions, and instead using actual named functions. To keep our named functions to ourself, we can define them into a “save” function. This will start to wrapper the calls to the File API.

function save (name, data, success, fail) {

  var gotFileSystem = function (fileSystem) {
    fileSystem.root.getFile(name, { create: true, exclusive: false }, gotFileEntry, fail);
  };

  var gotFileEntry = function (fileEntry) {
    fileEntry.createWriter(gotFileWriter, fail);
  };

  var gotFileWriter = function (writer) {
    writer.onwrite = success;
    writer.onerror = fail;
    writer.write(data);
  };

  window.requestFileSystem(window.LocalFileSystem.PERSISTENT, data.length || 0, gotFileSystem, fail);
}

var dataToSave = "save me to a file";
save("fileName.txt", dataToSave, function () {
  // success!
}, function (error) {
  // handle error
});

Use jQuery Deferreds

This code is a lot cleaner and easier to read. We also made a function that is actually reusable. We could stop there, but why not take it one step further and introduce jQuery deferreds into the function? Instead of taking in two callback functions, the save() function can just return a deferred.

function save (name, data) {
  var deferred = $.Deferred();

  var fail = function (error) {
    deferred.reject(error);
  };

  var gotFileSystem = function (fileSystem) {
    fileSystem.root.getFile(name, { create: true, exclusive: false }, gotFileEntry, fail);
  };

  var gotFileEntry = function (fileEntry) {
    fileEntry.createWriter(gotFileWriter, fail);
  };

  var gotFileWriter = function (writer) {
    writer.onwrite = function () {
      deferred.resolve();
    };
    writer.onerror = fail;
    writer.write(data);
  };

  window.requestFileSystem(window.LocalFileSystem.PERSISTENT, data.length || 0, gotFileSystem, fail);
  return deferred.promise();
}

var dataToSave = "save me to a file";
save("fileName.txt", dataToSave)
  .done(function () {
    // success!
  })
  .fail(function (error) {
    // handle error
  });

Save, Load, Delete

Now we have a nice wrapper around the Cordova File plugin for saving a file, and it simply returns a jQuery deferred for us to use to handle success or failure. To expand upon this concept, I like to make an object named fileStorage on global scope (or in an module if you are using RequireJS or Browserify) that contains these convenient functions for working with files.

Here is the full code:

window.fileStorage = {
  save: function (name, data) {
    var deferred = $.Deferred();

    var fail = function (error) {
      deferred.reject(error);
    };

    var gotFileSystem = function (fileSystem) {
      fileSystem.root.getFile(name, { create: true, exclusive: false }, gotFileEntry, fail);
    };

    var gotFileEntry = function (fileEntry) {
      fileEntry.createWriter(gotFileWriter, fail);
    };

    var gotFileWriter = function (writer) {
      writer.onwrite = function () {
        deferred.resolve();
      };
      writer.onerror = fail;
      writer.write(data);

    window.requestFileSystem(window.LocalFileSystem.PERSISTENT, data.length || 0, gotFileSystem, fail);
    return deferred.promise();
  },

  load: function (name) {
    var deferred = $.Deferred();

    var fail = function (error) {
      deferred.reject(error);
    };

    var gotFileSystem = function (fileSystem) {
      fileSystem.root.getFile(name, { create: false, exclusive: false }, gotFileEntry, fail);
    };

    var gotFileEntry = function (fileEntry) {
      fileEntry.file(gotFile, fail);
    };

    var gotFileWriter = function (writer) {
      reader = new FileReader();
      reader.onloadend = function (evt) {
        data = evt.target.result;
        deferred.resolve(data);
      };

      reader.readAsText(file);
    }

    window.requestFileSystem(window.LocalFileSystem.PERSISTENT, data.length || 0, gotFileSystem, fail);
    return deferred.promise();
  },

  delete: function (name) {
    var deferred = $.Deferred();

    var fail = function (error) {
      deferred.reject(error);
    };

    var gotFileSystem = function (fileSystem) {
      fileSystem.root.getFile(name, { create: false, exclusive: false }, gotFileEntry, fail);
    };

    var gotFileEntry = function (fileEntry) {
      fileEntry.remove();
    };

    window.requestFileSystem(window.LocalFileSystem.PERSISTENT, 0, gotFileSystem, fail);
    return deferred.promise();
  }
};

This gives us a nice reusable isolated set of functions that we can use to interact with the file system, and is much easier to work with than nesting 4 callback functions.

The latest code is also available as CoffeeScript from this Gist.

Advertisements
Tagged with: , ,
Posted in JavaScript, Mobile, Programming
2 comments on “Using Deferreds With the Cordova File API
  1. Samo Dadela says:

    Hi Jeff,

    Nice article.

    Your example only writes data once into a file. Do you know how that could be extended to append stuff to the file? I want to create a log() function that would append stuff to a file, however I don’t know how to queue writes (since write() is async). How could log() be written so that it would chain request?

    • rally25rs says:

      I haven’t tried this, but you can probably take the gotFileWriter() function and just have it resolve(writer); to return the file writer to the listener, instead of actually writing any content. This question also made me realize I forgot to close() my files after I read or write to them! oops!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

CodingWithSpike is Jeff Valore. A professional software engineer, focused on JavaScript, Web Development, C# and the Microsoft stack. Jeff is currently a Software Engineer at Virtual Hold Technologies.


I am also a Pluralsight author. Check out my courses!

%d bloggers like this: