Dart is kinda amazing
Dart is simple, yet elegant, expressive and complete. Talking about Dart, language features, and mostly the good.
Contents
What? #
Dart is a multi-purpose, multi-paradigm, cross-platform, and JIT/AOT-compilation runnable language. You mostly hear about it in the context of Flutter, but I want to talk about the language itself, in isolation.
Why? #
I'll list the parts I like about the language, not in any particular order. There are of course things that I don't like, but they are not for this post. In general, the ugly parts are mostly non-fundamental, even if they are annoying (semicolons, looking at you.)
Familiarity #
The post will contain a lot of Dart examples, but they should be pretty universally understandable. Given the initial "replace JS" purpose of the language and the later focus on the mobile, I believe the team intentionally made it so innately simple to understand.
class Dog {
void bark() {
print('Woof!');
}
}
int fibonacci(int n) {
if (n == 0 || n == 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
final result = fibonacci(20);
Almost Interslavic.
Type system #
Dart has a really tasty type system, in a way that it is possible to express any reasonable constraint, model most scenarios with data that can be modeled with data, and not find yourself choosing from multiple "right" ways of doing the same thing or fighting with a synthetic roadblock.
extension type Meters(int value) implements int {}
interface class Symmetric {
Meters get val;
}
sealed class Form {}
final class Circle extends Form implements Symmetric {
@override
final Meters val;
Circle(this.val);
}
On top of all that, it is safer than something like TypeScript and gives reasonable type errors.
Sound #
Dart 2.0 introduced soundness that obliterated the last of the previous dynamic Dart legacy – Dart made a significant journey.
Starting as a dynamic language, to making types opt-out, to being sound and null-safe – currently, we have a very "checked" language that is protecting the developer as much as possible without being over the top.
String? doubleName() => 'Yakov' * 2;
final name = doubleName();
final String nope = doubleName();
Because of that, both writing new code and creating abstractions AND refactoring is a process of little friction – the compiler holds your hand and screams at you in the right proportions, at the right time.
Complete OOP #
OOP is a base paradigm for Dart – even though multiparadigm and very functional approaches are allowed, on the fundamental level everything is object-oriented, and I don't think that is a bad thing, with this particular flavor.
interface class HasAge {
int get age;
}
interface class HasName {
String get name;
}
base class Creature implements HasAge, HasName {
@override
final String name;
@override
final int age;
Creature(this.name, this.age);
}
interface class CanSpeak {
void speak();
}
The main points about Dart's OOP are:
- Primitives are classes
- The Diamond problem is solved through mixins
- Classes are open by default
- There are no data classes, classes are co-data by default
- Classes are highly tunable by a list of modifications
- Objects have an implicit interface, taking every interaction to some level of abstraction by default
Documentation has a great entry on the specifics, but in essence, OOP is of "true" flavor for the current meaning of the paradigm and without any significant rough edges.
mixin SelfDescribingAbility on HasAge, HasName implements CanSpeak {
@override
void speak() {
print("My name is ${name} and I'm ${age}");
}
}
class Human = Creature with SelfDescribingAbility;
Flexible, yet safe and complete – once again, boring and tasty.
Anonymous Records #
The latest significant feature, marking a new era in data abstractions and even some first whiff of (almost)polymorphism.
Destructuring, ad-hoc data polymorphism, named and unnamed fields – all the standard features. However, Records open a few interesting doors.
Creating anonymous objects with full inference
final counter = Module(($) {
final increment = $.trigger<()>();
return (
increment: increment,
value: $.store(($) {
$.on(increment, (_) => $.self + 1);
return 0;
})
);
});
Performing operations on multiple inner values
final $Order = (
('city', City.new.$).with defaults(City('Haifa')),
('comment', Comment.new.$).optional(),
('buildings', $List(Building.new.$)),
).jsonSchema();
Or having some level of (pseudo)polymorphism
String doubleString(String val) => val * 2;
(
doubleString,
print
)
.compose('Hello, world')
Zero-cost abstractions #
New, relatively niche feature, but another nod to the completeness and relative power of the type system.
Extension types allow for both increase of the correctness and well, extending types, by composition.
Once again, the documentation does the job best of explaining.
Examples can be rather simple.
extension type Meters(int value) {}
Or rather complex
extension type Behavior<B, D>((B, D) _self) {
B get behavior => _self.$1;
D get data => _self.$2;
}
extension type Identity<T>(T _self) {
T get id => _self;
}
typedef Described<T> = Behavior<String, Identity<T>>;
extension type Describable<T>(Described<T> self) implements Described<T> {
String describe() => '${behavior} ${data.id}';
}
typedef Logged<T> = Describable<Behavior<void Function(String), T>>;
extension type Loggable<T>(Logged<T> self) implements Logged<T> {
void log() {
data.id.behavior(describe());
}
}
Not only OOP #
After everything written above, it is obvious that Dart not only has a complete OOP system but also offers a list of multi-paradigm features that even further remove restraints and allow for the choice of approach of a fitting paradigm for a job.
In my experience, having a strong OOP-based abstraction under the hood on the library level with a mostly functional interface is one of the most organic approaches, and Flutter demonstrates it the best.
/// I'm a function in disguise!
class CounterWidget extends StatelessWidget {
const CounterWidget({
super.key,
});
@override
Widget build(BuildContext context) {
final (:increment, :value) = context.use(counter);
return Text(
context.use(value).toString(),
style: Theme.of(context).textTheme.headlineMedium,
);
}
}
The underlying Element
system that powers descriptions like in the above is a classic, inheritance-based OOP.
Tooling #
Another selling point by offering minimal friction – every single piece of needed tooling is available out of the box, working, fast and simple.
Take a look at compilation, Flutter CLI and documentation tooling.
This solid toolkit allows for some magic like JIT/AOT compilation, hot reload, small output binaries, and other features that just feel and work correctly.
pub.dev #
An npm
for Dart – accounting for isEven
-style packages, the current choice of packages is not less in terms of functionality. Most of your common problems are solved and nicely packages, either as an official dart tools package or any of 51k public packages.
There are some silly ones, of course.
Standard Library #
Even not touching on the pub.dev, those are some of the in-built (= canonical, efficient, and idiomatic) libraries.
Async and Concurrency #
Once again, no particular innovations, but a very complete and pragmatic approach with just the right level of abstraction. TL;DR is BEAM processs/Actor model + Futures and async/await.
Also, generators.
In practice, working with Dart's models in this domain feels natural, pragmatic, and straightforward. There is enough syntactical sugar to freely express the desired scenario, and concurrency is a blast.
Some time ago, Isolates got much lighter and faster, and currently, it is possible to have the full benefit of running a piece of code in a separate process simply by spawning a green thread
int fib(int n) => n <= 1 ? 1 : fib(n - 1) + fib(n - 2);
await Isolate.run(() => fib(40));
Spicy features #
Dart has some interesting features that are unique or almost unique to the language. They are rarely used by the end developer but can be leveraged by library code to achieve elegant solutions to complex problems.
Some of those features are:
What next? #
Please explore Dart more! It can be in the form of Flutter, contributing to the Dart OSS community, or simply by looking at the documentation. I see the language as a really interesting example of how limited innovation and an extreme commitment to each layer produced such a sturdy worktool.
I have a list of open-source Dart projects on my profile, which can be one of the starting points of interaction with the Dart ecosystem :)