Skip to content

Instantly share code, notes, and snippets.

@stevenroose
Last active August 29, 2015 14:15
Show Gist options
  • Save stevenroose/2c9cd8da0839a13f1137 to your computer and use it in GitHub Desktop.
Save stevenroose/2c9cd8da0839a13f1137 to your computer and use it in GitHub Desktop.
Using deferred loading to create environment-independent libraries in Dart

Using deferred loading to create environment-independent libraries in Dart

The problem

Upon creating my first Dart library, I bumped into the issue of the environment-dependent libraries dart:io and dart:html, and most importantly the fact that some very similar functionality is present in both of them.

It bothered my ever-since that many libraries had to split up into two counterparts and so did some of the classes within. Let's say one of your classes uses WebSockets. The WebSocket is defined differently in dart:io and dart:html. So what you have to do create an abstract class for the general case and then create two subclasses in their own libraries that fill the gap of the WebSocket interface.

It would look like this:

  • a generic library:
library remote;

abstract class Remote {

  // an abstract constructor
  Remote({params}) {
    [...]
  }
  
  // all the general functionality
  [...]

  // the environment-specific functionality
  void sendOverWebSocket(message);
  void receiveFromWebSocket(message) { // to be called on websocket message
    [...]
  }
}
  • a dart:html implementation
library remote.html;
import "dart:html";
import "remote.dart";

class HtmlRemote extends Remote {

  WebSocket _ws;
  
  // a concrete constructor
  IoRemote({params}) : super({params}) {
    _ws = new WebSocket();
    _ws.listen(receiveFromWebSocket);
  }

  // implementation of the abstract methods
  void sendOverWebSocket(message) {[...]}
}
  • a dart:io implementation
library remote.io;
import "dart:io";
import "remote.dart";

class IoRemote extends Remote {

  WebSocket _ws;
  
  // a concrete constructor
  static Future<IoRemote> connect({params}) {
    return WebSocket.connect(...).then((ws) {
      return new IoRemote._(params);
    });
  }

  IoRemote._(ws, {params}) : super({params}) {
    _ws = ws;
    _ws.listen(receiveFromWebSocket);
  }

  // implementation of the abstract methods
  void sendOverWebSocket(message) {[...]}
}

(Note the difference in the WebSocket API for opening a new WebSocket.)

With this approach, users have to import a specific library depending on the environment they are programming for. Furthermore, all libraries depending on such a library will also have to split up, even though their own implementation is completely environment-agnostic.

A possible solution

Since I've been thinking a lot about this, I came up with a solution. It's not a beautiful solution, but it makes the library usable across environments and gives users a single method for constructing a Remote instance. Since anonymous classes are not supported in Dart, we will still need three separate libraries, but two of them will be hidden from the user.

library remote;
import "remote-io.dart" deferred as io;
import "remote-html.dart" deferred as html;

abstract class Remote {
  // all the same code as above

  // a generic way to create a Remote instance
  static Future<Remote> connect({params}) {
    Completer c = new Completer();
    io.loadLibrary().then((_) {
      c.complete(new io._IoRemote(params));
    }, onError: (_) {});
    html.loadLibrary().then((_) {
      c.complete(new html._HtmlRemote(params));
    }, onError: (_){});
    return completer.future;
  }

  // using the async/await functions that are yet to become available in the stable Dart release,
  // a factory constructor could be defined as well. (Although it is advised not to use await in constructors.)
  factory Remote({params}) {
    return await Remote.connect(params);
  }
}

The other two libraries are only slightly changed, just to make sure they are hidden from the documentation to avoid confusion.

library _remote_io;
import "dart:io";
import "remote.dart";

class _IoRemote extends Remote {

  WebSocket _ws;
  
  // a concrete constructor
  IoRemote({params}) : super({params}) {
    WebSocket.connect(...).then((ws) {_ws = ws;});
    _ws.listen(receiveFromWebSocket);
  }

  // implementation of the abstract methods
  void sendOverWebSocket(message) {[...]}
}
  • a dart:html implementation
library _remote_html;
import "dart:html";
import "remote.dart";

class _HtmlRemote extends Remote {

  WebSocket _ws;
  
  // a concrete constructor
  IoRemote({params}) : super({params}) {
    _ws = new WebSocket();
    _ws.listen(receiveFromWebSocket);
  }

  // implementation of the abstract methods
  void sendOverWebSocket(message) {[...]}
}

With this approach, the single way to obtain a Remote instance is to use the Remote.connect method and it's independent on wether the code is run in the browser or in the Dart VM. (I tested this and it works for dart2js as well.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment