The Story of my Migration to Dart 2


The latest version of Dart promises improved type safety and better client-side support. What else will it bring?

I am a power user of Google’s Dart programming language. I’ve always been a fan of the tooling and libraries, and since my first exposure to Dart almost three years ago, have published over 140 packages into the Dart ecosystem. To say that I like Dart would be a great understatement.

So, like many, it was only natural that I was happy to hear about the official release of the Dart SDK, version 2.0.0. It’s been quite some time coming, with months of frequent updates and changes to the VM and its surrounding tooling, as well as infrastructure like the Dart Dev Compiler, as well as Flutter, the ultra-hot framework for cross-platform mobile development.

All the goodness did not come for free, though. A lot of things broke, many with seemingly no explanation, and several things will never be the same.

Just like the title insinuates, this post is not so much about the features Dart 2 brings, but about my personal story of migrating personal projects, as well as the Angel framework to the new version of the language.

Porting really sucked

2fehox

I’m going to be honest – porting Dart code that I had written in Dart 1 to Dart 2 was not fun at all, for a variety of reasons. I spent several hours tracking down errors that were not picked up by the analyzer by default, only to explode Travis builds. Fortunately, the process of porting code got easier as it went along.

Cast errors, cast errors everywhere

IMHO, the biggest change in Dart 2 (by the number of errors it causes in old code), is that generic types are no longer erased, but reified.

That is to say (from this Gist by Arul Dhesiaseelan):

main() {  print(new List<String>() is <Object>();//true  print(new List<Object>() is List<String>);//false  print(new List<String>() is List<int>);//false  print(new List<String>() is List);//true  print(new Map() is Map<String,String>);//true}

Reified generics

Whereas Dart was previously gradually-typed with erasure of generics, it is now strongly-typed and sound, with reification of generics; that is to say, if you say that you’re using a list of integers, it must be a list of integers.

There is one thing to note, though. A list of integers can’t be just a list of integers, it must specifically be a list-of-integers.

var myIntegers = <dynamic>[1, 2, 3];doIt(numbers); // ERROR!void doIt(List<int> numbers) {    print('Hi');}

Even though I only have integers in my list, an error is thrown, because the list itself can contain objects of other types.

That, however, was a trivial example, that makes logical sense. If you want a list of integers, you’ll want to be sure you are getting a list of integers!

The problems, however, arose for me when I was sure I was getting a list of integers, but the compiler wasn’t.

Dealing with JSON/YAML can be improved (a lot)

This made dealing with JSON, YAML, and other configuration in Dart very tedious, as even though you might be sure you’re getting a Map<String, dynamic> (for example, if you yourself wrote the configuration file), if it’s not explicitly marked as <String, dynamic>, it’ll blow up in your face.

Dealing with JSON, just like any other strongly-typed languages (Java, C#, C++, etc.), was already not so great, but for years, I was able to work around this functionality with reflection in the form of json_god, which more or less works the same as JSON.NET or Jackson.

Eventually, though, I started getting hit with dozens of generic typing errors, and there seemed to be no solution, especially because dart:mirrors actually even ignores the type arguments you give it when calling functions, so I can’t explicitly call Map<T>.from.

Ultimately, the workaround was to create empty Maps, and then add each element in a loop. Not that bad, but far from the ideal solution.

By the way, you might need this function, which converts any Map to Map<String, dynamic>:

Map<String, dynamic> foldToStringDynamic(Map map) {  return map == null      ? null      : map.keys.fold<Map<String, dynamic>>(          <String, dynamic>{}, (out, k) => out..[k.toString()] = map[k]);}

When it got really inconvenient was when dealing with YAML. Dart’s built in JSON parser at least returns Map<String, dynamic> when it encounters a JSON object, but the YAML parser can’t provide the same guarantee, due to the nature of YAML.

So, when writing package:build_native, to parse user YAML configuration into a class generated via package:angel_serialize, I had preprocess the parsed input itself to convert it to a form that wouldn’t break the deserializer:

https://github.com/thosakwe/build_native/blob/master/build_native/lib/src/read_config.dart

Some community-powered options to ease deserialization have popped up, like stable|kernel’s package:codable, but I truly hope that the data serialization story improves as Dart goes forward. Especially because Dart is targeted primarily towards the client side, where handling JSON is extremely common, it would be nicer to have a smoother JSON serialization experience. The boilerplate is tolerable, because Dart truly is a strongly-typed language, so it makes sense that specific code needs to be provided to transform JSON text into concrete objects.

Implicit casts are enabled by default, even though they break everything

This one is a pretty simple fix; add the following to your analysis_options.yaml:

analyzer:  strong-mode:    implicit-casts: false

However, since implicit downcasts cause runtime errors without fail, every time, and the analyzer is indeed capable of identifying them, to me it would make a lot of sense to just enable this option by default.

This minor annoyance

This piece of code is no longer valid, for reasons of soundness, which make sense:

User user = await myService.read(...).then(UserSerializer.fromMap);

However, it’s still annoying, and the replacement is:

 var user = await services.userService          .read(post.userId)          .then((map) => UserSerializer.fromMap(map as Map))          as Iterable<User>;

What has become evident to me is that I actually quite value dynamic typing when it comes to Web development, at least on the server-side, when I know what data is coming out of the database, even if the compiler doesn’t believe me. I never thought I’d say that.

Personal changes

Dart isn’t the only thing that’s changed, though. When I started with Dart was my first real exposure to the world of open-source software. I was finishing my junior year of high school, and the only commercial software experience, outside of games I made when I was younger, that I had was a few years doing freelance Web development and automation jobs.

I didn’t really know what exactly I liked in a programming language, because I no real idea about what I would personally be trying to achieve with my programming. Eventually, as I started to venture into launching my own SaaS products as a sole proprietor, I really got used to the gradual typing of Dart 1, and having the benefit of good completion and static error detection, while also having the compiler trust me to a degree, when I was writing Web servers, and couldn’t care less about whether my List<int> was really just a List<dynamic>.

I’ve in the meantime become a lot better at C++, and have done a lot of experimentation with writing compilers and interpreters over the years, and the features I really appreciated from Dart are what I look for when trying other languages and platforms (for example, I really hate Python, but that’s a topic for another blog post).

Conclusion

Dart has been through a lot over the past year or so; in fact, it’s been through so much change, that it’s not even the same language anymore. Dart’s goals are not the same as they were when it was unveiled as an in-browser competitor to JavaScript; it’s a mature language ready for highly productive front-end workflows.

I’m not going to lie, and say that I think Dart is perfect. I have several gripes with the language, and disagree with some of the decisions introduced in Dart 2.

That being said, I’m sticking with Dart for the foreseeable future. All the hard work the team has done was for the good of the language, and was done intentionally to improve working with Dart. After all, if it weren’t for Dart 2, then Flutter, arguably Dart’s "killer app," would not be possible at all.

Disclaimer

I’m still a huge fan of Dart; these are just my honest opinions about it that I haven’t voiced before. That being said, this is also just a personal blog post, and not an academic work. Factor in the fact that I’m relatively unknown, and who really cares?