Flutter: Dart Immutable Objects and Values
When I started developing Flutter apps, I also met the Dart language for the first time in my life. I was quite surprised to notice that the Flutter team chose a language that does not support immutability extensively out-of-the-box. I still think that’s quite strange, in particular if you compare it to Swift UI that is deeply rooted in value types.
Let’s explore our alternatives.
The naïve immutability
First of all, let’s see what Dart (at current version, which is 2.8) provides out-of-the-box.
@immutable
annotation says that every field insidePerson
(and its subclasses) must befinal
. Otherwise, Dart compiler will throw a warning, but not an error.final
keyword says that you cannot assign a value to the property after its initialization. Otherwise, Dart compiler will emit an error.UnmodifiableListView
is aList
wrapper that forbids modifications (e.g.: add or remove items). It exposes methods likeadd()
oraddAll()
, but they throw exceptions at runtime.
It’s just ok, but it completely misses compiler-time safety requirement and you don’t have any method you would expect from a data class (like equality, hashing and cloning).
Source generation
Google itself proposes a package to help: it’s called built_value
. You define a class description and a source generator creates the missing parts in a paired file.
The same idea is borrowed by freezed
, another Dart package that uses a more modern syntax to achieve similar results. You install it by inserting a dependency inside pubspec.yaml
file:
I’m using any
version since this package does not expose a functionality that may change during time, so it’s ok to remain always at latest release.
Then, you can open a terminal window and you can execute:
build_runner
will observe the filesystem and it will generate the proper code every time you save a definition.
Basic usage
Let’s convert Person
to freezed
:
- The first line says to
build_runner
that the paired file isperson.freezed.dart
. This name could not be changed. @freezed
annotation tells that the following data class declaration have to be synthesized by the library.- The factory initializer specifies also the properties declared by this value type.
- Please note that
freezed
is ready to manage nullable types that will be introduced in the upcoming Dart 2.9 release. At this time it checks for null values only at runtime. In this case onlyjobTitle
property is nullable. - If you need to provide default value for non-required properties, you have to use
@Default
annotation.
And what do you get for free?
toString
is overridden to provide a pretty description of the object to print.==
operator compares the objects property by property.
Make copies
copyWith
method returns a new instance with updated properties:
If you have nested hierarchies to copy, you could use the straightforward chained syntax:
So p2
will contain a copy of p1
with new best friend’s father name and surname, if p1
has a best friend.
Custom methods and properties
If you need to add custom methods, you cannot use the simplified mixin syntax anymore:
You can also add custom getters, even annotated with @late
, which is equivalent to Swift’s lazy
or Kotlin’s lateinit
:
Sealed classes
Powerful enumerations are loved in Swift and Kotlin world and I miss them very much. With freezed
you can write beauties like this:
Obviously you cannot perform pattern matching with switch
but there are methods like when
or maybeWhen
that freezed
synthesized for you:
Immutable collections
What about compile-time safety of collections? freezed
does nothing to mitigate this problem, while built_value
has a twin package called build_collection
that exposes immutable lists, dictionaries and sets. These ones conform to Iterable
so they are pretty compatible with official Dart collections.
But I’d like to dare a little bit more with kt_dart
package, a port of kotlin-stdlib
including immutable collections and other benefits, like deep comparison of lists and map
/filter
/reduce
without exotic names. You install it by inserting a new dependency in pubspec.yaml
:
You can modify Person
data class just changing types:
The friends
list is now immutable by default. The creation of the list is easier than before:
For example, if you want to create a method to generate a new immutable person with a new friend, it’s easy like that:
Just a note: since KtList
does not conform to Iterable
, you cannot use for loop directly, but you should take the iterator before:
VS Code Integration
Since the boilerplate code could be tedious and hard to remember, maybe it’s convenient to setup some user snippets by selecting Preferences: Configure User Snippets in VS Code launched and then selecting Dart language. Now you can copy and paste the following snippets:
You can also exclude the generated files from Explorer tab. To do so, open Settings and search for Files: Exclude preference. Then, you can add this pattern to the list: **/*.freezed.dart
.
JSON serialization and deserialization
freezed
is integrated by design with json_serializable
package but there are some not obvious challenging using kt_dart
: I discussed this topic in depth in this follow-up.