Dart vs Swift: a comparison

Posted by Andrea Bizzotto on December 27, 2018

Dart and Swift are my two favourite programming languages. I have used them extensively in commercial and open-source code.

This article offers a side by side comparison between Dart and Swift, and aims to:

  • Highlight the differences between the two.
  • Be a reference for developers moving from one language to the other (or using both).

Some context:

  • Dart powers Flutter, Google’s framework for building beautiful native apps from a single codebase.
  • Swift powers Apple’s SDKs across iOS, macOS, tvOS and watchOS.

The following comparison is made across the main features of both languages (as of Dart 2.1 and Swift 4.2). As discussing each feature in-depth is beyond the scope of this article, I include references for further reading where appropriate.

Table of Contents

Comparison table

Feature Dart Swift
Type inference
Named parameters
Un-named parameters
Closures
Optionals ❌ (planned)
Tuples ❌ (3rd party lib)
Single inheritance
Multiple conformance
Mixins
Extensions
Enums with associated types ❌ (3rd party lib)
Structs
Error handling
Generics
Futures ❌ (async/await planned)
Streams ✅ (+ RxDart) ❌ (+ RxSwift)
Memory management GC ARC
Concurrency Isolates dispatch queues
Compiler JIT, AOT AOT

Variables

Variable declaration syntax looks like this in Dart:

String name;
int age;
double height;

And like this in Swift:

var name: String
var age: Int
var height: Double

Variable initialization looks like this in Dart:

var name = 'Andrea';
var age = 34;
var height = 1.84;

And like this in Swift:

var name = "Andrea"
var age = 34
var height = 1.84

In this example type annotations are not needed. This is because both languages can infer the types from the expression on the right side of the assignment.

Speaking of which…

Type inference

Type inference means that we can write the following in Dart:

var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object>

And the type of arguments is automatically resolved by the compiler.

In Swift, the same can be written as:

var arguments = [ "argA": "hello", "argB": 42 ] // [ String : Any ]

Some more details

Quoting the documentation for Dart:

The analyzer can infer types for fields, methods, local variables, and most generic type arguments. When the analyzer doesn’t have enough information to infer a specific type, it uses the dynamic type.

And for Swift:

Swift uses type inference extensively, allowing you to omit the type or part of the type of many variables and expressions in your code. For example, instead of writing var x: Int = 0, you can write var x = 0, omitting the type completely—the compiler correctly infers that x names a value of type Int.

Dynamic types

A variable that can be of any type is declared with the dynamic keyword in Dart, and the Any keyword in Swift.

Dynamic types are commonly used when reading data such as JSON.

Mutable / immutable variables

Variables can be declared to be mutable or immutable.

To declare mutable variables, both languages use the var keyword.

var a = 10; // int (Dart)
a = 20; // ok
var a = 10 // Int (Swift)
a = 20 // ok

To declare immutable variables, Dart uses final, and Swift uses let.

final a = 10;
a = 20; // 'a': a final variable, can only be set once.
let a = 10
a = 20 // Cannot assign to value: 'a' is a 'let' constant

Note: The Dart documentation defines two keywords, final and const, which work as follows:

If you never intend to change a variable, use final or const, either instead of var or in addition to a type. A final variable can be set only once; a const variable is a compile-time constant. (Const variables are implicitly final.) A final top-level or class variable is initialized the first time it’s used.

Further explanations are found on this post on the Dart website:

final means single-assignment. A final variable or field must have an initializer. Once assigned a value, a final variable’s value cannot be changed. final modifies variables.

TL;DR: use final to define immutable variables in Dart.


In Swift, we declare constants with let. Quoting:

A constant declaration introduces a constant named value into your program. Constant declarations are declared using the let keyword and have the following form:

let constant name: type = expression

A constant declaration defines an immutable binding between the constant name and the value of the initializer expression; after the value of a constant is set, it cannot be changed.

Read more: Swift Declarations.

Functions

Functions are first-class citizens in Swift and Dart.

This means that just like objects, functions can be passed as arguments, saved as properties, or returned as a result.

As an initial comparison, we can see how to declare functions that take no arguments.

In Dart, the return type precedes the method name:

void foo();
int bar();

In Swift, we use the -> T notation as a suffix. This is not required if there is no return value (Void):

func foo()
func bar() -> Int

Read more:

Named and un-named parameters

Both languages support named and un-named parameters.

In Swift, parameters are named by default:

func foo(name: String, age: Int, height: Double)
foo(name: "Andrea", age: 34, height: 1.84)

In Dart, we define named parameters with curly braces ({}):

void foo({String name, int age, double height});
foo(name: 'Andrea', age: 34, height: 1.84);

In Swift, we define un-named parameters by using an underscore (_) as an external parameter:

func foo(_ name: String, _ age: Int, _ height: Double)
foo("Andrea", 34, 1.84)

In Dart, we define un-named parameters by omitting the curly braces ({}):

void foo(String name, int age, double height);
foo('Andrea', 34, 1.84);

Read more: Function Argument Labels and Parameter Names in Swift.

Optional and default parameters

Both languages support default parameters.

In Swift, you can define a default value for any parameter in a function by assigning a value to the parameter after that parameter’s type. If a default value is defined, you can omit that parameter when calling the function.

func foo(name: String, age: Int = 0, height: Double = 0.0) 
foo(name: "Andrea", age: 34) // name: "Andrea", age: 34, height: 0.0

Read more: Default Parameter Values in Swift.

In Dart, optional parameters can be either positional or named, but not both.

// positional optional parameters
void foo(String name, [int age = 0, double height = 0.0]);
foo('Andrea', 34); // name: 'Andrea', age: 34, height: 0.0
// named optional parameters
void foo({String name, int age = 0, double height = 0.0});
foo(name: 'Andrea', age: 34); // name: 'Andrea', age: 34, height: 0.0

Read more: Optional parameters in Dart.

Closures

Being first-class objects, functions can be passed as arguments to other functions, or assigned to variables.

In this context, functions are also known as closures.

Here is a Dart example of a function that iterates over a list of items, using a closure to print the index and contents of each item:

final list = ['apples', 'bananas', 'oranges'];
list.forEach((item) => print('${list.indexOf(item)}: $item'));

The closure takes one argument (item), prints the index and value of that item, and returns no value.

Note the use of the arrow notation (=>). This can used in place of a single return statement inside curly braces:

list.forEach((item) { print('${list.indexOf(item)}: $item'); });

The same code in Swift looks like this:

let list = ["apples", "bananas", "oranges"]
list.forEach({print("\(String(describing: list.firstIndex(of: $0))) \($0)")})

In this case, we don’t specify a name for the argument passed to the closure, and use $0 instead to mean the first argument. This is entirely optional and we can use a named parameter if we prefer:

list.forEach({ item in print("\(String(describing: list.firstIndex(of: item))) \(item)")})

Closures are often used as completion blocks for asynchronous code in Swift (see section below about asynchronous programming).

Read more:

Tuples

From the Swift Docs:

Tuples group multiple values into a single compound value. The values within a tuple can be of any type and don’t have to be of the same type as each other.

These can be used as small light-weight types, and are useful when defining functions with multiple return values.

Here is how to use tuples in Swift:

let t = ("Andrea", 34, 1.84)
print(t.0) // prints "Andrea"
print(t.1) // prints 34
print(t.2) // prints 1.84

Tuples are supported with a separate package in Dart:

const t = const Tuple3<String, int, double>('Andrea', 34, 1.84);
print(t.item1); // prints 'Andrea'
print(t.item2); // prints 34
print(t.item3); // prints 1.84

Control flow

Both languages provide a variety of control flow statements.

Examples of this are if conditionals, for and while loops, switch statements.

Covering these here would be quite lengthy, so I refer to the official docs:

Collections (arrays, sets, maps)

Arrays / Lists

Arrays are ordered groups of objects.

Arrays can be created as Lists in Dart:

var emptyList = <int>[]; // empty list
var list = [1, 2, 3]; // list literal
list.length; // 3
list[1]; // 2

Arrays have a built-in type in Swift:

var emptyArray = [Int]() // empty array
var array = [1, 2, 3] // array literal
array.count // 3
array[1] // 2

Sets

Quoting the Swift docs:

A set stores distinct values of the same type in a collection with no defined ordering. You can use a set instead of an array when the order of items is not important, or when you need to ensure that an item only appears once.

This is defined with the Set class in Dart.

var emptyFruits = <String>{}; // empty set literal
var fruits = {'apple', 'banana'}; // set literal

Likewise, in Swift:

var emptyFruits = Set<String>()
var fruits = Set<String>(["apple", "banana"])

Maps / Dictionaries

The Swift docs have a good definition for a map/dictionary:

A dictionary stores associations between keys of the same type and values of the same type in a collection with no defined ordering. Each value is associated with a unique key, which acts as an identifier for that value within the dictionary.

Maps are defined like so in Dart:

var namesOfIntegers = Map<Int,String>(); // empty map
var airports = { 'YYZ': 'Toronto Pearson', 'DUB': 'Dublin' }; // map literal

Maps are called dictionaries in Swift:

var namesOfIntegers = [Int: String]() // empty dictionary
var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"] // dictionary literal

Read More:

Nullability & Optionals

In Dart, any object can be null. And trying to access methods or variables of null objects results in a null pointer exception. This is one the most common source of errors (if not the most common) in computer programs.

Since the beginning, Swift had optionals, a built-in language feature for declaring if objects can or cannot have a value. Quoting the docs:

You use optionals in situations where a value may be absent. An optional represents two possibilities: Either there is a value, and you can unwrap the optional to access that value, or there isn’t a value at all.

In contrast to this, we can use non-optional variables to guarantee that they will always have a value:

var x: Int? // optional
var y: Int = 1 // non-optional, must be initialized

Note: saying that a Swift variable is optional is roughly the same as saying that a Dart variable can be null.

Without language-level support for optionals, we can only check at runtime if a variable is null.

With optionals, we encode this information at compile-time instead. We can unwrap optionals to safely check if they hold a value:

func showOptional(x: Int?) {
  // use `guard let` rather than `if let` as best practice
  if let x = x { // unwrap optional
    print(x)
  } else {
    print("no value")
  }
}
showOptional(x: nil) // prints "no value"
showOptional(x: 5) // prints "5"

And if we know that a variable must have a value, we can use a non-optional:

func showNonOptional(x: Int) {
  print(x)
}
showNonOptional(x: nil) // [compile error] Nil is not compatible with expected argument type 'Int'
showNonOptional(x: 5) // prints "5"

The first example above can be implemented like this in Dart:

void showOptional(int x) {
  if (x != null) {
    print(x);
  } else {
    print('no value');
  }
}
showOptional(null) // prints "no value"
showOptional(5) // prints "5"

And the second one like this:

void showNonOptional(int x) {
  assert(x != null);
  print(x); 	
}
showNonOptional(null) // [runtime error] Uncaught exception: Assertion failed
showNonOptional(5) // prints "5"

Having optionals means that we can catch errors at compile-time rather than at runtime. And catching errors early results in safer code with fewer bugs.

Dart’s lack of support for optionals is somehow mitigated by the use of assertions (and the @required annotation for named parameters).

These are used extensively in the Flutter SDK, but result in additional boilerplate code.

For the record, there is a proposal about adding non-nullable types to Dart.

There is a lot more about optionals than I have covered here. For a good overview, see: Optionals in Swift.

Classes

Classes are the main building block for writing programs in object-oriented languages.

Classes are supported by Dart and Swift, with some differences.

Syntax

Here is class with an initializer and three member variables in Swift:

class Person {
  let name: String
  let age: Int
  let height: Double
  init(name: String, age: Int, height: Double) {
    self.name = name
    self.age = age
    self.height = height
  }
}

And the same in Dart:

class Person {
  Person({this.name, this.age, this.height});
  final String name;
  final int age;
  final double height;
}

Note the usage of this.[propertyName] in the Dart constructor. This is syntactic sugar for setting the instance member variables before the constructor runs.

Factory constructors

In Dart, it is possible to create factory constructors. Quoting:

Use the factory keyword when implementing a constructor that doesn’t always create a new instance of its class.

One practical use case of factory constructors is when creating a model class from JSON:

class Person {
  Person({this.name, this.age, this.height});
  final String name;
  final int age;
  final double height;
  factory Person.fromJSON(Map<dynamic, dynamic> json) {
    String name = json['name'];
    int age = json['age'];
    double height = json['height'];
    return Person(name: name, age: age, height: height);
  }
}
var p = Person.fromJSON({
  'name': 'Andrea',
  'age': 34,
  'height': 1.84,
});

Read more:

Inheritance

Swift uses a single-inheritance model, meaning that any class can only have one superclass. Swift classes can implement multiple interfaces (also known as protocols).

Dart classes have mixin-based inheritance. Quoting the docs:

Every object is an instance of a class, and all classes descend from Object. Mixin-based inheritance means that although every class (except for Object) has exactly one superclass, a class body can be reused in multiple class hierarchies.

Here is single-inheritance in action in Swift:

class Vehicle {
  let wheelCount: Int
  init(wheelCount: Int) {
    self.wheelCount = wheelCount
  }
}

class Bicycle: Vehicle {
  init() {
    super.init(wheelCount: 2)
  }
}

And in Dart:

class Vehicle {
  Vehicle({this.wheelCount});
  final int wheelCount;
}

class Bicycle extends Vehicle {
  Bicycle() : super(wheelCount: 2);
}

Properties

These are called instance variables in Dart, and simply properties in Swift.

In Swift, there is a distinction between stored and computed properties:

class Circle {
  init(radius: Double) {
    self.radius = radius
  }
  let radius: Double // stored property
  var diameter: Double { // read-only computed property
    return radius * 2.0
  }
}

In Dart, we have the same distinction:

class Circle {
  Circle({this.radius});
  final double radius; // stored property
  double get diameter => radius * 2.0; // computed property
}

In addition to getters for computed properties, we can also define setters.

Using the example above, we can rewrite the diameter property to include a setter:

var diameter: Double { // computed property
  get {
    return radius * 2.0
  }
  set {
    radius = newValue / 2.0
  }
}

In Dart, we can add a separate setter like so:

set diameter(double value) => radius = value / 2.0;

Property observers

This is a peculiar feature of Swift. Quoting:

Property observers observe and respond to changes in a property’s value. Property observers are called every time a property’s value is set, even if the new value is the same as the property’s current value.

This is how they can be used:

var diameter: Double { // read-only computed property
  willSet(newDiameter) {
    print("old value: \(diameter), new value: \(newDiameter)")  
  }
  didSet {
    print("old value: \(oldValue), new value: \(diameter)")  
  }
}

Read more:

Protocols / Abstract classes

Here we talk about a construct used to define methods and properties, without specifying how they are implemented. This is known as an interface in other languages.

In Swift, interfaces are called protocols.

protocol Shape {
  func area() -> Double
}

class Square: Shape {
  let side: Double
  init(side: Double) {
    self.side = side
  }
  func area() -> Double {
    return side * side
  }
}

Dart has a similar construct known as an abstract class. Abstract classes can’t be instantiated. They can however define methods which have an implementation.

The example above can be written like this in Dart:

abstract class Shape {
  double area();
}

class Square extends Shape {
  Square({this.side});
  final double side;
  double area() => side * side;
}

Read more:

Mixins

In Dart, a mixin is just a regular class, which can be reused in multiple class hierarchies.

Here is how we could extend the Person class we defined before with a NameExtension mixin:

abstract class NameExtension {
  String get name;
  String get uppercaseName => name.toUpperCase();
  String get lowercaseName => name.toLowerCase();
}

class Person with NameExtension {
  Person({this.name, this.age, this.height});
  final String name;
  final int age;
  final double height;	
}
var person = Person(name: 'Andrea', age: 34, height: 1.84);
print(person.uppercaseName); // 'ANDREA'

Read more: Dart Mixins

Extensions

Extensions are a feature of the Swift language. Quoting the docs:

Extensions add new functionality to an existing class, structure, enumeration, or protocol type. This includes the ability to extend types for which you do not have access to the original source code (known as retroactive modeling).

This is not possible with mixins in Dart.

Borrowing the example above, we can extend the Person class like so:

extension Person {
  var uppercaseName: String {
    return name.uppercased()
  }
  var lowercaseName: String {
    return name.lowercased()
  }
}
var person = Person(name: "Andrea", age: 34, height: 1.84)
print(person.uppercaseName) // "ANDREA"

There is a lot more to extensions than I have presented here, especially when they are used in conjunction with protocols and generics.

One very common use case for extensions is adding protocol conformance to existing types. For example, we can use an extension to add serialization capabilities to an existing model class.

Read more: Swift Extensions

Enums

Dart has some very basic support for enums.

Enums in Swift are very powerful, because they support associated types:

enum NetworkResponse {
  case success(body: Data) 
  case failure(error: Error)
}

This makes it possible to write logic like this:

switch (response) {
  case .success(let data):
    // do something with (non-optional) data
  case .failure(let error):
    // do something with (non-optional) error
}

Note how the data and error parameters are mutually exclusive.

In Dart we can’t associate additional values to enums, and the code above may be implemented along these lines:

class NetworkResponse {
  NetworkResponse({this.data, this.error})
  // assertion to make data and error mutually exclusive
  : assert(data != null && error == null || data == null && error != null);
  final Uint8List data;
  final String error;
}
var response = NetworkResponse(data: Uint8List(0), error: null);
if (response.data != null) {
  // use data
} else {
  // use error
}

A couple of notes:

  • Here we use of assertions to compensate for the fact that we don’t have optionals.
  • The compiler can’t help us to check for all possible cases. This is because we don’t use a switch to process the response.

In summary, Swift enums are a lot more powerful and expressive than in Dart.

3rd party libraries such as Dart Sealed Unions provide similar functionality to what is offered by Swift enums, and can help to fill the gap.

Read more: Swift Enums.

Structs

In Swift we can define structures and classes.

Both constructs have many things in common, and some differences.

The main difference is that:

Classes are reference types, and structures are value types

Quoting the documentation:

A value type is a type whose value is copied when it’s assigned to a variable or constant, or when it’s passed to a function.

All structures and enumerations are value types in Swift. This means that any structure and enumeration instances you create—and any value types they have as properties—are always copied when they are passed around in your code.

Unlike value types, reference types are not copied when they are assigned to a variable or constant, or when they are passed to a function. Rather than a copy, a reference to the same existing instance is used.

To see what this means, consider the following example, where we re-purpose the Person class to make it mutable:

class Person {
  var name: String
  var age: Int
  var height: Double
  init(name: String, age: Int, height: Double) {
    self.name = name
    self.age = age
    self.height = height
  }
}

var a = Person(name: "Andrea", age: 34, height: 1.84)
var b = a
b.age = 35
print(a.age) // prints 35

If we re-define Person to be a struct, we have this:

struct Person {
  var name: String
  var age: Int
  var height: Double
  init(name: String, age: Int, height: Double) {
    self.name = name
    self.age = age
    self.height = height
  }
}

var a = Person(name: "Andrea", age: 34, height: 1.84)
var b = a
b.age = 35
print(a.age) // prints 34

There is a lot more to structs than I have covered here.

Structs can be used to great effect to handle data and models in Swift, leading to robust code with fewer bugs.

For a better overview, read: Structures and Classes in Swift.

Error handling

Using a definition from the Swift docs:

Error handling is the process of responding to and recovering from error conditions in your program.

Both Dart and Swift use try/catch as a technique for handling errors, with some differences.

In Dart, any method can throw an exception of any type.

class BankAccount {
  BankAccount({this.balance});
  double balance;
  void withdraw(double amount) {
    if (amount > balance) {
      throw Exception('Insufficient funds');
    }
    balance -= amount;
  }
}

Exceptions can be caught with a try/catch block:

var account = BankAccount(balance: 100);
try {
  account.withdraw(50); // ok
  account.withdraw(200); // throws
} catch (e) {
  print(e); // prints 'Exception: Insufficient funds'
}

In Swift, we explicitly declare when a method can throw an exception. This is done with the throws keyword, and any errors must conform to the Error protocol:

enum AccountError: Error {
  case insufficientFunds
}

class BankAccount {
  var balance: Double
  init(balance: Double) {
    self.balance = balance
  }
  func withdraw(amount: Double) throws {
    if amount > balance {
      throw AccountError.insufficientFunds
    }
    balance -= amount
  }
}

When handling errors, we use a try keyword inside a do/catch block.

var account = BankAccount(balance: 100)
do {
  try account.withdraw(amount: 50) // ok
  try account.withdraw(amount: 200) // throws
} catch AccountError.insufficientFunds {
  print("Insufficient Funds")
}

Note how the try keyword is mandatory when calling methods that can throw.

And error themselves are strongly typed, so we can have multiple catch blocks to cover all possible cases.

try, try?, try!

Swift offers less verbose ways of dealing with errors.

We can use try? without a do/catch block. And this will cause any exceptions to be ignored:

var account = BankAccount(balance: 100)
try? account.withdraw(amount: 50) // ok
try? account.withdraw(amount: 200) // fails silently

Or if we are certain that a method won’t throw, we can use try!:

var account = BankAccount(balance: 100)
try! account.withdraw(amount: 50) // ok
try! account.withdraw(amount: 200) // crash

The example above will cause the program to crash. Hence, try! is not recommended in production code, and it is better suited when writing tests.


Overall, the explicit nature of error handling in Swift is very beneficial in API design, because it makes it easy to know if a method can or cannot throw.

Likewise, the usage of try in method calls draws the attention to code that can throw, forcing us to consider error cases.

In this respect, error handling feels safer and more robust than in Dart.

Read more:

Generics

Quoting the Swift docs:

Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.

Generics are supported by both languages.

One of the most common use cases for generics is collections, such as arrays, sets and maps.

And we can use them to define our own types. Here is how we would define a generic Stack type in Swift:

struct Stack<Element> {
  var items = [Element]()
  mutating func push(_ item: Element) {
    items.append(item)
  }
  mutating func pop() -> Element {
    return items.removeLast()
  }
}

Similarly, in Dart we would write:

class Stack<Element> {
  var items = <Element>[]
  void push(Element item) {
    items.add(item)
  }
  void pop() -> Element {
    return items.removeLast()
  }
}

Generics are very useful and powerful in Swift, where they can be used to define type constraints and associated types in protocols.

I recommend reading the documentation for more information:

Access control

Quoting the Swift documentation:

Access control restricts access to parts of your code from code in other source files and modules. This feature enables you to hide the implementation details of your code, and to specify a preferred interface through which that code can be accessed and used.

Swift has five access levels: open, public, internal, file-private, and private.

These are used in the context of working with modules and source files. Quoting:

A module is a single unit of code distribution—a framework or application that is built and shipped as a single unit and that can be imported by another module with Swift’s import keyword.

The open and public access levels can be used to make code accessible outside modules.

The private and file-private access level can be used to make code not accessible outside the file it is defined in.

Examples:

public class SomePublicClass {}
internal class SomeInternalClass {}
fileprivate class SomeFilePrivateClass {}
private class SomePrivateClass {}

Access levels are simpler in Dart, and limited to public and private. Quoting:

Unlike Java, Dart doesn’t have the keywords public, protected, and private. If an identifier starts with an underscore _, it’s private to its library.

Examples:

class HomePage extends StatefulWidget { // public
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> { ... } // private

Access control was designed with different goals for Dart and Swift. And as a result the access levels are very different.

Read more:

Asynchronous Programming: Futures

Asynchronous programming is an area where Dart really shines.

Some form of asynchronous programming is needed when dealing with use cases such as:

  • Downloading content from the web
  • Talking to a backend service
  • Perform a long running operations

In these cases it is best to not block the main thread of execution, which can make our programs freeze.

Quoting the Dart documentation:

Asynchronous operations let your program complete other work while waiting for an operation to finish. Dart uses Future objects (futures) to represent the results of asynchronous operations. To work with futures, you can use either async and await or the Future API.

As an example, let’s see of how we may use asynchronous programming to:

  • authenticate a user with a server
  • store the access token to secure storage
  • get the user profile information

In Dart this can be done with async/await in combination with Futures:

Future<UserProfile> getUserProfile(UserCredentials credentials) async {
  final accessToken = await networkService.signIn(credentials);
  await secureStorage.storeToken(accessToken, forUserCredentials: credentials);
  return await networkService.getProfile(accessToken);
}

In Swift, there is no support for async/await and we can only accomplish this with closures (completion blocks):

func getUserProfile(credentials: UserCredentials, completion: (_ result: UserProfile) -> Void) {
  networkService.signIn(credentials) { accessToken in
    secureStorage.storeToken(accessToken) {
      networkService.getProfile(accessToken, completion: completion)
    }
  }
}

This leads to a “pyramid of doom” due to nested completion blocks. And error handling becomes very difficult in this scenario.

In Dart, handling errors in the code above is simply done by adding a try/catch block around the code to the getUserProfile method.

For reference, there is a proposal to add async/await to Swift in the future. This is documented in detail here:

Until this is implemented, developers can use 3rd party libraries such as this Promises library by Google.

As for Dart, the excellent documentation can be found here:

Asynchronous Programming: Streams

Streams are implemented as part of the Dart core libraries, but not in Swift.

Quoting the Dart documentation:

A stream is a sequence of asynchronous events.

Streams are at the basis of reactive applications, where they play an important role with state management.

For example, streams are a great choice for searching content, where a new set of results is emitted each time the user updates the text in a search field.

Streams are not included in the Swift core libraries. 3rd party libraries such as RxSwift offer streams support and much more.

Streams are a broad topic that is not discussed here.

Read more: Dart Asynchronous Programming: Streams

Memory management

Dart manages memory with and advanced garbage collection scheme.

Swift manages memory via automatic reference counting (ARC).

This guarantees great performance because memory is released immediately when it is no longer used.

It does however shift the burden partially from the compiler to the developer.

In Swift we need to think about lifecycle and ownership of objects, and use the appropriate keywords (weak, strong, unowned) correctly to avoid retain cycles.

Read more: Swift Automatic Reference Counting.

Compilation and execution

First of all, an important distinction between just-in-time (JIT) and ahead-of-time (AOT) compilers:

JIT compilers

A JIT compiler runs during execution of the program, compiling on the fly.

JIT compilers are typically used with dynamic languages, where types are not fixed ahead of time. JIT programs run via an interpreter or a virtual machine (VM).

AOT compilers

An AOT compiler runs during creation of the program, before runtime.

AOT compilers are normally used with static languages, which know the types of the data. AOT programs are compiled into native machine code, which is executed directly by the hardware at runtime.


Quoting this great article by WM Lever:

When AOT compilation is done during development, it invariably results in much slower development cycles (the time between making a change to a program and being able to execute the program to see the result of the change). But AOT compilation results in programs that can execute more predictably and without pausing for analysis and compilation at runtime. AOT compiled programs also start executing faster (because they have already been compiled).

Conversely, JIT compilation provides much faster development cycles, but can result in slower or jerkier execution. In particular, JIT compilers have slower startup times, because when the program starts running the JIT compiler has to do analysis and compilation before the code can be executed. Studies have shown that many people will abandon an app if it takes more than a few seconds to start executing.


As a static language, Swift is compiled ahead-of-time.

Dart can be compiled both AOT and JIT. This provides significant advantages when used with Flutter. Quoting again:

JIT compilation is used during development, using a compiler that is especially fast. Then, when an app is ready for release, it is compiled AOT. Consequently, with the help of advanced tooling and compilers, Dart can deliver the best of both worlds: extremely fast development cycles, and fast execution and startup times. - WM Lever


With Dart we get the best of both worlds.

Swift suffers from the main drawback of AOT compilation. That is, compilation time increases with the size of the codebase.

For a medium sized app (between 10K and 100K lines), it can easily take minutes to compile an app.

Not so with Flutter apps, where we consistently get sub-second hot-reload, irrespective of the size of the codebase.


Other features not covered

The following features were not covered as they are quite similar in Dart and Swift:

  • Operators (see reference for Swift and Dart)
  • Strings (see reference for Swift and Dart)
  • Optional chaining in Swift (known as conditional member access in Dart).

Concurrency

  • Concurrent programming this is provided with isolates in Dart.
  • Swift uses Grand Central Dispatch (GCD) and dispatch queues.

My favourite Swift features missing from Dart

  • Structs
  • Enums with associated types
  • Optionals

My favourite Dart features missing from Swift

  • Just-in-time compiler
  • Futures with await/async (see async/await proposal by Chris Lattner)
  • Streams with yield/async* (RxSwift offers a superset of streams for reactive applications)

Conclusion

Both Dart and Swift are excellent languages, well suited for building modern mobile apps and beyond.

Neither language is superior, as they both have their own unique strong points.

When looking at mobile app development and the tooling for the two languages, I feel that Dart has the upper hand. This is due to the JIT compiler, which is at the foundation of stateful hot-reload in Flutter.

And hot-reload delivers a huge productivity gain when building apps, because it speeds up the development cycle from seconds or minutes to less than a second.

Developer time is a more scarce resource than computing time.

So optimising for developer time is a very smart move.


On the other hand, I feel that Swift has a very strong type system. Type safety is baked into all language features, and leads more naturally to robust programs.


Once we set aside personal preferences, programming languages are just tools. And it is our job as developers to choose the most appropriate tool for the job.

In any case, we can hope that both languages will borrow the best ideas from each other as they evolve.

References & Credits

Both Dart and Swift have an extensive feature set, and have not been fully covered here.

I prepared this article borrowing information from the official Swift and Dart documentation, which can be found here:

In addition, the section about JIT and AOT compilers is heavily inspired by this great article by WM Lever:

Have I missed something? Let me know in the comments. 🙂

Happy coding!

LEARN FLUTTER TODAY

Sign up for updates and get my free Flutter Layout Cheat Sheet.

No Spam. Ever. Unsubscribe at any time.