Dart is an object-oriented, class-based language used with Flutter. Dart's recent update introduced new features like enums, mixins, extensions, and more. Today, we explore the most important Dart 2 updates with code examples.
Dart is an object-oriented, class-based, simple, and clean language. Google’s popular mobile framework, Flutter, uses Dart to implement high-quality native applications. In 2018, Dart 2 was released with all sorts of new features that make it much safer, more expressive, and more usable.
This update modified the basics of the Dart language, libraries, build system, and web development tools.
Flutter and Dart are a powerful combination, so it’s important to understand all that Dart 2 can do and get the full potential of the language. Today, we’ll introduce you to the most important new language features of Dart 2 to take your web applications to the next level.
To be most successful with this article, a basic knowledge of Flutter and Dart is recommended. Check out our article Flutter Tutorial: the beginner's guide to cross-platform apps before continuing here.
Today, we will go over:
Dart Extensions
Dart extensions were officially released with Dart 2.7. The extensions feature adds functionality to existing Dart libraries. The extensions feature is useful when you want to add utility method(s) in a third-party library or a core class.
With the first version of Dart, it was not always practical to add methods in those classes. Extensions help by implicitly extending the type as opposed to the old style where the object was explicitly passed as an argument to static methods.
Extensions are defined inside one block of code that begins with extension
and contains a name for the extension, the on
keyword, and the data type. Dart supports three types of extensions that we will go over in more detail below:
extension<T> on List<T> {
//extension methods
//extension operators
//extension properties
}
Extension methods
Extension methods allow you to add new members to existing types. You might already be using extension methods. For example, when using code completion in an IDE, it will suggest an extension method. Extension methods are located in libraries.
You use them by importing the right library and using them like an ordinary method.
Let’s see an example to understand extension methods. Below, we will write an extension method for the List
data type. The extension method priceList()
returns the price listing in the same condition. This method demonstrates how extensions implicitly extend the type using this
.
//Local extension method
extension<T> on List<T> {
//Extension Method demonstration
List<T> priceList() => this.map((item) => item).toList();
}
void main() {
//List of prices
List prices = [1, 1.99, 4];
print("Price listing:");
//priceList() is being called on `prices` list
//and returns the list of prices
print(prices.priceList());
}
Output is "Price listing: [1, 1.99, 4]"
Extension operators
Dart provides support for operators as well. Below, we will define an operator extension for the ^
operator. We assume this operator increases the price by the n
times, where n
is the passed argument.
The operator
keyword declares an extension operator. It is followed by the operator sign. In the following example, the this
list is iterated over to multiply each item by n
, and then the updated list is returned.
extension<T> on List<T> {
//Extension Operator: Hike up the price by n
List<num> operator ^(int n) =>
this.map((item) => num.parse("${item}") * n).toList();
}
The operator ^
is applied to the prices
list. The operator ^
multiplies each item in prices
by 3 and returns. The updated list is then printed using the print()
method.
void main() {
//List of prices
List prices = [1, 1.99, 4];
print("\nPrice listing after hiking up prices 3x of the original value");
//argument is passed after the operator sign
print(prices ^ 3);
}
This gives us the output below:
Price listing after hiking up prices 3x of the original value
[3, 5.97, 12]
Extension property
Dart also provides support for Properties. In the extension below, we add a property that returns the total number of printed price labels needed for a price listing. Let’s assume we want to print 3 labels for each price amount in the list [1, 1.99, 4]
.
An extension property definition has three parts: the type of data to be returned by the property, the get
keyword, and the name of the property. For example:
<return_type> get <property_name> => //implementation
The number of labels is of type int
, and the property name is labelCount
. We need 3 labels for each item, so we need three times the total size of the list. We can calculate the number of total labels as length * 3
. The extension has implicit access to the length
property.
extension<T> on List<T> {
//Extension Property: 3 printed labels for each price.
int get labelCount => length * 3;
}
The property labelCount
is called on the price listing prices
.
void main() {
//List of prices
List prices = [1, 1.99, 4];
print("\nNumber of Printed Labels:");
print(prices.labelCount);
}
This gives us the following output:
Number of Printed Labels:
9
Dart Enums
Enumerated Types (a.k.a. Enums) were added with the release of Dart 1.8. Enums act like a class that represent a fixed number of constant values. For example, you could have an app that fetches data from a remote server. The app shows one of the following statuses:
- done: The app received the response successfully.
- waiting: The app is waiting for the response.
- error: The app received an error from the server.
The above responses can be declared using the enum
keyword:
enum Status {
done,
waiting,
error, //This comma is optional
}
Let’s understand this better with an example and pretend we have a weather application. We need to represent states of the weather: sunny, cloudy, and rainy.
We could represent these states with a const
keyword.
const SUNNY = 'Sunny';
const CLOUDY = 'Cloudy';
const RAINY = 'Rainy';
Or, we could use enumerated types with the enum
keyword.
enum Weather {
sunny,
cloudy,
rainy,
}
Switch block with Enums
We can use the switch
block for enums, and it requires case-blocks for all members of our enum
class and a default
block if a case-block implementation is missing, otherwise, you get a compilation error. Take a look at the example below.
Note:
switch
block implementations can be done for constants and enums. However, enums are preferred when you do not want to lose an opportunity to handle a particular case.
//Using Enums to display weather information
enum Weather {
sunny,
cloudy,
rainy,
}
void main() {
var weather = Weather.sunny;
//Following code will complain about if all members are not present
//Use default-block when all case-blocks are not available
switch (weather) {
case Weather.sunny:
print("Sunny weather today!");
break;
case Weather.cloudy:
print("Cloudy today!");
break;
case Weather.rainy:
print("Rainy and gloomy weather.");
break;
default:
print("Current weather:${weather}");
}
}
Keep the learning going.
Learn how to develop web applications using Dart without scrubbing through videos or documentation. Educative's text-based courses are easy to skim and feature live coding environments, making learning quick and efficient. By the end of this course, you’ll be able to use this language in your own Flutter projects.
Developing Web Applications with Dart
Dart Mixins
Mixins allow our Dart code to be reusable across separate classes. It is far more efficient to reuse code from classes that share common behaviors. A mixin class contains methods used by other classes, but it is not their parent. Therefore, we can use the code from a class without inheriting from it, unlike interfaces and abstract classes.
Mixins are declared using the mixin
keyword:
mixin SharedBehavior {
}
The syntax of mixins is simple. Below, B
is the parent class to A
.C
is the mixin with methods that B
can implement.
class A extends B with C {
//Implement methods from B & C
}
Mixins example
Let’s understand this better with an example. Say we have different people with different occupations: artist, engineer, doctor, and athlete. We can assume that the four types of people share a combination of common behaviors (like sketching, reading, exercise, boxing, etc.) in addition to inheriting the class Person
.
In other words, each type of person is extending the
Person
class and one or more shared behaviors.
Overlapping common behaviors like these can be extracted into mixins. We will create mixins for these behaviors. For example, the Sketching
mixin defines the common sketch()
method. This method takes a message
parameter.
mixin Sketching {
sketch(String message) {
print(message);
}
}
Let's see our example in code. Below, in the main()
method, each class’ object is created and method(s) are called for their shared behavior which is implemented using Mixins.
//Person class
abstract class Person {
int age;
int name;
eat() {}
sleep() {}
}
//Artist class
class Artist extends Person with Sketching {
sketchLandscape() {
sketch("Making landscapes sketches");
}
}
//Engineer class
class Engineer extends Person with Sketching, Reading {
sketchBuildings() {
sketch("Sketching engineering drawings");
}
readResearchPaper() {
String topic = "Building Construction";
dailyReading(topic);
}
}
//Doctor class
class Doctor extends Person with Reading, Exercise {
readReports() {
String topic = "flu";
dailyReading(topic);
}
workout() {
running(1);
weightTraining(10);
}
}
//Athlete class
class Athlete extends Person with Exercise {
generalRoutine() {
running(2);
weightTraining(20);
}
}
//Boxer class
class Boxer extends Athlete with Boxing {
punchPractice() {
punch(100);
}
routineExercise() {
running(4);
weightTraining(40);
}
}
//Mixins
//Sketching mixin
mixin Sketching {
sketch(String message) {
print(message);
}
}
//Reading mixin
mixin Reading {
dailyReading(String topic) {
print("Daily reading on ${topic}");
}
}
//Exercise mixin
mixin Exercise {
running(int mile) {
print("Daily run of ${mile} mile(s)");
}
weightTraining(int weights) {
print("Lifting ${weights} lbs");
}
}
//Boxing
mixin Boxing on Athlete {
punch(int n) {
print("Boxer practicing ${n} punches");
}
}
void main() {
print("Artist");
Artist artist = Artist();
artist.sketchLandscape();
print("\nEngineer");
Engineer engineer = Engineer();
engineer.sketchBuildings();
engineer.readResearchPaper();
print("\nDoctor");
Doctor doctor = Doctor();
doctor.readReports();
doctor.workout();
print("\nBoxer");
Boxer boxer = Boxer();
boxer.punchPractice();
boxer.routineExercise();
}
Dart Generics
Generics are used to apply stronger type checks at compile time. They also enforce type-safety. Generics help write reusable classes and methods/functions for different data types. Generics in Dart are similar to Java generics or C++ templates.
Definition: Type Safety is a programming concept that allows a memory block to contain only one type of data.
Dart’s collection can hold different data types in one collection, but this can lead to crashing the program if a particular data type is not handled appropriately. Generics can solve this problem by enforcing one data type to the collection.
Let’s learn how to declare type-safe collections. To ensure type safety, the angular brackets <>
with data type enclosed, declare the collection of the given data type.
CollectionType <dataType> identifier = CollectionType <dataType>();
Generics are parameterized, so they use type variable notations to restrict the type of data. We commonly represent these type variables with single letter names, such as:
-
E: represents the element type in a collection, i.e.
List
- R: represents the return type of a function or method
-
K: represents the key type in associative collections, i.e.
Map
-
V: represents the value type in associative collections, i.e.
Map
Note: We can also represent generics with descriptive names like
Product
orInventory
Take a look at an example of the single-letter name generics implementation.
import 'dart:collection';
//Demonstrating use of single letter for generics
//A class for grocery product
class Product {
final int id;
final double price;
final String title;
Product(this.id, this.price, this.title);
@override
String toString() {
return "Price of ${this.title} is \$${this.price}";
}
}
//A class for product's inventory
class Inventory {
final int amount;
Inventory(this.amount);
@override
String toString() {
return "Inventory amount: $amount";
}
}
//Custom type variables- Single letter
class Store<P, I> {
final HashMap<P, I> catalog = HashMap<P, I>();
List<P> get products => catalog.keys.toList();
void updateInventory(P product, I inventory) {
catalog[product] = inventory;
}
void printProducts() {
catalog.keys.forEach(
(product) => print("Product: $product, " + catalog[product].toString()),
);
}
}
//Demonstrating single letter
void main() {
Product milk = Product(1, 5.99, "Milk");
Product bread = Product(2, 4.50, "Bread");
//Using single letter names for Generics
Store<Product, Inventory> store1 = Store<Product, Inventory>();
store1.updateInventory(milk, Inventory(20));
store1.updateInventory(bread, Inventory(15));
store1.printProducts();
}
Output:
Product: Price of Bread is $4.5, Inventory amount: 15
Product: Price of Milk is $5.99, Inventory amount: 20
Asynchrony in Dart
Asynchronicity allows multiple things to happen at the same time. In Dart, asynchronous operations can perform time-consuming operations and allow for their processing to finish at a later time.
Dart provides two ways to handle events/requests asynchronously. Dart’s dart:async
library provides support for asynchronous programming with Future
and Stream
classes.
- Future objects: represent the results of asynchronous operations. This is like a promise for a result to arrive in the future.
- Stream objects: provides a sequence of events that are either a value or an error.
Future
objects
Let’s explore future objects. Asynchronous operations results are returned as Future
objects. The future object is represented as Future<T>
, where T
is the type of results returned. When we need the result of a completed Future, we can use either:
-
await
andasync
- The
Future
API
We use the await
and async
keywords together. The function that is expected to perform the expensive work will be marked with the keyword async
. The expensive call is prefixed by the keyword await
inside the function.
The program will suspend when await
is called, when a function returns, or when it reaches the end of the function. Let’s see an example of async
and await
below:
// Expensive function could be a function that takes
// long time to process data and return results.
// Assume this function takes long time to return in real-world
String getExpansiveData() {
return "I'm expansive data";
}
// This is the asynchronous function that makes the expensive
// data call and prints the results.
Future<void> makeDataCall() async {
var data = await getExpansiveData();
print(data);
}
//----END----//
//Entry point function
void main() {
makeDataCall();
}
Output: I'm expansive data
Above, the Future
keyword before the function makeDataCall()
means that this function will be executed asynchronously. Therefore, it will be suspended when it encounters await
.
The makeDataCall()
returns a Future
of type void
since there is nothing returned by the function. It calls the getExpansiveData()
method with the keyword await
, which returns the string I'm expansive data
. The method makeDataCall()
prints the results with the functionprint()
.
The
Future
API can also be used to execute asynchronous operations. In theFuture
API, thethen()
method registers a callback, which fires upon the completion ofFuture
.There are two variants of the Future API.
Future<String>
: Future returningString
data type .Future<void>
: Future returningvoid
.
What to learn next
Congrats! You should now have a good understanding of what Dart 2 brings to the table. With Dart in our Flutter projects, we can make high-quality native applications, and these new features take your Dart skills to the next level.
But there is still more to learn about Dart 2 to truly use this language to its full potential. Your next learning steps are the following concepts:
- Callable Classes
- Generator Functions
- Dart Libraries
- Generic Collections
- API and Streams in Dart
- OS Variables/
Platform
Class - Encryption in Dart (
encrypt
package)
To get started with these intermediate Dart 2 concepts, check out Educative’s course Developing Web Applications with Dart. You will dive deep into Dart 2 language features with hands-on code. Gain confidence using everything from extensions to callable classes and beyond. By the end, you’ll be able use this language in your own Flutter projects. You can even earn a certificate and add Dart skills to your resume!
Happy learning!
Top comments (1)
To be blunt, your examples do not show the actual power of Mixins. What you show could be better implemented by adding behaviours to the Person class and its various descendants. Perhaps showing that dogs and persons can share some common behaviours, but cannot descend from the same base class
As for Generics, do you think that using single-letter variable names, specified by convention, is a good idea? This is not Rails. We strive towards giving things good names because it helps. Let's not regress when specifying generics
Your Future examples have almost exactly the same behaviour as a regular call: wait until the call returns
Cheers