Chapter 3. The Power of HTML5 with Dart

HTML5 features are already widely supported by most of the modern browsers. In this chapter, we're going to take a look at some of them while building two small apps:

  • Reddit Read Later: This app fetches the latest posts tagged as dartlang from www.reddit.com via JSONP, renders them into an HTML list, and allows you to save interesting ones to local IndexedDB
  • Music visualizer: This app uses the File Drag and Drop API to load music from your computer, analyze frequencies with the Audio API, and draw bar charts on the 2D canvas

On top of that, we'll take a quick look at some technologies commonly used with graphics and games and a few notes about performance, low-level typed arrays, and Dart's Isolates.

If you're planning to support older browsers with limited support of HTML5 features, you can take a look at http://caniuse.com to see what's available to you before you start developing.

Both apps are going to heavily use Dart's Future-Based API for all asynchronous operations with async and await keywords that we talked about in the previous chapter.

Creating the Reddit Read Later app

We can start writing the app right away. We can split it into three separate parts:

  • Fetch JSONP data via Reddit API
  • Render latest articles into a list
  • Let the user save articles into IndexedDB

It's fair to say that this example doesn't use any special HTML5 features apart from IndexedDB. What's interesting here is how it combines multiple things that we've seen in previous chapters, most importantly, the integration of JavaScript within Dart, its Future-Based API, and JsObject proxy objects. You can use exactly the same approach for any JavaScript that needs to call Dart callbacks.

Fetching the JSONP data

Simply said, JSONP is a JavaScript object wrapped into a function call and included just like any other JavaScript file (http://en.wikipedia.org/wiki/JSONP). Our app is entirely client side. This means that we can't call Ajax requests to servers on a different domain to ours, and unless the remote server returns appropriate Access-Control-Allow-Origin (ACAO) headers, all responses will be blocked by the browser due to the same-origin policy (this is mostly for security reasons).

With JSONP, we can avoid browser restrictions and make use of cross-origin resource sharing, aka CORS (http://en.wikipedia.org/wiki/Cross-origin_resource_sharing).

In practice, the JSON response would look like this:

{"key1":"value1","key2":"value2"}

The JSONP response is wrapped into a function call:

callback({'key1': 'value1', 'key2': 'value2'});

Note

Note that included JSONP is just like any other JavaScript file and has full access to the client's browser; therefore, it can be a security threat. Use JSONP only with providers that you trust.

We're going to use JSONP to get a list of the latest posts on www.reddit.com that are tagged as dartlang from http://www.reddit.com/r/dartlang/search.json?sort=new&jsonp=callback&limit=10&restrict_sr=on.

The Reddit API allows us to set a custom callback with the jsonp parameter. This is the name of the function that will wrap the entire JavaScript object for us. When using Dart, all JavaScript has to be run in an isolated scope and as we want to be able to refresh Reddit posts anytime we want, we're not going to statically include the preceding URL but rather include it on demand and proxy the JavaScript callback function with ours using dart:js:

// web/main.dart
import 'dart:html';
import 'dart:js';
import 'dart:async';

class RedditReadLater {
  static const String jsonpUrl = 'http://www.reddit.com/...';

  Future<List> refreshReddit() {
    // At this point we have to use [Completer] class to return a 
    // Future object and complete it when we process all items
    // in the JavaScript callback.
    var completer = new Completer();
    
    // Just like window.callback = function() {} in JavaScript.
    // Context is defined in [dart:js] package.
    context['callback'] = (JsObject jsonData) {
      JsArray redditItems = jsonData['data']['children'];
      List<Map<String, String>> items;

      // Iterate all items in the JavaScript array. 
      for (int i = 0; i < redditItems.length; i++) {
        JsObject item = redditItems[i]['data'];
        items[i] = ({
          'title': item['title'],
          'url': item['url']
        });
      }
      completer.complete(items);
    };

    // To dynamically reload data we need to create
    // and append <script> element every time.
    ScriptElement script = new ScriptElement();
    script.src = jsonpUrl;
    document.body.children.add(script);

    // Return a Future object that will be resolved when the
    // included script is loaded and the callback() method called.
    return completer.future;
  }

  void renderReddit(UListElement targetUl, List items) {
    // Remove all children and populate it with new ones.
    var frag = new DocumentFragment();
    for (Map<String, String>item in items) {
      /* Fill the <ul> list with items */
    }
  }
}

Note

For a detailed explanation of the Reddit API, refer to http://www.reddit.com/dev/api.

Now we can write the stub code for our main() function that will use the Future object from refreshReddit():

void main() {
  var readLater = new RedditReadLater();

  // We can set an optional parameter "_" which is required by
  // onClick stream listener although we don't need it.
  refresh([_]) async {
    var items = await readLater.refreshReddit();
    readLater.renderReddit(
        document.querySelector('#reddit-list'), items);
  }


  document.querySelector('#refresh-btn').onClick.listen(refresh);
  
  refresh();
}

Our HTML template for this app is going to be very short:

<body>
  <h2>Latest posts on reddit.com/r/dartlang
    [<a href="#" id="refresh-btn">refresh</a>]
  </h2>
  <ul id="reddit-list"></ul>
    
  <h2>Read later</h2>
  <ul id="saved-articles"></ul>
</body>

The final app filled with posts from www.reddit.com will look like this in the browser:

Fetching the JSONP data

When we know how to fetch and display data via the Reddit API, we can save them to IndexedDB.

IndexedDB

IndexedDB is a transactional client-side storage API with support for high performance searches using indices and is implemented in almost every browser (including mobile browsers). It's designed specifically to store JavaScript objects.

There are also some restrictions for IndexedDB:

  • The database size is usually limited to tens of MB, although this limitation varies over different browsers. If you want to store a large amount of data, make sure your targeted platform can handle it.
  • Your application can use multiple databases, although the size limit in browsers might be per database and also per domain.
  • The same-origin policy applies to IndexedDB as with any other resource in JavaScript.

It's a good practice to check whether a client's browser supports HTML5 APIs that might not be available in older browsers. With the dart:indexed_db Dart package, you can check it thanks to IdbFactory.supported:

import 'dart:indexed_db' as idb;
if (!idb.IdbFactory.supported) {
   return;
}

During development, it's very useful when you can see what's in your IndexedDB right in Dartium's Developer Tools:

IndexedDB

Suppose you end up in a situation where your object store is corrupted or you want to test the behavior when the database doesn't exist yet. In this case, you can right-click on the object store name in Developer Tools and select the Clear option from the context menu, and remove it completely.

Initializing IndexedDB

First, include the dart:indexed_db package with a prefix because it contains class names that are already used in dart:html (for example, the Request class):

import 'dart:indexed_db' as idb;

To create or open a database, call:

var db = await window.indexedDB.open('databaseName', version: 1,
    onUpgradeNeeded: onUpgradeNeededCallback
);

The onUpgradeNeededCallback function is called when you're opening a database with this name for the first time or when the version number is higher than the version of the currently existing database that allows you to migrate to a newer database structure.

Records in the database are stored in object stores, which is much like a database table from relational databases. Each database can contain multiple object stores independently on each other. As we'll use the object store name and a reference to our database multiple times in our app, we can create them as object properties:

static const String objectStoreName = 'articles_store';
idb.Database _db;

Creating an object store for our database is very simple:

void onUpgradeNeededCallback(idb.VersionChangeEvent e) {
  idb.Database db = (e.target as idb.Request).result;
  var store = db.createObjectStore(
        objectStoreName, autoIncrement: true);
}

A callback to onUpgradeNeeded is the only place where you can create new object stores. Setting autoIncrement to true makes an object store to take care of generating unique keys for new records inserted to the object store.

Fetching stored records

All operations in IndexedDB are performed via transactions. The first argument to transaction() is its scope, which can be multiple object stores. The second argument is a mode that can be readonly or readwrite. You can perform multiple readonly operations concurrently but only one readwrite operation at the time.

We're going to simply get a cursor object that points at the beginning of our object store and gets one record at a time. When used as a stream, it fires events for every record in the object store. The autoAdvance parameter set to true makes the cursor automatically move to the next record; otherwise, we would have to call the next() method manually:

loadDataFromDB() async {
  var trans = _db.transaction(objectStoreName, 'readonly'),
  var store = trans.objectStore(objectStoreName);
    
  Map<String, Map<String, String>>dbItems = new Map();
  var cursors = store.openCursor(
      autoAdvance: true).asBroadcastStream();
  // Bind an event which is called for each record.
  cursors.listen((cursor) {
    dbItems[cursor.key] = cursor.value;
  });

  // Wait until all records have emitted their events.
  await cursors.length;
  renderReadLaterItems(dbItems);
}

The renderReadLaterItems() method just renders all records into a list just like the renderReddit() method.

You can, of course, fetch a single record by its key, which, again, returns a Future object:

store.getObject(key)

Saving records

We need to update the renderReddit() method to handle our onClick event, add a clicked item to the object store, and reload all records:

void renderReddit(UListElement targetUl, List items) {
  var frag = new DocumentFragment();
  for (Map<String, String>item in items) {      
    AnchorElement aElm = new AnchorElement();
      ..text = '[save]';
      ..href = '#';
      ..onClick.listen((e) async {
        e.preventDefault();
        await this._save(item);
        // Let's not worry about performance now.
        loadDataFromDB();
      });

    /* … */
    frag.append(liElm);
  }
  targetUl.children.clear();
  targetUl.append(frag);
}

Here we're meeting Future objects again:

Future _save(Map item) {
  var trans = this._db.transaction(objectStoreName, 'readwrite'),
  var store = trans.objectStore(objectStoreName);
  
  // add() method persists record and returns a [Future] object.
  // Item's key is generated by our object store.
  store.add(item).then((addedKey) => print(addedKey));
  // We're not using await because only the transactions can tell
  // when it's finished. [Future.then()] is just better here.
 // [Transaction.completed] is an instance of [Future].
  return trans.completed;
}

Deleting records

Note that in order to remove a record from the object store, we need to know its database key:

Future _delete(key) {
  var trans = this._db.transaction(objectStoreName, 'readwrite'),
  // Delete record by its generated key.
  trans.objectStore(objectStoreName).delete(key);
  return trans.completed;
}

Similarly, we can remove all records:

trans.objectStore(objectStoreName).clear();

Indices

Although we didn't need any indicies in our app, it's worth mentioning how to use them. Just like all operations that modify database structures, we have to define indices in the onUpgradeNeeded callback:

store.createIndex('index_name', 'col_to_index', unique: true);

If we wanted, for example, to search our saved Reddit posts by their title, we would set the index like this:

store.createIndex('title_index', 'title'),

Let's set a search query to filter, say, only those posts that start with the letter R:

var trans = this._db.transaction(objectStoreName, 'readonly'),
var store = trans.objectStore(objectStoreName);
var index = store.index(titleIndex);
    
var range = new idb.KeyRange.lowerBound('R'),
var cursors = index.openCursor(
    range: range, autoAdvance: true).asBroadcastStream();
cursors.listen((cursor) {
  print(cursor.value['title']);
});

This is very similar to fetching all records, as we did a moment ago.

Note that we're opening the cursor over index instead of the entire store and passing a range argument, which is basically our search query. The idb.KeyRange item has four named constructors that you can use:

Key name

Description

KeyRange.bound(lower, upper)

Matches all values between these two bounds.

KeyRange.lowerBound(bound)

Matches all values with this lower bound and everything after it.

KeyRange.only(value)

Single value key range. Indexed column needs to match this value.

KeyRange.upperBound(bound)

Matches all values with this upper bound and everything before.

Note

Bounds don't need to be numbers. You can use strings for bounds as well.

Compared to any SQL-like database, indices in IndexedDB are very primitive, but thanks to its simplicity, IndexedDB can be available even on mobile devices with very limited resources.

Polishing the application

The finalized application can look like the following screenshot. You probably don't want to rewrite all the source code by yourself, so feel free to download the source code for this chapter and see how it's done.

Polishing the application

LocalStorage versus IndexedDB

LocalStorage is a key-value storage that can be used instead of IndexedDB in some situations. For example, in the application that we have just created, we could use LocalStorage instead of IndexedDB without any problem, but remember that LocalStorage is a very primitive storage and in terms of use cases, it's like cookies with less restrictions.

Note

If you're looking for a universal storage engine with multiple backends integrated, take a look at https://pub.dartlang.org/packages/lawndart.

What about WebSQL?

WebSQL was supposed to be a SQL variant that could run in clients' browsers. However, it's been discontinued by W3C and probably won't be supported by any browser in the future because the only available implementation was SQLite.

It's recommended that you use IndexedDB instead.

Note

You should remember that IndexedDB isn't an SQL-like database. It's not even a drop in replacement for WebSQL.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.50.222