My favorite Dart features - And why I love them
Keff
Posted on June 3, 2022
Hey there 馃憢
It's no secret that I love Dart and Flutter. In this post I will share some of my favorite Dart features and why I love them. I will also share some examples on why I think they're useful and how they help us write Flutter apps.
What is Dart exactly?
Well, it's yet another programming language. Dart is client-optimized, meaning it's optimized to run on the client side. It focuses on making the client experience as fast and stable as possible.
Dart is usually used with Flutter to build cross-platform apps, with rich, homogenous and beautiful UI with almost native performance and with most code shared between platforms. In most cases performance will not be an issue, if you need performance you can always write native code.
Why do I love Dart?
In summary, it's really ergonomic and it helps us write safe cross-platform code faster than we would with other solutions I've tried.
聽Ergonomic? What does that mean?
Ergonomics: an applied science concerned with designing and arranging things people use so that the people and things interact most efficiently and safely
In my own words: it's a term used to denote that a system, be it a chair, a mouse, a shoe or even a programing language is designed to be as confortable and safe as posible.
In the case of Dart, this is accomplished by a variety of factors, including but not limited to:
Table of contents
- Dart is type safe
- Mixins
- Named constructors
- const vs final
- No need to use the "this" keyword most times
- Formal parameters
- Named parameters
- Cascade notation
- Operator overloading
- Conditionally adding items to literal lists
- Typedefs
Dart is type safe
It uses a combination of static type checking and runtime checks to ensure that a variable鈥檚 value always matches the variable鈥檚 static type, sometimes referred to as sound typing. Although types are mandatory, type annotations are optional because of type inference.
Mixins
Have you ever wanted to extend a class from multiple other classes? Or extract common functionality that does not fit into any other abstract class? Then mixins is what you need. You can define a mixin and then use it in some other classes that share the functionality. Without needing to create an extorsionate amount of classes.
They are similar to PHP's traits.
This is what Dart says about them:
Mixins are a way of reusing a class鈥檚 code in multiple class hierarchies.
Let me show you an example:
class Musician extends Performer with Musical {
// 路路路
}
class Maestro extends Person with Musical, Aggressive, Demented {
Maestro(String maestroName) {
name = maestroName;
canConduct = true;
}
}
mixin Musical {
bool canPlayPiano = false;
bool canCompose = false;
bool canConduct = false;
void entertainMe() {
if (canPlayPiano) {
print('Playing piano');
} else if (canConduct) {
print('Waving hands');
} else {
print('Humming to self');
}
}
}
Example extracted from Dart docs on mixins
As you can see mixins allow us to compose classes with a bit more freedom than just by inheritance.
How do they help us on the day-to-day job?
It allows us to separate functionality in a more granular way, keeping the class hierarchy cleaner and easier to follow and maintain.
Real life example:
When writing Flutter apps, you sometimes need a particular Widget to have a loading state, i.e. when loading some data or calling an API. For simple apps or widgets, it's sometimes easier and simpler to just add a state to the widget instead of adopting a state management solution like Provider or rxdart.
So if your app is going to have multiple widgets that need to manage the loading state, instead of creating an abstract class that implements it, we can instead create a mixin to contain that particular logic.
mixin StateLoading<T extends StatefulWidget> on State<T> {
bool isLoading = false;
setLoading(bool isLoading) {
setState(() {
this.isLoading = isLoading;
});
}
startLoading() => setLoading(true);
stopLoading() => setLoading(false);
}
In the above example, we create a mixin that can only be applied to a class that is or extends State (we tell dart that by using the on
keyword)
Named constructors
Named constructors are similar to Java's constructor overloading, but with named constructors instead of overloading.
const double xOrigin = 0;
const double yOrigin = 0;
class Point {
final double x;
final double y;
// Regular constructor
Point(this.x, this.y);
// Named constructor
Point.origin()
: x = xOrigin,
y = yOrigin;
}
How does this help us build better software?
It can make code more descriptive, for example let's say you build a class that represents a value in some DB which can be nullable.
We could force the implementer to use named parameters, to make it more clear that that field is nullable.
Check it out, instead of writing this
final myField = Field(name: 'test', defaultValue: 'bla', nullable: true);
We can force the implementor to write it like this:
final myField = Field.nullable(name: 'test', defaultValue: 'bla');
IMO this is better that the previous example. The first thing we read after the class name is whether the field is nullable or not. And when dealing with nullable data, I think it's really important to put that in front, as to prevent mistakes and make it really clear. Though this is kinda subjective, would love to hear you opinions on this.
const vs final
One thing I like as opposed to other languages is the differentiation of final
and const
keywords.
-
final
A variable that can only be assigned one, but you can still add items to a final collection for example.
final name = 'boby';
name = 'jhon'; // Will give an error
final Map<String, String> testMap = {};
testMap = {}; // Will give an error
testMap['key'] = 'value'; // This is allowed
-
const
A compile-time variable can also only be assigned once (it behaved like final). The difference is that it's compile time and will fail when compiling instead of at runtime. Using const on an object, makes the object鈥檚 entire deep state strictly fixed at compile-time and that the object with this state will be considered frozen and completely immutable.
const name = 'boby';
name = 'jhon'; // Will give an error like with final
const Map<String, String> testMap = const {};
testMap = {}; // Will give an error
testMap['key'] = 'value'; // This will give error when compiling
I think Dart does a pretty good job of keeping the semantics and the keywords nicely clear and distinct. (There was a time where const was used both for const and final. It was confusing.) The only downside is that when you want to indicate a member that is single-assignment and on the class itself, you have to use both keywords: static final.
Excerpt from news.dartlang.org
No need to use the "this" keyword most times
This is not exclusive to Dart, there are other languages out there that don't require you to write the this
keyword all over the code.
In most cases you don't need to use it, as dart knows if it's an instance variable or not. Dart style recommends to only use the "this" keyword when there is a name conflicts, like the one shown below:
class Point {
double x = 0;
double y = 0;
Point(double x, double y) {
this.x = x;
this.y = y;
}
}
Formal parameters
The pattern of assigning a constructor argument to an instance variable is so common, Dart has initializing formal parameters to make it easy.
What's that you ask?
Take this common pattern:
class Point {
double x = 0;
double y = 0;
Point(double x, double y) {
this.x = x;
this.y = y;
}
}
Instead of manually assigning the parameters to the instance variables, we can write that using formal parameters as follows:
class Point {
final double x;
final double y;
// Sets the x and y instance variables
// before the constructor body runs.
Point(this.x, this.y);
}
Initializing parameters can also be used to initialize non-nullable or final instance variables, which both must be initialized or provided a default value.
Named parameters
Another thing I love about dart is the ability to have named parameters in your constructors and methods. Languages such as python also allow this.
Have you ever had a class or method that needs to receive a good amount of arguments? And ended up looking something like:
superMethod(true, 'enabled', Invoker());
Well with dart we can make more clear what each argument is by using named parameters.
superMethod(
propagate: true,
statusMessage: 'enabled',
actionInvoker: Invoker(),
);
And you define them as follows:
void superMethod({
bool propagate,
String statusMessage,
Invoker actionInvoker,
}) { // Some code }
Notes
- This is not limited to methods, you can also make use of named parameters in class constructors
Cascade notation
Cascades (.., ?..) allow you to make a sequence of operations on the same object. In addition to accessing instance members, you can also call instance methods on that same object. This often saves you the step of creating a temporary variable and allows you to write more fluid code.
Excerpt from dart.dev guide on cascade notation
Let's look at a common example without using cascading:
var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;
this is very common to do, we first create a instance of a class. And then we modify some instance variables, or call some method. But as you can see there's a bit of repetition (needing to write the name of the variable for each variable we want to modify) which just adds noise to the code.
Now let's look at the same example but using cascading:
var paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;
As you can see this accomplishes exactly the same as not using cascading but ends up with more readable and less noisy code.
It's not limited to variables
You can also call methods using the cascade notation, like so:
querySelector('#confirm') // Get an object.
?..text = 'Confirm' // Use its members.
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed!'))
..scrollIntoView();
PD: Examples extracted from dart.dev guide on cascade notation
Operator overloading
Dart allows classes to define certain operators, like ==
, +
, *
, etc... To make it more ergonomic to work with them.
Let see an example, using the Point class used in previous examples. Let's say we want to sum the coordenates of 2 points together. The normal way would be to do it manually:
final point = Point(1, 2);
final point2 = Point(2, 2);
final pointSum = Point(point.x + point2.x, point.y + point2.y);
Now let's define a sum operator in the point class:
class Point {
final double x;
final double y;
Point(this.x, this.y);
Point operator +(Point other) {
return Point(x + other.x, y + other.y);
}
}
Now we can sum points like this:
final point1 = Point(1, 2);
final point2 = Point(2, 2);
final pointSum = point1 + point2;
This hides the implementation and makes it a lot cleaner.
Conditionally adding items to collections
This is a small feature, but I really like it. When manually defining a literal list, map, set or other collections, we can conditionally decide to add or not, without needing to temporarily store the collection in a variable:
Regular way of doing it
final myList = [];
if(someCondition) list.add(value);
if(!someCondition) list.add(value2);
Dart's way
final someCondition = true;
final myList = [
if(someCondition) 'test';
if(!someCondition) 'test2;
];
The list will result in ['test']
, test2 will not be added if condition is not met.
Using it inside a map:
final map = {
if(condition) 'key': 'value,
};
Typedefs
Dart allows us to right what are called typedefs. Encapsulates function signatures or complex types in a named typedef, for more readable code:
typedef ListMapper<X> = Map<X, List<X>>;
Map<String, List<String>> m1 = {}; // Verbose.
ListMapper<String> m2 = {}; // Same thing but shorter and clearer.
Note: Dart recommends using inline function types instead of typedefs for functions, in most situations. However, function typedefs can still be useful:
typedef Compare<T> = int Function(T a, T b);
int sort(int a, int b) => a - b;
void main() {
assert(sort is Compare<int>); // True!
}
Version note: Before 2.13, typedefs were restricted to function types. Using the new typedefs requires a language version of at least 2.13.
聽Summary
We've taken a look at some of my favorite features from the Dart language, seen demo examples, and some real world examples. We've also compared how it used to be done or is done in other languages and how Dart handles it differently.
As I've said before, I'm a big fan of Dart. After trying quite a lot of languages over the years, I have to say that it's a pleasure to write Dart code. It's well thought out, designed and implemented. It works like a charm with Flutter and makes me want to go to work each day, and learn more and more about it.
That's it for me this time, I hope you've learned a thing or two, or at least see the value of certain features.
Thanks for reading, and hope you have a great weekend! 鉂わ笍
Posted on June 3, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.