Dart for Kotliners

Similarities and differences from the perspective of a Kotlin developer

David Miguel
19 min readApr 21, 2021

Disclaimer: this article was written in 2021. Since then, both languages have evolved significantly. Please be aware that some information might be outdated. I may revise the article in the future.

When you are trying to learn something new it is always very helpful to relate the new concepts and information to your prior knowledge.

That is exactly what this article is meant for. Applying all your existing Kotlin knowledge and expertise to learn the Dart programming language as a first step to get into Flutter development.

We will cover the following topics:

· What is Dart?
· Hello world
· Type system
Numbers
Boolean
String
Dynamic
· Variables & Constants
· Operators
Arithmetic operators
Relational operators
Logical operators
Bitwise operators
Type check and cast operators
Nullability operators
Operator overloading
· Conditional expressions
· Loops
· Functions
· Classes
Extension functions
· Collections
List
Set
Map
Collection operations
· Asynchronous programming
Threading and parallelism
· Generator functions
Synchronous generation
Asynchronous generation
· Dependency management
· IDEs
· Learning resources

What is Dart?

Dart is an open-source, general-purpose, object-oriented, statically typed programming language developed by Google in 2011 with the ambition to be a better alternative to JavaScript for building scalable complex web applications.

The first stable release was published in November 2013. In February 2018, Dart 2.0 was released embracing a new vision of being a “language uniquely optimized for client-side development for web and mobile”. And last month, Dart 2.12 was released featuring stable versions of sound null safety and Dart FFI.

Leaving behind the initial criticism and bad press that it had during its first years, Dart is rapidly gaining popularity thanks to the emergence of Flutter for building cross-platform apps.

Even though it hasn’t yet broken into the top 10 on the PYPL Popularity index (20th) nor in the TIOBE Index (36th), it is in the top 7 of the “Most Loved vs. Dreaded Languages” according to the results of the 2021 Stack Overflow annual survey.

https://insights.stackoverflow.com/survey/2021

Although Dart is a general-purpose language, it contains a unique combination of capabilities for building apps:

  • Productive: iterative development (hot reload) and rich constructs like isolates and async/await for handling common concurrent and event-driven app patterns.
  • Portable: multi-platform with native performance as it is compiled to x86 and ARM machine code or optimized JavaScript for the web.
  • Robust: a sound, null-safe type system to catch errors during development and a scalable platform ready for building large business-critical apps.

Iterative development

During development, a specific toolchain is used for fast, incremental compilation to provide instant feedback to the developer.

Native performance

Once the app is ready for release, a separate toolchain is used for building the production app:

  • Native: an Ahead Of Time (AOT) compiler is used to transform the Dart code into native x86/ARM binary code.
  • Web: the dart2js tool is used to transpile the Dart code into optimized JavaScript.
  • Backend: the server-side application can be run directly in the DartVM using its JIT compiler or it can be AOT compiled.

Robust

Dart has a sound, null-safe type system. It uses a combination of static and runtime checks to guarantee that an expression of one type cannot produce a value of another type.

General overview of Dart and Kotlin

Hello world

Before entering in details, let’s see how a “Hello World” looks like in each language:

Hello world (Dartpad / Kotlin Playground / Gist)

Type system

Dart is a statically typed language with sound null safety support.

In Dart, everything that a variable can hold is an object including primitive types, functions, and even the value null which is an instance of the Null class.

All the classes inherit from Object? and have a subtype Never (equivalent to Any? and Nothing in Kotlin).

If a function doesn’t return a meaningful value, the type void is used (like Unit in Kotlin).

Although both languages are non-nullable by default, nullability in the type system is implemented in different ways:

  • In Kotlin, nullable types form a parallel subtype hierarchy and nullability itself is a subtype relationship. null is a reserved keyword in the language.
  • In Dart, the value null is an instance of the class Null and a nullable type is defined as the union of the underlying type and the Null type. The soundness of this system enables more compiler optimizations and performance improvements.
Dart and Kotlin Type Hierarchy

Numbers

Dart only provides int type for integer numbers and double for fractional numbers (equivalent to Long and Double in Kotlin). Both types inherit from num (Number in Kotlin). The internal representation varies depending on the target platform.

In the following table we can see the equivalent types between both languages:

Working with numbers is quite similar:

Numbers (Dartpad / Kotlin Playground / Gist)

Boolean

The bool datatype represents the boolean values true and false (equivalent to Boolean in Kotlin).

Boolean (Dartpad / Kotlin Playground / Gist)

String

In both languages the String type represents a sequence of UTF-16 code units.

In Dart, you can write a String using both single (') or double-quotes (). Multi-line strings are also supported and String interpolation works the same way as in Kotlin.

Strings (Dartpad / Kotlin Playground / Gist)

Dynamic

In Dart, if you need the flexibility of a dynamic language you can use the dynamic type. The dynamic type itself is static but can contain any type at runtime. Kotlin only supports dynamic type when targeting Kotlin/JS.

Dynamic (Dartpad / Kotlin Playground / Gist)

Variables & Constants

Dart has mutable and immutable variables and compile-time constants.

Like Kotlin, Dart supports type inference (type coercion), so you can omit the type whenever it can be inferred from the context.

Dart requires every statement to end with a semicolon (probably the most annoying difference when coming from Kotlin).

Nullable types are declared appending ? at the end of the type just as in Kotlin.

Variables & Constants (Dartpad / Kotlin Playground / Gist)

Operators

Arithmetic operators

The only difference between both languages is how the division operator works. While in Kotlin a division can be integer or fractional depending on the type of the operands, Dart provides two different operators for each case.

Arithmetic operators (Dartpad / Kotlin Playground / Gist)

Relational operators

Identity and equality checks work in a similar manner. The default equality implementation in Object / Any fallbacks to the identity operator that checks whether both variables refer to the same instance.

Comparing collections is an exception. Collections in Dart have no inherent equality. Two sets are not equal, even if they contain exactly the same objects as elements. For that, you need to use some of the utility classes provided by the collection package.

Relational operators (Dartpad / Kotlin Playground / Gist)

Logical operators

Nothing new when it comes to logical operators.

Bitwise operators

Type check and cast operators

As in Kotlin, flow-based type promotion (smart cast) is applied after a type check (except for class fields, no matter if they are final).

Type check and cast operators (Dartpad / Kotlin Playground / Gist)

Nullability operators

Working with nullable types is very similar in both languages:

Nullability operators (Dartpad / Kotlin Playground / Gist)

Operator overloading

Kotlin and Dart support operator overloading. For example, if we want to sum two points like Point(2,3) + Point(1,3), we can override the plus operator:

Operator overloading (Dartpad / Kotlin Playground / Gist)

Conditional expressions

In Kotlin everything is an expression, so almost everything has a value including if and when. It is not the case in Dart, you cannot assign the result of an if to a variable but you can use the ternary conditional operator instead.

Dart switch is similar to Kotlin when statement, but far less powerful. It doesn’t support equality tests with non-constant values, ranges, type matching, variable declaration in the switch expression, return value, implicit break between cases… Let’s hope for an enhanced switch in future versions of Dart.

Conditional expressions (Dartpad / Kotlin Playground / Gist)

Loops

Kotlin ranges are very useful when it comes to working with loops. Unfortunately, Dart doesn’t support them. It follows a more classical syntax.

Loops (Dartpad / Kotlin Playground / Gist)

Functions

Both languages support more or less the same features when it comes to functions (optional parameters, default values, top-level functions, one-line functions, lambdas, high order functions, function references, etc.).

One of the main differences is how they handle optional and named parameters:

  • In Kotlin, you can mix named and positional parameters without any limitation. Also, you don’t need any special notation in the function definition to be able to use named parameters when calling it. If you want to make a parameter optional, you can just make it nullable and assign null as the default value.
  • In Dart, you cannot mix named and positional arguments and to be able to use named parameters or default values, you need to wrap the parameters in [] (positional) or {} (named).
Dart function parameter types

Let see some examples:

Functions (Dartpad / Kotlin Playground / Gist)

In Kotlin, there is the trailing lambda convention where if the last parameter of a function is a function, then a lambda expression passed as the corresponding argument can be placed outside the parentheses. This together with the ability to have function literals with receiver, allows writing very expressive, type-safe DSLs. Unfortunately, Dart does not support any of these features yet.

Classes

Kotlin and Dart are object-oriented languages, so classes are first-class citizens. Dart syntax for classes is almost identical to Java or C# but slightly different from Kotlin.

Classes, members, and functions are public by default in both languages. While Kotlin provides private, protected, internal visibility modifiers, Dart handles visibility at the file/library level and it can only be public (default) or private. To make a class or a member private you need to prefix its name with _.

Unlike Kotlin, Dart doesn’t support method overload, so you cannot have more than a function with the same name. One consequence of this is that you cannot have several “standard” constructors like in Kotlin. Instead, you need to use named constructors.

Dart also supports factory constructors which don’t necessarily create a new instance of the class. They are useful to return an instance of a subclass, implement a singleton or return an instance from a cache.

Both languages have implicit getters and setters for class properties that you can freely override.

In Kotlin, you need to mark a class with open to make it inheritable. In Dart, they are always open for inheritance. Both languages have single inheritance. To inherit a class theextends keyword is used.

Dart doesn’t have an interface keyword. Every class implicitly defines an interface containing all the instance members of the class and of any interfaces it implements. To implement the interface of a class theimplements keyword is used. To define an abstract class the abstract keyword is used.

The equivalent of Kotlin lateinit is late. late is also used for lazy fields (by lazy {} in Kotlin).

In Dart, you can “attach” functionality to classes without inheriting from them using mixins. The closest equivalent in Kotlin would be interface delegation. But as in Kotlin a class by itself doesn’t define an interface, you need both a base interface and a concrete implementation, making it substantially more verbose than Dart mixins.

Dart doesn’t have an equivalent for Kotlin data classes and sealed classes yet.

Classes (Dartpad / Kotlin Playground / Gist)

Extension functions

Both languages have support for extension functions to add functionality to classes without having to inherit from them or modify their internals.

Extension functions (Dartpad / Kotlin Playground / Gist)

Collections

Both languages offer List, Set and Map. However, Kotlin differentiates between mutable and immutable collections at the interface level. Whereas in Dart, collections are always mutable by default (unless you create a copy using unmodifiable() factory or a view using UnmodifiableListView).

Also, Dart collections (generic classes in general) are always covariant. This means that, if a Car class inherits from Vehicle, you can use a List<Car> anywhere the List<Vehicle> is required. In Kotlin, only immutable collections are covariant to avoid runtime subtyping errors.

Dart and Kotlin Collection hierarchy

List

Dart has two types of List:

  • Fixed-length list: an error occurs when you attempt to use operations that can change the length of the list (similar to an Array in Kotlin).
    + Use List.filled(), List.empty() or List.generate(growable: false) factories to create a fixed-length list.
  • Growable list: a list that can grow in size (similar to a MutableList in Kotlin). The length getter on List also has a corresponding setter, which you can use to truncate or increase the size of the list (increasing the size is only possible with nullable types).
    + Use [] , List.generate() or List.filled(growable: true) to create a growable list.

The default implementation of List in Kotlin is ArrayList, whereas in Dart is _GrowableList. Both of them can be seen as resizable arrays.

In Kotlin, two lists are considered equal if they have the same sizes and structurally equal elements at the same positions. In Dart, however, collections have no inherent equality, so to compare lists you need to use the ListEquality class provided in the collection package (or your custom implementation).

Lists (Dartpad / Kotlin Playground / Gist)

Set

The default implementation of Set in both languages is a LinkedHashSet , which evaluates equality using the == operator and preserves the insertion order.

Both languages offer HashSet, an alternative implementation of a Set which requires less memory to store the same number of elements but does not preserve the insertion order.

In Kotlin, two Set are equal if they have the same size and for each element of a Set there is an equal element in the other Set. In Dart, we have to use the SetEquality class provided in the collection package (or your custom implementation).

Sets (Dartpad / Kotlin Playground / Gist)

Map

Types of Map available in Dart:

  • LinkedHashMap: it is the default implementation of Map (same as in Kotlin). It is based on a hash-table and the insertion order is remembered.
  • HashMap: it is also based on a hash table but it does not preserve the insertion order. The keys must have consistent == and hashCode implementations.
  • SplayTreeMap<K, V>: it is based on a self-balancing binary tree that allows recently accessed elements to be accessed quicker (e.g. cache). It is also useful when you need to iterate the keys in sorted order. It allows most operations in amortized logarithmic time.

In Kotlin, two Map containing the equal pairs are equal regardless of the pair order. In Dart, we have to use the MapEquality class provided in the collection package.

Maps (Dartpad / Kotlin Playground / Gist)

Collection operations

When it comes to manipulating collections, both languages support the most common functional operations:

  • Filtering
  • Transforming
  • Aggregating

In general, the Kotlin Standart library provides a richer set of functions to work with collections. If you miss any of them, you can make use of the packages supercharged or kt.dart that port the Kotlin Standard library to Dart.

However, Dart has three features without a close equivalent in Kotlin that are very useful for working with collections:

  • Spread operator ( /?…): it provides a concise way to insert multiple values into a collection (shallow copy).
Spread operator (Dartpad / Gist)
  • Collection if: enables adding elements conditionally.
Collection if (Dartpad / Gist)
  • Collection for: enables adding elements in a loop.
Collection for (Dartpad / Gist)

Asynchronous programming

When it comes to asynchronous programming, Kotlin and Dart follow quite different approaches for solving the same problem:

How do we write some code that waits for something most of the time?

Although you could solve this problem using callbacks, you would quickly end up suffering from what is known as the “callback hell”. That is why both languages offer better tools for dealing with asynchronous code.

Kotlin’s approach to working with asynchronous code is using coroutines and the idea of functions that can suspend its execution at some point and resume later on (suspending functions).

Dart, however, follows a more classical C# approach using futures (aka promises) in combination with await / async keywords.

A Future represents the result of an asynchronous operation, and can have two states: uncompleted or completed (with value or error). It is similar to Deferred in Kotlin.

The async keyword allows us to define asynchronous functions. The return type of the function has to be Future<T> (you don’t need to explicitly return a Future value inside you async function, the compiler will wrap your plain return value into a Future). await keyword allows us to wait for a Future to complete.

The mechanism behind the Kotlin suspending functions and Dart asynchronous functions is also quite different. Kotlin uses a Continuation-Passing Style (CPS) behind the scenes. Dart however uses an event loop mechanism with an associated event queue of pending asynchronous operations (similar to Android Handlers).

Another critical difference is the default behavior of the asynchronous code:

  • In Kotlin, calls to suspending functions are sequential by default. Only if you explicitly call async{} the execution will be concurrent.
  • In Dart, calls to asynchronous functions are concurrent by default. Only if you await the asynchronous function then the execution will be sequential.

Let see an example of an asynchronous sequential code:

Asynchronous sequential-like code (Dartpad / Kotlin Playground / Gist)

Now let’s see another example with actual asynchronous concurrent code:

Asynchronous concurrent code (Dartpad / Kotlin Playground / Gist)

In both languages, try-catch clauses are you for handling errors in asynchronous code as you would with synchronous code.

Threading and parallelism

In the previous section, we have talked about how to run your asynchronous code in a sequential or concurrent fashion in a single thread. But what about if you want to do some heavy computation and you don’t want to block your UI thread?

In Kotlin, you can easily off-load work from your main thread by specifying in the context of the coroutine the propper dispatcher. So for example, Dispatchers.Default is optimized for CPU-intensive work as it is backed by a thread-pool with as many threads as there are CPU cores in the system.

By default, different coroutines running in different threads share the same memory area. Although Kotlin also supports other programming styles that avoid having a shared mutable state (like CSP or Actor model using coroutine channels).

Dart follows a much more strict approach (inspired by Erlang’s actor model). By default, your code runs in an isolate. An isolate is a construct that has its own memory area (heap), its own thread, and its own event loop. If you want to do a heavy computation in another thread you have to spawn a new isolate. Isolates do not share anything between them. The only way that isolates can communicate with each other is by passing messages back and forth.

To spawn an isolate you use the Isolate.spawn() method which takes as parameters the function to be spawned (entry point) and the object that will be passed to the spawned function.

Threading and parallelism (Dartpad / Kotlin Playground / Gist)

Isolates are not cheap in terms of memory, they are not as lightweight as coroutines. Luckily there is already a solution for that called isolate groups. Isolates in an isolate group share various internal data structures representing the running program, making them much faster to spawn and cheaper in terms of memory usage. Also, while isolate groups still prevent shared access to mutable objects between isolates, the group is implemented with a shared heap, which unlocks further capabilities (e.g. passing objects from one isolate to another).

Generator functions

Different constructs are used in Dart for dealing with a sequence of on-demand generated values depending on the nature of the generation.

Synchronous generation

Synchronous generator functions are used to lazily produce a sequence of values in an on-demand synchronous manner.

The body of this type of function has to be marked with sync* and the return type has to be an Iterable<T> object. To emit values the yield keyword is used (you cannot use return in this type of function).

In Kotlin, the behavior of Iterable is different. The equivalent of a Dart’s Iterable and synchronous generator functions is Kotlin’s Sequence<T> and its builders. A Sequence lazily performs all the processing steps one-by-one for every single element (like Dart’s Iterable). Whereas an Iterable in Kotlin eagerly completes each operation for the whole collection and then proceeds to the next step.

In the following example, we can clearly see the difference in behavior:

Synchronous generation (Dartpad / Kotlin Playground / Gist)

Asynchronous generation

Asynchronous generator functions are quite common in Flutter, especially when you use the Bloc pattern. They allow returning a sequence of multiple on-demand asynchronously computed values.

The body of this type of function has to be marked with async* and the return type has to be an Stream. The keyword yield is also used to emit values on the Stream. They are equivalent to Kolin’s Flow type, flow {} builder and emit() function.

To consume an Stream in Dart, you can use the listen() method (like collect {} in Kotlin) or await for , the asynchronous version of the for loop.

Both types of generators support delegating the production of the values to another generator function (or the same generator in a recursive way) using the yield* keyword (similar to emitAll() in Kotlin).

There are actually two types of Stream:

  • Single subscription Stream: the generation of values does not start until you subscribe to it. You can only subscribe once. Equivalent to a cold Flow in Kotlin.
  • Broadcast Stream: the generation of values happens independently of the presence of subscribers. There can be multiple simultaneous subscribers. It is equivalent to a hot Flow (like SharedFlow or StateFlow) in Kotlin.

If you want to be able to emit values in the Stream from different places (e.g. from different methods of a class), you can use a StreamController and emit values using its add() method (similar to SharedFlow and emit() or StateFlow and value in Kotlin).

Both Dart’s Stream and Kotlin’s Flow can be transformed with operators.

As with collections, the amount of operators available in Dart’s core library is smaller than in Kotlin’s. However, the Dart team maintains a couple of complementary packages. The package:async (do not confuse with dart:async) contains utility classes to work with asynchronous computations (e.g. StreamGroup.merge()). The package:stream_transform contains extension methods on Stream adding common transform operators (e.g. asyncWhere()). And otherwise, you can make use of package:rxdart that is also built on top of the Stream API.

Regarding error handling, in both languages you can wrap the Flow / Stream into a try-catch, or you can use handleError() operator in Dart which is equivalent to catch{} in Kotlin.

Asynchronous generation (Dartpad / Kotlin Playground / Gist)

Dependency management

In Kotlin, you are probably used to managing your dependencies using Gradle or Maven. The equivalent in the Dart ecosystem is pub, the official package manager.

You can easily find Dart packages using pub.dev. And later, to use them in your code you just need to import them in your pubspec.yaml file (similar to build.gradle or pom.xml files).

IDEs

If you only want to play with Dart you can use DartPad in the browser. Otherwise, Dart provides official plugins for the following IDEs:

The community also maintains the following plugins:

Learning resources

In dart.dev you can find a tour through the language and several tutorials and codelabs. There are also several books written about the Dart language and many Flutter books also have dedicated sections about Dart.

For any specific doubt about the language, you can always check the Dart language specification. And in the Dart language repository, you can find accepted and open language features proposals and the discussions around them, very useful to find out why a certain design decision was made.

Wrapping up

After this long article, you should have a basic idea about Dart. Although we have only scratched the surface, you can see that you can port a lot of your existing Kotlin knowledge directly to Dart or with small syntax changes.

In the end, most modern languages share their main functionality but excel in some specific features that make them ideal for different use cases. In the case of Dart, it excels in its portability and performance in different platforms, its fast development experience, and a set of handy features for event-driven UI development (async-await, null-safety, spread operator, collection if and for, etc.), which makes it ideal for cross-platform app development for web, mobile, and desktop.

--

--