In which I port a snazzy little JavaScript audio web app to Dart , discover a bug, and high-five type annotations. Here's what I learned.



[As it says in the header of this blog, I'm a seasoned Dart developer. However, I certainly don't write Dart every day (I wish!). Don't interpret this post as "Hi, I'm new to Dart". Instead, interpret this post as "I'm applying what I've been documenting."]



This post analyzes two versions of the same app, both the original (JavaScript) version and the Dart version. The original version is a proxy for any small JavaScript app, there's nothing particularly special about the original version, which is why it made for a good example.



This post discusses the differences between the two implementations: file organization, dependencies and modules, shims, classes, type annotations, event handling, calling multiple methods, asynchronous programming, animation, and interop with JavaScript libraries. Finally, I detail the lessons learned.

Background





Mr. Paul Lewis wrote a snazzy little web app for audio processing. His Music DNA app uses JavaScript to load up an audio file, play it, and visualize the sounds in a browser. It’s a cool little demo of what’s possible on the web platform.









I’m always impressed with Paul’s designs and code, so I asked myself: Could I easily port this app to Dart? Was Dart up to the challenge of a web audio app? Is there benefit to using Dart for little apps like this?





Spoiler: yes, yes (mostly), and yes. Read on!

Fine print





I ported the original JavaScript version of Music DNA app as of this revision . The code for the Dart version is also available. It’s possible that by the time you read this, the original JavaScript version will have changed. I also attempted to minimize the differences between the two code bases. I tried to keep the names the same (when appropriate), and the number of files the same (again, when appropriate).

Organization





In which I examine file layout and navigation of both the original project and my Dart version.

Original version





The original version collected all files into a single directory.





Original version





To discover the “main” JavaScript file, I opened index.html and I found six script files:





Original version





Hm, where does the application start? I wasn’t sure. I opened each file, in order. It appeared that bootstrap.js contained the “entry point” of the application.





The entry point was a “immediately invoked function expression” (IIFE):





Original version

Dart version





Dart’s package manager, pub , defines a layout convention for packages and apps. The Dart version has the HTML, and Dart files for the app, inside a web directory. The library files (files that contain libraries that might be useful for any app) are inside a lib directory. (More on this soon.)





Dart version





To find the “entry point”, I opened the main HTML file (music_dna.html). I found a single Dart file and two supporting JavaScript files.





Dart version





I did not port the id3.js file to Dart for two reasons: it was minified, and I wanted to test Dart-JavaScript interop. The dart.js file is boilerplate: it helps the app run in both JavaScript engines and Dart VM.





Dart version





The application starts from the main() function in bootstrap.dart. All Dart programs start at the main() function.

Dependencies and modules





In which I examine how both versions handle linking to libraries or modules.

Original version





JavaScript doesn’t have a native library or module system. The original version listed each “module” as a separate script tag from the main HTML file (screenshot above). The order of the scripts is important, as scripts that load later depend on scripts that were loaded earlier. The code does not explicitly declare its dependencies.





For example, the bootstrap script creates a new instance of MusicDNA. The bootstrap.js file does not include a reference to the location of the definition of the MusicDNA function.





Original version





The MusicDNA function comes from the music-dna.js file, which is loaded before the bootstrap.js file via script tags.

Dart version





The Dart language natively supports libraries . A library can import other libraries. For example, the bootstrap.dart file imports its dependencies:





Dart version





The Dart SDK has some standard libraries like dart:html for DOM access, dart:js for interop with JavaScript (more on this below), and more. The code also imports music_dna.dart which explicitly provides the MusicDNA class. Using the show keyword with import makes it easy to glance at what names are provided by what library.





The music_dna.dart file is its own library, which also declares its own dependencies:





Dart version





Notice the two types of URIs used in imports: dart:____ and package:_____ . Imports from dart: come from the Dart SDK, and imports from package: refer to libraries that live in packages . A package is a bundle of Dart code, conforming to some layout conventions, and declared by a pubspec.yaml file .





Both libraries and applications can live inside of a package. The music_dna application is itself a package. The music_dna library imports the audio library from its own package. This is a handy way to avoid using relative (and brittle) paths that would have to reach up and out of the file’s directory.





Back to libraries: A library can be split into multiple parts . The audio.dart file declares the library, its imports, and the two other files that contain the library’s classes: audio_parser.dart and audio_renderer.dart.





Dart version





An abbreviated way to look at the libraries and dependencies of the Dart version:





Dart version

Shims





In which I explore how both versions deal will shimming browser APIs, such as Web Audio and requestAnimationFrame. A shim is a thin layer of code that compensates for browser differences in function names or APIs.

Original version





The original version shimmed the APIs using a common JavaScript language pattern. For example, here is the shim for AudioContext (from Web Audio) in audio_parser.js:





Original version





The original code also shimmed requestAnimationFrame in bootstrap.js:





Original version

Dart version





The dart:html library hides the shims, so I don’t need to worry about it. For example, I just needed to import the dart:web_audio library and then use the AudioContext name.





Dart version





Dart version





When I compile the Dart app to JavaScript, the shims are included in the generated JavaScript. This ensures the generated code works in all modern browsers.

Classes





In which I examine how both versions model and construct concepts.

Original version





JavaScript is not a class-based object oriented language. The original version used standard JavaScript patterns for “classes”.





Original version





The JavaScript version initializes fields by constructing new objects as well calling methods. The object isn’t initialized until the entire body of the AudioParser function is run.

Dart version





Dart is a class-based object oriented language. I created a formal class for AudioParser.





Dart version





The Dart version initializes most fields inside of the constructor body. By the time the constructor body is run, the object is initialized.

Type annotations (aka discovering a bug)





In which I use type annotations and discover a bug in the original code.

Original version





JavaScript doesn’t have a way to explicitly state that a variable can hold a specific type of object. Fields are simply marked as var , as in this example:





Original version





It’s unclear what kind of function can be assigned to audioDecodedCallback . I can assume what sourceNode or audioRenderer can be, because I know this is a Web Audio app and there’s another file called audio-renderer.js.





Method parameters also do not have a type annotation. Here is one example:





Original version





Can you spot the bug in the above example? I didn’t, until I converted the code to Dart.

Dart version





Type annotations are optional in Dart, but I love ‘em. I use type annotations as inline and explicit docs for my fellow programmers and my productive tools.





We’ve seen it before, but here’s the recap of how I use type annotations for fields in AudioParser:





Dart version





So, gainNode is of type GainNode . I could probably have guessed that, if I knew there was a Web Audio type called GainNode. Without the type annotation, editors and analyzers can’t infer that gainNode is a GainNode . And that’s important, because…





Dart version





Dart Editor and the Dart analyzer knows that disconnect() takes an int and not a GainNode . A warning is displayed, and bug identified!





Turns out, the code didn’t need the call to disconnect() anyway. But, I feel more confident knowing that tools can identify potential errors.

Event handlers, and calling multiple methods on the same object





In which I encounter a Dart-y DOM, use a Dart language feature, and reduce the amount of code I have to type.

Original version





The traditional way to listen for DOM events (like drop or dragover ) is to use addEventListener() . This method takes a string as the first parameter. If you misspell the name of the event, you will not get a warning, and the program will keep on trucking. There is no code completion for the values of the strings, because the language can’t express all the valid values for the first argument.





Here is an example, from the original code, of listening to multiple events on a single fileDropArea object.





Original version

Dart version





The dart:html library offers DOM APIs that are more toolable. There are specific methods for specific events, for example onDrop . If you misspell the method name, the tools can warn you about an unknown method. Also, you can use code completion to help discover other events to listen for.





Dart supports method cascades , which help reduce repetitive code. If you need to call multiple methods on a single object, cascade the calls and avoid repeating the variable.





Here is the Dart code for setting up the drag and drop area:





Dart version





Notice how fileDropArea is stated only once. The double dot (..) syntax is the cascade. Compare this version to the original JavaScript version above.

Asynchronous callbacks





In which I explore how both versions handle asynchronicity.

Original version





When something needs to run “later”, JavaScript calls a callback function . The MusicDNA app uses asynchronous callbacks when decoding audio data.





MusicDNA has a onAudioDataParsed function. MusicDNA creates a new AudioParser, and passes onAudioDataParsed to it. MusicDNA calls audioParser.parseArrayBuffer() , which calls an asynchronous decodeAudioData method.





Control is returned to the browser, and events are processed as normal. The audio is decoded in the background. Sometime later...





When decodeAudioData is finished and is successful, it calls audioParser.onDecodeData . When onDecodeData is finished, it calls onAudioDataParsed . Now we’re back in MusicDNA!





As you can see, a function can get passed around, with no clear line of execution.





Here’s the code, see if you can follow the callback:

Original version





In AudioParser:

Original version





parseArrayBuffer() calls decodeAudioData() which asynchronously calls onDecodeData.

Original version





The onDecodeData function calls the callback given to AudioParser from the constructor:





Original version

Dart version





Dart’s core SDK has a Future class, which represents values available sometime in the future. An asynchronous function or method returns a Future instead of accepting a callback as a parameter. The Future completes when the asynchronous function is finished, and a value is available.





In the Dart version, parseArrayBuffer returns a Future, as seen below:

Dart version





Now that the async methods can return futures, the code can get simplified. For example, Futures are chainable, so music_dna.dart looks like this:





Dart version





The above code reads:





Read the file into an array buffer. When the loadEnd event is done, then parse the array buffer. When the array buffer is done, then read the duration. Return a future to caller of parse knows when parsing is finished.

Animation





Modern web apps animate with requestAnimationFrame , which is an API that lets your app get notified when the browser is ready for your app to draw a frame.

Original version





Again, because JavaScript uses callbacks for async notifications, the requestAnimFrame (a shimmed version of requestAnimationFrame) uses callbacks.





Original version

Dart version





Dart uses Futures in many places, including in the HTML libraries. Specifically, requestAnimationFrame has been rewritten to return a Future that completes when the frame is ready.





Dart version

Code size





There are two ways to look at code size: the original code that the developer works with, and the compiled, minified, gzipped code size. It's important to stress that neither the original version or the Dart version made any attempts to create the most terse code possible. Line counts include blank lines and comments. I believe this app is too small to make any reasonable claim about lines of code correlation to language.

Original version





The important JavaScript files, their sizes, and number of lines:

audio-parser.js: 1823 bytes, 78 lines

audio-renderer.js: 2751 bytes, 120 lines

bootstrap.js: 1803 bytes, 69 lines

music-dna.js: 3608 bytes, 130 lines Total bytes for the scripts (not counting id3.js): 9985 bytes.





Total lines for the scripts: 397 lines.





The original app did not concat the JS files or minify them, however most developers should perform those steps with "real" apps. Most web servers will gzip on the fly, so it's worth noting that the total number of gzipped bytes from scripts (gzipping each file individually) results in 3825 bytes.

Dart version





The important Dart files, their sizes, and number of lines: lib/audio.dart: 427 bytes, 12 lines

lib/src/audio_parser.dart: 1786 bytes, 68 lines

lib/src/audio_renderer.dart: 2273 bytes, 99 lines

web/bootstrap.dart: 1763 bytes, 71 lines

web/music_dna.dart: 1623 bytes, 59 lines Total bytes for the scripts (not counting id3.js): 7872 bytes.





Total lines for the scripts: 309 lines.





Compiling and minifying the Dart scripts to JavaScript generates 143kb bytes. Gzipping the generated JavaScript (as most web servers and browsers compress/decompress on the fly) results in 41kb bytes. (For comparison, jQuery itself is 32kb minified and gzipped. However, the original Music DNA app does not use jQuery.)

Interop with JavaScript libraries





The Music DNA app uses a JavaScript file called id3.js to read the ID3 tags and extract metadata like song title and artist.

Original version





The id3.js file is included as a script tag, just like any other JavaScript file.





Original version





Using the ID3 functionality is easy, as it’s just JavaScript in JavaScript. Here is how the ID3 library is used inside of bootstrap.js:





Original version

Dart version





Using JavaScript files in Dart requires the dart:js library, which provides Dart-JavaScript interop. I had to import the dart:js library, explicitly get a reference to the ID3 object (which originally came from the id3.js file), and use the callMethod method to call JavaScript methods.





I was able to use id3.js in the Dart app, but the integration isn’t as trivial as it is with JavaScript-JavaScript interop.





Here is the Dart code:





Dart version

Lessons learned





Porting the Music DNA app from JavaScript to Dart was a lot of fun. Here’s what I learned:





Futures are great! They really simplify API design.

Type annotations save the day. I found a bug in the original code after I ported it to Dart, thanks to type annotations and code analysis.

You can interoperate with JavaScript from Dart, it’s just not a “cut and paste” job.

It’s easy to tell where Dart programs start.

Compiled, minified, and gzipped apps about this size end up a little bigger than jQuery.

Dart’s library mechanism makes it easy to determine where names come from.

Dart’s HTML library shims APIs like requestAnimationFrame for you.

Method cascades are sweet.

Code completion. Love it. Especially helpful when exploring new APIs.





There are other language and library features of Dart that I didn't get a chance to use. If you're interested, there is a Dart language tour that might be interesting to you.





Of course, most of the techniques used by Dart in this article aren't impossible with JavaScript. In fact, future versions of JavaScript should get features like modules and promises. They just aren't built-in, obvious, or easy "out of the box" with JavaScript as we know it today. And this is where I begin to see some of the real power with Dart: the out of the box experience is really good. Shims? yup. Futures? yup. Layout convention? yup. Classes? yup. Type annotations (instead of comment)? yup. Dart seems to have a broader and more functional base on which we can build. I like it.



