Dart for Kotliners
Similarities and differences from the perspective of a Kotlin developer
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.
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.
- Android, Desktop & Backend: Dart Virtual Machine (DVM) + Just In Time (JIT) compiler.
- iOS: Dart Virtual Machine (DVM) + Dart Byte Code (DBC) interpreter.
- Web: Dart Development Compiler (
dartdevc
).
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.
Hello world
Before entering in details, let’s see how a “Hello World” looks like in each language:
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 classNull
and a nullable type is defined as the union of the underlying type and theNull
type. The soundness of this system enables more compiler optimizations and performance improvements.
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:
Boolean
The bool
datatype represents the boolean values true
and false
(equivalent to Boolean
in Kotlin).
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.
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.
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.
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.
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.
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).
Nullability operators
Working with nullable types is very similar in both languages:
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:
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.
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.
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).
Let see some examples:
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.
Extension functions
Both languages have support for extension functions to add functionality to classes without having to inherit from them or modify their internals.
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.
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).
+ UseList.filled()
,List.empty()
orList.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). Thelength
getter onList
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()
orList.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).
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).
Map
Types of Map
available in Dart:
LinkedHashMap
: it is the default implementation ofMap
(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==
andhashCode
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.
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).
- Collection if: enables adding elements conditionally.
- Collection for: enables adding elements in a loop.
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:
Now let’s see another example with actual asynchronous concurrent code:
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.
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:
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 coldFlow
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 hotFlow
(likeSharedFlow
orStateFlow
) 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.
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:
- Android Studio
- IntelliJ IDEA (and other JetBrains IDEs)
- Visual Studio Code
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.