Welcome to Freezed, yet another code generator for unions/pattern-matching/copy.
0.14.0 and null-safety
Important note:
From 0.14.0 and onwards, Freezed does not support non-null-safe code.
If you want to keep using Freezed but cannot migrate to null-safety yet, use the version 0.12.7 instead.
Note that this version is no-longer maintained (so bugs found there won't be fixed).
For the documentation of the version 0.12.7, refer to https://pub.dev/packages/freezed/versions/0.12.7
In the scenario where you are using the version 0.12.7, but one of your dependency is using 0.14.0 or above, you will have a version conflict on freezed_annotation.
In that case, you can fix the error by adding the following to your pubspec.yaml:
dependency_overrides:
freezed: ^0.12.7
freezed_annotation: ^0.12.0
Motivation
While there are many code-generators available to help you deal with immutable objects, they usually come with a trade-off.
Either they have a simple syntax but lack features, or they have very advanced features but with complex syntax.
A typical example would be a "clone" method.
Current generators have two approaches:
a copyWith, usually implemented using ??:
The syntax is very simple to use, but doesn't support some use-cases: nullable values.
We cannot use such copyWith to assign null to a property like so:
a builder method combined with a temporary mutable object, usually used this way:
The benefits of this approach are that it does support nullable values.
On the other hand, the syntax is not very readable and fun to use.
Say hello to Freezed~, with support for advanced use-cases without compromising on the syntax.
See the example or the index for a preview on what's available
Index
How to use
To use Freezed, you will need your typical build_runner/code-generator setup.
First, install build_runner and Freezed by adding them to your pubspec.yaml file:
# pubspec.yaml
dependencies:
freezed_annotation:
dev_dependencies:
build_runner:
freezed:
This installs three packages:
If you plan on using [Frezed] in combination with json_serializable, recent versions of json_serializable and meta may require you to disable the invalid_annotation_target warning.
Similarly, you may want to disable warnings that happen in .freezed.dart, if any.
To do that, you can add the following to an analysis_options.yaml at the root of your project:
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
errors:
invalid_annotation_target: ignore
To run the code generator you have two possibilities:
Note that like most code-generators, Freezed will need you to both import the annotation (meta) and use the part keyword on the top of your files.
As such, a file that wants to use Freezed will start with:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'my_file.freezed.dart';
CONSIDER also importing package:flutter/foundation.dart.
The reason being, importing foundation.dart also imports classes to make an object nicely readable in Flutter's devtool.
If you import foundation.dart, Freezed will automatically do it for you.
A full example would be:
// main.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'main.freezed.dart';
@freezed
class Union with _$Union {
const factory Union(int value) = Data;
const factory Union.loading() = Loading;
const factory Union.error([String? message]) = ErrorDetails;
}
It is likely that the code generated by Freezed will cause your linter to report warnings.
The solution to this problem is to tell the linter to ignore generated files, by modifying your analysis_options.yaml:
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
The features
Freezed works differently than most generators. To define a class using Freezed, you will not declare properties but instead factory constructors.
For example, if you want to define a Person class, which has 2 properties:
To do so, you will have to define a factory constructor that takes these properties as parameter:
@freezed
class Person with _$Person {
factory Person({ String? name, int? age }) = _Person;
}
Which then allows you to write:
var person = Person(name: 'Remi', age: 24);
print(person.name); // Remi
print(person.age); // 24
NOTE:
You do not have to use named parameters for your constructor.
All valid parameter syntaxes are supported. As such you could write:
@freezed
class Person with _$Person {
factory Person(String name, int age) = _Person;
}
Person('Remi', 24)
@freezed
class Person with _$Person {
const factory Person(String name, {int? age}) = _Person;
}
Person('Remi', age: 24)
...
You are also not limited to one constructor and non-generic class.
From example, you should write:
@freezed
class Union<T> with _$Union<T> {
const factory Union(T value) = Data<T>;
const factory Union.loading() = Loading<T>;
const factory Union.error([String? message]) = ErrorDetails<T>;
}
See unions/Sealed classes for more information.
As you might have noticed, the abstract keyword is not needed anymore when declaring freezed classes.
This allows you to easily use mixins with the benefit of having your IDE telling you what to implement.
We can now turn this:
@freezed
abstract class MixedIn with Mixin implements _$MixedIn {
MixedIn._();
factory MixedIn() = _MixedIn;
}
mixin Mixin {
int method() => 42;
}
into this:
@freezed
class MixedIn with _$MixedIn, Mixin {
const MixedIn._();
factory MixedIn() = _MixedIn;
}
mixin Mixin {
int method() => 42;
}
Sometimes, you may want to manually define methods/properties on that class.
But you will quickly notice that if you try to do:
@freezed
class Person with _$Person {
const factory Person(String name, {int? age}) = _Person;
void method() {
print('hello world');
}
}
then it won't work.
This is because by default, Freezed has no way of "extending" the class and instead "implements" it.
To fix it, we need a subtle syntax change to allow Freezed to generate valid code.
To do so, we have to define a single private constructor:
@freezed
class Person with _$Person {
const Person._(); // Added constructor
const factory Person(String name, {int? age}) = _Person;
void method() {
print('hello world');
}
}
A common use-case with classes is to want to add assert(...) statements to a construtor:
class Person {
Person({
String? name,
int? age,
}) : assert(name.isNotEmpty, 'name cannot be empty'),
assert(age >= 0);
final String name;
final int age;
}
Freezed supports this use-case through the @Assert decorator:
abstract class Person with _$Person {
@Assert('name.isNotEmpty', 'name cannot be empty')
@Assert('age >= 0')
factory Person({
String? name,
int? age,
}) = _Person;
}
Unfortunately, Dart does not allow constructors with the syntax used by Freezed to specify default values.
Which means you cannot write:
abstract class Example with _$Example {
const factory Example([int value = 42]) = _Example;
}
But Freezed offers an alternative for this: @Default
As such, you could rewrite the previous snippet this way:
abstract class Example with _$Example {
const factory Example([@Default(42) int value]) = _Example;
}
NOTE:
If you are using serialization/deserialization, this will automatically add a @JsonKey(defaultValue: <something>) for you.
A common use-case is to do a one-to-one mapping between the parameters of a callback and a constructor.
For example, you may write:
future.catchError((error) => MyClass.error(error))
But that's kind of redundant. As such, Freezed offers a simpler syntax:
future.catchError($MyClass.error)
This new code is strictly equivalent to the previous snippet, just shorter.
Note that this is both compatible with default values and generics.
Freezed supports property and class level decorators/documentation by decorating/documenting their respective parameter and constructor definition.
Consider:
@freezed
class Person with _$Person {
const factory Person({
String? name,
int? age,
Gender? gender,
}) = _Person;
}
If you want to document name, you can do:
@freezed
class Person with _$Person {
const factory Person({
/// The name of the user.
///
/// Must not be null
String? name,
int? age,
Gender? gender,
}) = _Person;
}
If you want to mark the property gender as @deprecated, then you can do:
@freezed
class Person with _$Person {
const factory Person({
String? name,
int? age,
@deprecated Gender? gender,
}) = _Person;
}
This will deprecate both:
Similarly, if you want to decorate the generated class you can decorate the defining factory constructor.
As such, to deprecate _Person, you could do:
@freezed
class Person with _$Person {
@deprecated
const factory Person({
String? name,
int? age,
Gender? gender,
}) = _Person;
}
When you have multiple types in the same class you might want to make one of those types to implement a interface or mixin a class. You can do that using the @Implements decorator or @With respectively. In this case City is implementing with GeographicArea.
abstract class GeographicArea {
int get population;
String get name;
}
@freezed
class Example with _$Example {
const factory Example.person(String name, int age) = Person;
@Implements(GeographicArea)
const factory Example.city(String name, int population) = City;
}
In case you want to specify a generic mixin or interface you need to declare it as a string using the With.fromString constructor, Implements.fromString respectively. Similar Street is mixing with AdministrativeArea<House>.
abstract class GeographicArea {}
abstract class House {}
abstract class Shop {}
abstract class AdministrativeArea<T> {}
@freezed
class Example with _$Example {
const factory Example.person(String name, int age) = Person;
@With.fromString('AdministrativeArea<House>')
const factory Example.street(String name) = Street;
@With(House)
@Implements(Shop)
@Implements(GeographicArea)
const factory Example.city(String name, int population) = City;
}
In case you want to make your class generic, you do it like this:
@freezed
class Example<T> with _$Example<T> {
const factory Example.person(String name, int age) = Person<T>;
@With.fromString('AdministrativeArea<T>')
const factory Example.street(String name, T value) = Street<T>;
@With(House)
@Implements(GeographicArea)
const factory Example.city(String name, int population) = City<T>;
}
Note: You need to make sure that you comply with the interface requirements by implementing all the abstract members. If the interface has no members or just fields, you can fulfil the interface contract by adding them in the constructor of the union type. Keep in mind that if the interface defines a method or a getter, that you implement in the class, you need to use the Custom getters and methods instructions.
Note 2: You cannot use @With/@Implements with freezed classes. Freezed classes can neither be extended nor implemented.
When using Freezed, the toString, hashCode and == methods are overridden as you would expect:
@freezed
class Person with _$Person {
factory Person({ String? name, int? age }) = _Person;
}
void main() {
print(Person(name: 'Remi', age: 24)); // Person(name: Remi, age: 24)
print(
Person(name: 'Remi', age: 24) == Person(name: 'Remi', age: 24),
); // true
}
As stated in the very beginning of this readme, Freezed does not compromise on the syntax to have a powerful copy.
The copyWith method generated by Freezed does support assigning a value to null.
For example, if we take our previous Person class:
@freezed
class Person with _$Person {
factory Person(String name, int age) = _Person;
}
Then we could write:
var person = Person('Remi', 24);
// `age` not passed, its value is preserved
print(person.copyWith(name: 'Dash')); // Person(name: Dash, age: 24)
// `age` is set to `null`
print(person.copyWith(age: null)); // Person(name: Remi, age: null)
Notice how copyWith correctly was able to understand null parameters.
While copyWith is very powerful in itself, it starts to get inconvenient on more complex objects.
Consider the following classes:
@freezed
class Company with _$Company {
factory Company({String? name, Director? director}) = _Company;
}
@freezed
class Director with _$Director {
factory Director({String? name, Assistant? assistant}) = _Director;
}
@freezed
class Assistant with _$Assistant {
factory Assistant({String? name, int? age}) = _Assistant;
}
Then, from a reference on Company, we may want to perform changes on Assistant.
For example, to change the name of an assistant, using copyWith we would have to write:
Company company;
Company newCompany = company.copyWith(
director: company.director.copyWith(
assistant: company.director.assistant.copyWith(
name: 'John Smith',
),
),
);
This works, but is relatively verbose with a lot of duplicates.
This is where we could use Freezed's "deep copy".
If an object decorated using @freezed contains other objects decorated with @freezed, then Freezed will offer an alternate syntax to the previous example:
Company company;
Company newCompany = company.copyWith.director.assistant(name: 'John Smith');
This snippet will achieve strictly the same result as the previous snippet (creating a new company with an updated assistant name), but no longer has duplicates.
Going deeper in this syntax, if instead, we wanted to change the director's name then we could write:
Company company;
Company newCompany = company.copyWith.director(name: 'John Doe');
Overall, based on the definitions of Company/Director/Assistant mentioned above, all the following "copy" syntaxes will work:
Company company;
company = company.copyWith(name: 'Google', director: Director(...));
company = company.copyWith.director(name: 'Larry', assistant: Assistant(...));
company = company.copyWith.director.assistant(name: 'John', age: 42);
Null consideration
Some objects may also be null. For example, using our Company class, then Director may be null.
As such, writing:
Company company = Company(name: 'Google', director: null);
Company newCompany = company.copyWith.director.assistant(name: 'John');
doesn't make sense.
We can't change the director's assistant if there is no director to begin with.\
In that situation, company.copyWith.director will return null, and our previous example will result in a null exception.
To fix it, we can use the ?. operator and write:
Company? newCompany = company.copyWith.director?.assistant(name: 'John');
Coming from other languages, you may be used with features like "tagged union types" / sealed classes/pattern matching.
These are powerful tools in combination with a type system, but Dart currently does not support them.
But fear not, Freezed supports them all, by using a syntax similar to Kotlin.
Defining a union/sealed class with Freezed is simple: write multiple constructors:
@freezed
class Union with _$Union {
const factory Union(int value) = Data;
const factory Union.loading() = Loading;
const factory Union.error([String? message]) = ErrorDetails;
}
This snippet defines a class with three states.
Note how we gave meaningful names to the right hand of the factory constructors we defined. They will come in handy later.
When defining multiple constructors, you will lose the ability to read properties that are not common to all constructors:
For example, if you write:
@freezed
class Example with _$Example {
const factory Example.person(String name, int age) = Person;
const factory Example.city(String name, int population) = City;
}
Then you will be unable to read age and population directly:
var example = Example.person('Remi', 24);
print(example.age); // does not compile!
On the other hand, you can read properties that are defined on all constructors.
For example, the name variable is common to both Example.person and Example.city constructors.
As such we can write:
var example = Example.person('Remi', 24);
print(example.name); // Remi
example = Example.city('London', 8900000);
print(example.name); // London
You also can use copyWith with properties defined on all constructors:
var example = Example.person('Remi', 24);
print(example.copyWith(name: 'Dash')); // Example.person(name: Dash, age: 24)
example = Example.city('London', 8900000);
print(example.copyWith(name: 'Paris')); // Example.city(name: Paris, population: 8900000)
To be able to read the other properties, you can use pattern matching thanks to the generated methods:
Alternatively, you can use the is operator:
var example = Example.person('Remi', 24);
if (example is Person) {
print(example.age); // 24
}
The when method is the equivalent to pattern matching with destructing.
Its prototype depends on the constructors defined.
For example, with:
@freezed
class Union with _$Union {
const factory Union(int value) = Data;
const factory Union.loading() = Loading;
const factory Union.error([String? message]) = ErrorDetails;
}
Then when will be:
var union = Union(42);
print(
union.when(
(int value) => 'Data $data',
loading: () => 'loading',
error: (String? message) => 'Error: $message',
),
); // Data 42
Whereas if we defined:
@freezed
class Model with _$Model {
factory Model.first(String a) = First;
factory Model.second(int b, bool c) = Second;
}
Then when will be:
var model = Model.first('42');
print(
model.when(
first: (String a) => 'first $a',
second: (int b, bool c) => 'second $b $c'
),
); // first 42
Notice how each callback matches with a constructor's name and prototype.
NOTE:
All callbacks are required and must not be null.
If that is not what you want, consider using maybeWhen.
The maybeWhen method is equivalent to when, but doesn't require all callbacks to be specified.
On the other hand, it adds an extra orElse required parameter, for fallback behavior.
As such, using:
@freezed
class Union with _$Union {
const factory Union(int value) = Data;
const factory Union.loading() = Loading;
const factory Union.error([String message]) = ErrorDetails;
}
Then we could write:
var union = Union(42);
print(
union.maybeWhen(
null, // ignore the default case
loading: () => 'loading',
// did not specify an `error` callback
orElse: () => 'fallback',
),
); // fallback
This is equivalent to:
var union = Union(42);
String label;
if (union is Loading) {
label = 'loading';
} else {
label = 'fallback';
}
But it is safer as you are forced to handle the fallback case, and it is easier to write.
The map and maybeMap methods are equivalent to when/maybeWhen, but without destructuring.
Consider this class:
@freezed
class Model with _$Model {
factory Model.first(String a) = First;
factory Model.second(int b, bool c) = Second;
}
With such class, while when will be:
var model = Model.first('42');
print(
model.when(
first: (String a) => 'first $a',
second: (int b, bool c) => 'second $b $c'
),
); // first 42
map will instead be:
var model = Model.first('42');
print(
model.map(
first: (First value) => 'first ${value.a}',
second: (Second value) => 'second ${value.b} ${value.c}'
),
); // first 42
This can be useful if you want to do complex operations, like copyWith/toString for example:
var model = Model.second(42, false)
print(
model.map(
first: (value) => value,
second: (value) => value.copyWith(c: true),
)
); // Model.second(b: 42, c: true)
While Freezed will not generate your typical fromJson/toJson by itself, it knowns what json_serializable is.
Making a class compatible with json_serializable is very straightforward.
Consider this snippet:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'model.freezed.dart';
@freezed
class Model with _$Model {
factory Model.first(String a) = First;
factory Model.second(int b, bool c) = Second;
}
The changes necessary to make it compatible with json_serializable consists of two lines:
The end result is:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'model.freezed.dart';
part 'model.g.dart';
@freezed
class Model with _$Model {
factory Model.first(String a) = First;
factory Model.second(int b, bool c) = Second;
factory Model.fromJson(Map<String, dynamic> json) => _$ModelFromJson(json);
}
Don't forget to add json_serializable to your pubspec.yaml:
dev_dependencies:
json_serializable:
That's it!
With these changes, Freezed will automatically ask json_serializable to generate all the necessary fromJson/toJson.
Note:
Freezed will only generate a fromJson if the factory is using =>.
For classes with multiple constructors, Freezed will check the JSON response for a string element called runtimeType and choose the constructor to use based on its value. For example, given the following constructors:
@freezed
class MyResponse with _$MyResponse {
const factory MyResponse(String a) = MyResponseData;
const factory MyResponse.special(String a, int b) = MyResponseSpecial;
const factory MyResponse.error(String message) = MyResponseError;
factory MyResponse.fromJson(Map<String, dynamic> json) => _$MyResponseFromJson(json);
}
Then Freezed will use each JSON object's runtimeType to choose the constructor as follows:
[
{
"runtimeType": "default",
"a": "This JSON object will use constructor MyResponse()"
},
{
"runtimeType": "special",
"a": "This JSON object will use constructor MyResponse.special()",
"b": 42
},
{
"runtimeType": "error",
"message": "This JSON object will use constructor MyResponse.error()"
}
]
You can customize key and value with something different using @Freezed and @FreezedUnionValue decorators:
@Freezed(unionKey: 'type', unionValueCase: FreezedUnionCase.pascal)
abstract class MyResponse with _$MyResponse {
const factory MyResponse(String a) = MyResponseData;
@FreezedUnionValue('SpecialCase')
const factory MyResponse.special(String a, int b) = MyResponseSpecial;
const factory MyResponse.error(String message) = MyResponseError;
// ...
}
which would update the previous json to:
[
{
"type": "Default",
"a": "This JSON object will use constructor MyResponse()"
},
{
"type": "SpecialCase",
"a": "This JSON object will use constructor MyResponse.special()",
"b": 42
},
{
"type": "Error",
"message": "This JSON object will use constructor MyResponse.error()"
}
]
If you want to customize key and value for all the classes, you can specify it inside your build.yaml file, for example:
targets:
$default:
builders:
freezed:
options:
union_key: type
union_value_case: pascal
If you don't control the JSON response, then you can implement a custom converter. Your custom converter will need to implement its own logic for determining which constructor to use.
class MyResponseConverter implements JsonConverter<MyResponse, Map<String, dynamic>> {
const MyResponseConverter();
@override
MyResponse fromJson(Map<String, dynamic> json) {
if (json == null) {
return null;
}
// type data was already set (e.g. because we serialized it ourselves)
if (json['runtimeType'] != null) {
return MyResponse.fromJson(json);
}
// you need to find some condition to know which type it is. e.g. check the presence of some field in the json
if (isTypeData) {
return MyResponseData.fromJson(json);
} else if (isTypeSpecial) {
return MyResponseSpecial.fromJson(json);
} else if (isTypeError) {
return MyResponseError.fromJson(json);
} else {
throw Exception('Could not determine the constructor for mapping from JSON');
}
}
@override
Map<String, dynamic> toJson(MyResponse data) => data.toJson();
}
To then apply your custom converter pass the decorator to a constructor parameter.
@freezed
class MyModel with _$MyModel {
const factory MyModel(@MyResponseConverter() MyResponse myResponse) = MyModelData;
factory MyModel.fromJson(Map<String, dynamic> json) => _$MyModelFromJson(json);
}
By doing this, json serializable will use MyResponseConverter.fromJson() and MyResponseConverter.toJson() to convert MyResponse.
You can also use a custom converter on a constructor parameter contained in a List.
@freezed
class MyModel with _$MyModel {
const factory MyModel(@MyResponseConverter() List<MyResponse> myResponse) = MyModelData;
factory MyModel.fromJson(Map<String, dynamic> json) => _$MyModelFromJson(json);
}
Note:
In order to serialize nested lists of freezed objects, you are supposed to either specify a @JsonSerializable(explicitToJson: true) or change explicit_to_json inside your build.yaml file (see the documentation).
What about @JsonKey annotation?
All decorators passed to a constructor parameter are "copy-pasted" to the generated property too.
As such, you can write:
@freezed
class Example with _$Example {
factory Example(@JsonKey(name: 'my_property') String myProperty) = _Example;
factory Example.fromJson(Map<String, dynamic> json) => _$ExampleFromJson(json);
}
What about @JsonSerializable annotation?
You can pass @JsonSerializable annotation by placing it over constructor e.g.:
@freezed
class Example with _$Example {
@JsonSerializable(explicitToJson: true)
factory Example(@JsonKey(name: 'my_property') SomeOtherClass myProperty) = _Example;
factory Example.fromJson(Map<String, dynamic> json) => _$ExampleFromJson(json);
}
If you want to define some custom json_serializable flags for all the classes (e.g. explicit_to_json or any_map) you can do it via build.yaml file as described here.
See also the decorators section
Utilities
The Freezed extension might help you work faster with freezed. For example :
You can get Live Templates for boiler plate code here.
Example:
factory Demo.fromJson(Map<String, dynamic> json) => _$DemoFromJson(json);
@freezed
abstract class Demo with _$Demo {
}
Person person;
person.copyWith(gender: Gender.something); // gender is deprecated
Person person;
print(person.gender); // gender is deprecated
_Person(gender: Gender.something); // gender is deprecated
Person(gender: Gender.something); // gender is deprecated
person.rebuild((person) {
return person
..b = person;
})
person.copyWith(location: null)
MyClass copyWith({ int? a, String? b }) {
return MyClass(a: a ?? this.a, b: b ?? this.b);
}
Run this command:
With Dart:
$ dart pub add freezed
This will add a line like this to your package's pubspec.yaml (and run an implicit dart pub get):
dependencies:
freezed: ^0.14.5
Alternatively, your editor might support dart pub get. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:freezed/builder.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'main.freezed.dart';
part 'main.g.dart';
@freezed
class MyClass with _$MyClass {
factory MyClass({String? a, int? b}) = _MyClass;
}
@freezed
class Union with _$Union {
const factory Union(int value) = Data;
const factory Union.loading() = Loading;
const factory Union.error([String? message]) = ErrorDetails;
const factory Union.complex(int a, String b) = Complex;
factory Union.fromJson(Map<String, Object> json) => _$UnionFromJson(json);
}
@freezed
class SharedProperty with _$SharedProperty {
factory SharedProperty.person({
String? name,
int? age,
}) = SharedProperty0;
factory SharedProperty.city({
String? name,
int? population,
}) = SharedProperty1;
}
void main() {
final myClassexample = MyClass(a: '42', b: 42);
// clone
print(myClassexample.copyWith(a: null)); // MyClass(a: null, b: 42)
print(myClassexample.copyWith()); // MyClass(a: '42', b: 42)
// ------------------
// == override
print(MyClass(a: '42', b: 42) == MyClass(a: '42', b: 42)); // true
print(MyClass(a: '42', b: 42) == MyClass()); // false
// ------------------
// destructuring pattern-matching
const unionExample = Union(42);
print(
// `when` requires all callbacks to be not null
unionExample.when(
(value) => '$value',
loading: () => 'loading',
error: (message) => 'Error: $message',
complex: (a, b) => 'complex $a $b',
),
); // 42
print(
// maybeWhen allows some callbacks to be missing, but requires an `orElse` callback
unionExample.maybeWhen(
null,
loading: () => 'loading',
// voluntarily didn't pass error/complex callbacks
orElse: () => 42,
),
); // 42
// ------------------
// non-destructuring pattern-matching
// works the same as `when`, but the callback is slightly different
print(
// `map` requires all callbacks to be not null
unionExample.map(
(Data value) => '$value',
loading: (Loading value) => 'loading',
error: (ErrorDetails error) => 'Error: ${error.message}',
complex: (Complex value) => 'complex ${value.a} ${value.b}',
),
); // 42
print(
// maybeWhen allows some callbacks to be missing, but requires an `orElse` callback
unionExample.maybeMap(
null,
error: (ErrorDetails value) => value.message,
// voluntarily didn't pass error/complex callbacks
orElse: () => 'fallthrough',
),
); // fallthrough
// ------------------
// nice toString
print(const Union(42)); // Union(value: 42)
print(const Union.loading()); // Union.loading()
print(const Union.error(
'Failed to fetch')); // Union.error(message: Failed to fetch)
// ------------------
// shared properties between union possibilities
var example = SharedProperty.person(name: 'Remi', age: 24);
// OK, `name` is shared between both .person and .city constructor
print(example.name); // Remi
example = SharedProperty.city(name: 'London', population: 8900000);
print(example.name); // London
// COMPILE ERROR
// print(example.age);
// print(example.population);
}
Author: rrousselGit
Official Website: https://github.com/rrousselGit/freezed