freezed 0.1.0

  • Readme
  • Changelog
  • Example
  • Installing
  • 94

pub package

Welcome to Freezed, yet another code generator for unions/pattern-patching/copy.

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 in feature, or they have very advanced features but with a complex syntax.

A typical example would be a "clone" method.
Current generators have two approaches:

  • a copyWith, usually implemented using ??:

    MyClass copyWith({ int a, String b }) {
        return MyClass(a: a ?? this.a, b: b ?? this.b);
    }
    

    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:

    person.copyWith(location: null)
    
  • a builder method combined with a temporary mutable object, usually used this way:

    person.rebuild((person) {
      return person
        ..b = person;
    })
    

    The benefits of this approach is 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 a 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 #

Install #

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
dev_dependencies:
  build_runner:
  freezed:

As opposed to other code-generators, Freezed does not have custom annotations.
Instead it uses @immutable from meta, which comes built-in Flutter (but can be installed separatly if needed).

Run the generator #

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 may either start with:

import 'package:flutter/foundation.dart';

part 'my_file.freezed.dart';

or:

import 'package:meta/meta.dart';

part 'my_file.freezed.dart';

PREFER: importing package:flutter/foundation.dart over package:meta/meta.dart.
The reason being, importing foundation.dart also imports the necessary classes to make an object nicely readable in Flutter's devtool.
If you import foundation.dart, Freezed will automatically them.

A full example would be:

// main.dart
import 'package:flutter/foundation.dart';

part 'main.freezed.dart';

@immutable
abstract class Union with _$Union {
  const factory Union(int value) = Data;
  const factory Union.loading() = Loading;
  const factory Union.error([String message]) = ErrorDetails;
}

From there, to run the code-generator, you have two possibilities:

  • flutter pub pub run build_runner build, if your package depends on Flutter
  • pub run build_runner build otherwise

The features #

The syntax #

Freezed works differently than most generators. To define a class using Freezed, you will not declare properties but factory constructors instead.

For example, if you want to define a Person class, which has 2 properties:

  • name, a String
  • age, an int

To do so, you will have to define a factory constructor that takes these properties as parameter:

@immutable
abstract 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 syntax are supported. As such you could write:

@immutable
abstract class Person with _$Person {
  factory Person(String name, int age) = _Person;
}

Person('Remi', 24)
@immutable
abstract class Person with _$Person {
  const factory Person(String name, {int age = 42}) = _Person;
}

Person('Remi', age: 24)

...

You are also not limited to one constructor and non-generic class.
From example, you should write:

@immutable
abstract 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.

==/toString #

When using Freezed, the toString, hashCode and == methods are overriden as you would expect:

@immutable
abstract 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
}

copyWith #

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:

@immutable
abstract 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: Person, age: null)

Notice how copyWith correctly was able to understand null parameters.

Unions/Sealed classes #

Coming from other languages, you may be used with features like "tagged union types" / sealed clases / pattern matching.
These are powerful tool 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:

@immutable
abstract 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.

Shared properties #

When defining multiple constructors, you will loose the ability to do read properties that are not common to all constructors:

For example, if you write:

@immutable
abstract 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
}

When #

The when method is the equivalent to pattern matching with destructing.
Its prototype depends on the constructors defined.

For example, with:

@immutable
abstract 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:

@immutable
abstract 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.

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 a fallback behavior.

As such, using:

@immutable
abstract 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.

Map/MaybeMap #

The map and maybeMap methods are equivalent to when/maybeWhen, but without destructuring.

Consider this class:

@immutable
abstract 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)

FromJson/ToJson #

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:flutter/foundation.dart';

part 'model.freezed.dart';

@immutable
abstract 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 three lines:

  • a new import: import 'package:json_annotation/json_annotation.dart'
  • a new part: part 'model.g.dart';
  • a new constructor on the desired targeted: factory Model.fromJson(Map<String, dynamic> json) => _$ModelFromJson(json);

The end result is:

import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';

part 'model.freezed.dart';
part 'model.g.dart';

@immutable
abstract 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);
}

That's it!
With these changes, Freezed will automatically ask json_serializable to generate all the necessary fromJson/toJson.

Then, for classes with multiple constructors, Freezed will take care of deciding which constructor should be used.

Roadmap #

  • support for properties shared between multiple constructors but with a different type.
  • default variable support on the custom generated constructors.

0.1.0 #

Add support for json_serializable

0.0.2 #

Implicitly generate debugFillProperties if the necessary classes are imported.

0.0.1 #

Add generic support

0.0.0 #

Initial release

example/lib/main.dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

part 'main.freezed.dart';

@immutable
abstract class MyClass with _$MyClass {
  factory MyClass({String a, int b}) = _MyClass;
}

@immutable
abstract 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;
}

@immutable
abstract 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);
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  freezed: ^0.1.0

2. Install it

You can install packages from the command line:

with pub:


$ pub get

Alternatively, your editor might support pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:freezed/builder.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
88
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
94
Learn more about scoring.

We analyzed this package on Mar 25, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.7.1
  • pana: 0.13.6

Health suggestions

Format lib/src/generator.dart.

Run dartfmt to format lib/src/generator.dart.

Format lib/src/templates/abstract_template.dart.

Run dartfmt to format lib/src/templates/abstract_template.dart.

Format lib/src/templates/concrete_template.dart.

Run dartfmt to format lib/src/templates/concrete_template.dart.

Fix additional 3 files with analysis or formatting issues.

Additional issues in the following files:

  • lib/src/templates/from_json_template.dart (Run dartfmt to format lib/src/templates/from_json_template.dart.)
  • lib/src/templates/parameter_template.dart (Run dartfmt to format lib/src/templates/parameter_template.dart.)
  • lib/src/templates/prototypes.dart (Run dartfmt to format lib/src/templates/prototypes.dart.)

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.7.0 <3.0.0
analyzer ^0.39.0 0.39.4
build >=0.12.6 <2.0.0 1.2.2
build_config >=0.4.2 <1.0.0 0.4.2
meta ^1.1.0 1.1.8
source_gen ^0.9.0 0.9.5
Transitive dependencies
_fe_analyzer_shared 1.0.3
args 1.6.0
async 2.4.1
charcode 1.1.3
checked_yaml 1.0.2
collection 1.14.12
convert 2.1.1
crypto 2.1.4
csslib 0.16.1
dart_style 1.3.3
glob 1.2.0
html 0.14.0+3
js 0.6.1+1
logging 0.11.4
node_interop 1.0.3
node_io 1.0.1+2
package_config 1.9.2
path 1.6.4
pedantic 1.9.0
pub_semver 1.4.4
pubspec_parse 0.1.5
source_span 1.7.0
string_scanner 1.0.5
term_glyph 1.1.0
typed_data 1.1.6
watcher 0.9.7+14
yaml 2.2.0
Dev dependencies
build_runner ^1.7.4
build_test ^0.10.11
json_annotation any 3.0.1
json_serializable any
matcher ^0.12.6
source_gen_test ^0.1.0
test ^1.6.0