Guide: Dart/Flutter

Boost 🚀 your development of Dart/Flutter apps using a GraphQL API by generating models directly from your GraphQL Schema.

Currently, this plugin only generates Freezed models but work is ongoing to make it way easier to work with GraphQL by scaffolding an entire GraphQL client with support for Queries, Mutations and Subscriptions taking huge inspiration from KitQL

TL;DR

The flutter-freezed plugin generates Freezed models from a GraphQL Schema.

Motivation

Dart is awesome, but defining a “model” can be tedious. We may have to:

  • define a constructor + the properties
  • override toString, operator ==, hashCode
  • implement a copyWith method to clone the object
  • handling de/serialization

On top of that, Dart is also missing features such as union types and pattern-matching.

Implementing all of this can take hundreds of lines, which are error-prone and the readability of your model significantly.

Freezed tries to fix that by implementing most of this for you, allowing you to focus on the definition of your model. https://pub.dev/packages/freezed

Fortunately enough, GraphQL is strongly typed, and so is Dart. Save yourself from implementing a model to match your strongly typed GraphQL types, and let Freezed handle the work while you chill with this flutter-freezedplugin

Features

Currently, the plugin supports the following features

  • Generate Freezed classes for ObjectTypes
  • Generate Freezed classes for InputTypes
  • Support for EnumsTypes
  • Support for custom ScalarTypes
  • Support freeze documentation of class & properties from GraphQL SDL description comments
  • Ignore/don’t generate freezed classes for certain ObjectTypes
  • Support directives
  • Support deprecation annotation
  • Support for InterfaceTypes
  • Support for UnionTypes union/sealed classes
  • Merge InputTypes with ObjectType as union/sealed class union/sealed classes

TODO:

  • Support Queries, Mutations, and Subscription: make it way easier to use GraphQL in flutter without going through any complex process. Inspired by KitQL

Demo

Given the following GraphQL schema:

input RequestOTPInput {
  email: String
  phoneNumber: String
}
 
input VerifyOTPInput {
  email: String
  phoneNumber: String
  otpCode: String!
}
 
union AuthWithOTPInput = RequestOTPInput | VerifyOTPInput

Using the following config:

schema: demo-schema.graphql
generates:
  ./lib/data/models/app_models.dart:
    plugins:
      - flutter-freezed

This is the generated output:

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
 
part 'app_models.freezed.dart';
part 'app_models.g.dart';
 
@unfreezed
class RequestOtpInput with _$RequestOtpInput {
  const RequestOtpInput._();
 
  const factory RequestOtpInput({
    String? email,
    String? phoneNumber,
  }) = _RequestOtpInput;
 
  factory RequestOtpInput.fromJson(Map<String, dynamic> json) => _$RequestOtpInputFromJson(json);
}
 
@unfreezed
class VerifyOtpInput with _$VerifyOtpInput {
  const VerifyOtpInput._();
 
  const factory VerifyOtpInput({
    String? email,
    String? phoneNumber,
    required String otpCode,
  }) = _VerifyOtpInput;
 
  factory VerifyOtpInput.fromJson(Map<String, dynamic> json) => _$VerifyOtpInputFromJson(json);
}
 
@freezed
class AuthWithOtpInput with _$AuthWithOtpInput {
  const AuthWithOtpInput._();
 
  const factory AuthWithOtpInput.requestOtpInput({
    String? email,
    String? phoneNumber,
  }) = RequestOtpInput;
 
  const factory AuthWithOtpInput.verifyOtpInput({
    String? email,
    String? phoneNumber,
    required String otpCode,
  }) = VerifyOtpInput;
 
  factory AuthWithOtpInput.fromJson(Map<String, dynamic> json) => _$AuthWithOtpInputFromJson(json);
}

Getting started

To get started, make sure you have the following installed:

  • Node.js (10 or later)
  • NPM or Yarn

Follow the Installation Guide for more details on getting started with GraphQL Code Generator

Inside your Flutter project root folder:

  1. Install freezed in your flutter project

  2. Install json_serializable in your flutter project

  3. Download your GraphQL schema in graphql format and place it at the root of your Flutter project using a tool like get-graphql-schema

npm install -g get-graphql-schema
 
get-graphql-schema https://your-graphql-endpoint > schema.graphql
  1. Add the following to the .gitignore file:
# graphql-code-generator related
node_modules/
  1. Create a node project with npm init -y and add a script to run the generator:
{
  "scripts": {
    "generate": "graphql-codegen"
  }
}
  1. Install the graphql-code-generator and the flutter-freezed plugin
npm i graphql
npm i -D typescript @graphql-codegen/cli @graphql-codegen/flutter-freezed
  1. Create a codegen.ts file at the root of the Flutter project with the following:
import type { CodegenConfig } from '@graphql-codegen/cli'
 
const config: CodegenConfig = {
  generates: {
    'lib/data/models/app_models.dart': {
      plugins: {
        'flutter-freezed': {}
      }
    }
  }
}
export default config
  1. Generate your freezed models with the following command and chill 🍻:
npm run generate

Configuring the plugin

To configure the plugin, you need to first understand how to use Patterns to configure specific GraphQL Types and its fields and also apply a config option globally to all GraphQL Types and fields.

This plugin is heavily documented so please take a look into the tests directory to learn more.

Also, understanding how the generated output is identified helps in granular configuration

Using the schema below:

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}
 
type Actor {
  name: String!
  appearsIn: [Episode]!
}
 
type Starship {
  id: ID!
  name: String!
  length: Float
}
 
interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}
 
type Human implements Character {
  id: ID!
  name: String!
  friends: [Actor]
  appearsIn: [Episode]!
  totalCredits: Int
}
 
type Droid implements Character {
  id: ID!
  name: String!
  friends: [Actor]
  appearsIn: [Episode]!
  primaryFunction: String
}
 
union SearchResult = Human | Droid | Starship

With the following configuration:

import type { CodegenConfig } from '@graphql-codegen/cli'
 
const config: CodegenConfig = {
  // ...
  generates: {
    'lib/data/models/app_models.dart': {
      plugins: {
        'flutter-freezed': Config.create({
          defaultValues: [
            [FieldNamePattern.forFieldNamesOfAllTypeNames([friends]), '[]', ['union_factory_parameter']],
            [FieldNamePattern.forFieldNamesOfAllTypeNames([appearsIn]), '[]', ['default_factory_parameter']]
          ],
          deprecated: [
            [FieldNamePattern.forAllFieldNamesOfTypeName([Actor]), ['default_factory_parameter']],
            [TypeNamePattern.forTypeNames(SearchResultDroid), ['union_factory']]
          ],
          final: [[FieldNamePattern.forFieldNamesOfAllTypeNames([id, name]), ['parameter']]],
          mergeTypes: {
            Human: ['Actor'],
            Actor: ['Human']
          },
          immutable: TypeNamePattern.forAllTypeNamesExcludeTypeNames([Actor, Human])
        })
      }
    }
  }
}

Generates output below:

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
 
part 'app_models.freezed.dart';
part 'app_models.g.dart';
 
enum Episode { // @1
  @JsonKey(name: 'NEWHOPE')
  newhope // @1.i
  @JsonKey(name: 'EMPIRE')
  empire // @1.ii
  @JsonKey(name: 'JEDI')
  jedi // @1.iii
}
 
@unfreezed
class Actor with _$Actor { // @2
  const Actor._();
 
  factory Actor({ // @2.a
    @deprecated
    required final String name, // @2.a.i
    @deprecated
    @Default([])
    required List<Episode?> appearsIn, // @2.a.ii
  }) = _Actor;
 
  const factory Actor.human({ // @2.b
    required final String id, // @2.b.i
    required final String name, // @2.b.ii
    List<Actor?>? friends, // @2.b.iii
    required List<Episode?> appearsIn, // @2.b.iv
    int? totalCredits, // @2.b.v
  }) = Human; // @2.b.1
 
  factory Actor.fromJson(Map<String, dynamic> json) => _$ActorFromJson(json);
}
 
@freezed
class Starship with _$Starship { // @3
  const Starship._();
 
  const factory Starship({ // @3.a
    required final String id, // @3.a.i
    required final String name, // @3.a.ii
    double? length, // @3.a.iii
  }) = _Starship;
 
  factory Starship.fromJson(Map<String, dynamic> json) => _$StarshipFromJson(json);
}
 
@unfreezed
class Human with _$Human { // @4
  const Human._();
 
  factory Human({ // @4.a
    required final String id, // @4.a.i
    required final String name, // @4.a.ii
    List<Actor?>? friends, // @4.a.iii
    @Default([])
    required List<Episode?> appearsIn, // @4.a.iv
    int? totalCredits, // @4.a.v
  }) = _Human;
 
  const factory Human.actor({ // @4.b
    required final String name, // @4.b.i
    required List<Episode?> appearsIn, // @4.b.ii
  }) = Actor; // @4.b.1
 
  factory Human.fromJson(Map<String, dynamic> json) => _$HumanFromJson(json);
}
 
@freezed
class Droid with _$Droid { // @5
  const Droid._();
 
  const factory Droid({, // @5.a
    required final String id, // @5.a.i
    required final String name, // @5.a.ii
    List<Actor?>? friends, // @5.a.iii
    @Default([])
    required List<Episode?> appearsIn, // @5.a.iv
    String? primaryFunction, // @5.a.v
  }) = _Droid;
 
  factory Droid.fromJson(Map<String, dynamic> json) => _$DroidFromJson(json);
}
 
@freezed
class SearchResult with _$SearchResult { // @6
  const SearchResult._(); // @6.1
 
  const factory SearchResult.human({ // @6.a
    required final String id, // @6.a.i
    required final String name, // @6.a.ii
    @Default([])
    List<Actor?>? friends, // @6.a.iii
    required List<Episode?> appearsIn, // @6.a.iv
    int? totalCredits,// @6.a.v
  }) = Human; // @6.a.1
 
  @deprecated
  const factory SearchResult.droid({ // @6.b
    required final String id, // @6.b.i
    required final String name, // @6.b.ii
    @Default([])
    List<Actor?>? friends, // @6.b.iii
    required List<Episode?> appearsIn, // @6.b.iv
    String? primaryFunction, // @6.b.v
  }) = Droid; // @6.a.2
 
  const factory SearchResult.starship({ // @6.c
    required final String id, // @6.c.i
    required final String name, // @6.c.ii
    double? length, // @6.c.iii
  }) = Starship; // @6.a.3
 
  factory SearchResult.fromJson(Map<String, dynamic> json) => _$SearchResultFromJson(json);
}

Identifying the building blocks

The generated output consists of several blocks enabling you to specify a configuration targetting a specific block.

All APPLIES_ON_* values are exported from the plugin-config module of this package.

@1: is the enum block. Use APPLIES_ON_ENUM to configure this block.

@1.i to @1.iii makes up the enum_value block. Use APPLIES_ON_ENUM_VALUE to configure this block

@2 to @6 are the class blocks. Use APPLIES_ON_CLASS to configure these blocks.

There are 2 types of factory constructors in each class:

  1. The Default Factory Constructor: Created automatically for each GraphQL Type. Use APPLIES_ON_DEFAULT_FACTORY to configure these blocks. @2.a to @5.a are the default factory constructors.

  2. The Named Factory: There are 2 types of named factory constructors:

  • Merged Factory Constructors: created manually by merging two different GraphQL Types in the config. See config.mergeTypes above. Use APPLIES_ON_MERGED_FACTORY to configure these block @2.b and @4.b are the merged factory constructors.

  • Union Factory Constructors: created automatically from a GraphQL Union Type. Each GraphQL Type in the Union is generated as a named factory in the class of the GraphQL Union Type. Use APPLIES_ON_UNION_FACTORY to configure these factories @6.a, @6.b and @6.c are the merged factory constructors.

Use APPLIES_ON_NAMED_FACTORY to configure both merged and union factories.

Use APPLIES_ON_FACTORY to configure all factories.

Each class block has exactly one default factory constructor and maybe one or more named factory constructors.

The fields of the GraphQL Type are generated as parameters to the factory constructors:

There are 3 types of parameters are generated depending on the type of factory constructors:

  1. Parameters found in the default_factory are called default_factory_parameter. Use APPLIES_ON_DEFAULT_FACTORY_PARAMETERS to configure these parameters The following are all default factory parameters:
  • @2.a.i to @2.a.iii
  • @3.a.i to @3.a.iii
  • @4.a.i to @4.a.v
  • @5.a.i to @5.a.v
  1. Parameters found on the merged_factory are called merged_factory_parameter. Use APPLIES_ON_MERGED_FACTORY_PARAMETERS to configure these parameters The following are all merged factory parameters:
  • @2.b.i to @2.b.v
  • @4.b.i and @4.b.ii
  1. Parameters found in the union_factory are called union_factory_parameter. Use APPLIES_ON_UNION_FACTORY_PARAMETERS to configure these parameters. The following are all merged factory parameters:
  • @6.a.i to @6.a.v
  • @6.b.i to @6.b.v
  • @6.c.i to @6.c.iii

Use APPLIES_ON_NAMED_FACTORY_PARAMETERS to configure both merged and union factor parameters

Use APPLIES_ON_PARAMETERS to configure all parameters

Patterns

A compact string of patterns used in the config for granular configuration for each Graphql Type and/or its fieldNames

The string can contain more than one pattern, each pattern ends with a semi-colon (;).

A dot (.) separates the TypeName from the FieldNames in each pattern

To apply an option to all Graphql Types or fields, use the allTypeNames (@*TypeNames) and allFieldNames (@*FieldNames) tokens respectively

Wherever you use the allTypeNames and the allFieldNames, know very well that you can make some exceptions. After all, to every rule, there is an exception

A square bracket ([]) is used to specify what should be included and a negated square bracket (-[]) is used to specify what should be excluded

Manually typing out a pattern may be prone to typos and invalid patterns therefore the TypeFieldName class exports some builder methods which you can use in your plugin config file.

The patterns themselves are readable and easy to manually type it out in the config but its RECOMMENDED that you the builder methods. However, along with builder methods, the TypeFieldName class also exports the Regular Expression(RegExp) used to test the patterns for a match as well as matcher methods. You can use these to find out if you manually typed out patterns would work with this plugin.

Usage for Graphql Types

Configuring specific Graphql Types

You can explicitly list out the names of the Graphql Types that you want to configure.

const pattern = Pattern.forTypeNames([Droid, Starship])
console.log(pattern) // "Droid;Starship;"

Configuring all Graphql Types

Instead of manually listing out all the types in the Graphql Schema, use the allTypeNames (@*TypeNames) to configure all the Graphql Types in the Schema

const pattern = Pattern.forAllTypeNames()
console.log(pattern) // "@*TypeNames;"

Configuring all Graphql Types except those specified in the exclusion list of TypeNames

You can configure all GraphQL Types except those specified.

The example below configures all the Graphql Types in the Schema except the Droid and Starship Graphql Types

const pattern = Pattern.forAllTypeNamesExcludeTypeNames([Droid, Starship])
console.log(pattern) // "@*TypeNames-[Droid,Starship];"

Usage for fields of Graphql Types

Configuring specific fields of a specific Graphql Type

You can explicitly list out the names of the fields of the Graphql Types that you want to configure.

const pattern = Pattern.forFieldNamesOfTypeName([
  [Droid, [id, name, friends]],
  [Human, [id, name, title]],
  [Starship, [name, length]]
])
console.log(pattern) // "Droid.[id,name,friends];Human.[id,name,title];Starship.[name,length];"

Configuring all fields of a specific Graphql Type

Instead of manually listing out all the fields of the Graphql Type, use the allFieldNames (@*FieldNames) to configure all the fields of the Graphql Type.

const pattern = Pattern.forAllFieldNamesOfTypeName([Droid, Movie])
console.log(pattern) // "Droid.@*FieldNames;Movie.@*FieldNames;"

Configuring all fields except those specified in the exclusion list of FieldNames for a specific GraphQL Type

In the example below, the id and the name fields will be excluded from the configuration while all the remaining fields of the Droid Graphql Type will be configured

const pattern = Pattern.forAllFieldNamesExcludeFieldNamesOfTypeName([
  [Droid, [id, name, friends]],
  [Human, [id, name, title]],
  [Starship, [name, length]]
])
console.log(pattern) // "Droid.@*FieldNames-[id,name,friends];Human.@*FieldNames-[id,name,title];Starship.@*FieldNames-[name,length];"

Configuring specific fields of all Graphql Types

When you use the allTypeNames (@*TypeNames), you can specify the fields to be configured. If field name that doesn’t exists for a given Graphql Type, it would simply be ignored.

The example below configures the id and name fields of all Graphql Types

const pattern = Pattern.forFieldNamesOfAllTypeNames([id, name, friends])
console.log(pattern) // "@*TypeNames.[id,name,friends];"

Configuring all fields of all Graphql Types

Using the allFieldNames (@*FieldNames) on the allTypeNames (@*TypeNames), you can configure all fields of all the Graphql Types in the Schema

const pattern = Pattern.forAllFieldNamesOfAllTypeNames()
console.log(pattern) // "@*TypeNames.@*FieldNames;"

Configuring all fields except those specified in the exclusion list of FieldNames for all GraphQL Types

As always, you can make some exception when you use the allFieldNames (@*FieldNames) to except some fields from the configuration.

In the example below, the id and the name fields will be excluded from the configuration while all the remaining fields of all Graphql Type will be configured

const pattern = Pattern.forAllFieldNamesExcludeFieldNamesOfAllTypeNames([id, name, friends])
console.log(pattern) // "@*TypeNames.@*FieldNames-[id,name,friends];"

Configuring specific fields of all GraphQL Types except those specified in the exclusion list of TypeNames

In the example below, the id and name fields will be configured for all the Graphql Types in the Schema exceptDroid and Starship

const pattern = Pattern.forFieldNamesOfAllTypeNamesExcludeTypeNames([Droid, Human], [id, name, friends])
console.log(pattern) // "@*TypeNames-[Droid,Human].[id,name,friends];"

Configuring all fields of all GraphQL Types except those specified in the exclusion list of TypeNames

In the example below, all fields of all Graphql Types in the Schema except for the fields of Droid and Starship will be excluded from the configuration while all the remaining fields of all Graphql Type will be configured

 * const pattern = Pattern.forAllFieldNamesOfAllTypeNamesExcludeTypeNames([Droid, Human]);
 * console.log(pattern); // "@*TypeNames-[Droid,Human].@*FieldNames;"

Configuring all fields except those specified in the exclusion list of FieldNames of all GraphQL Types except those specified in the exclusion list of TypeNames

In the example below, the id and the name fields of Droid or Starship will be excluded from the configuration while all the remaining fields of all Graphql Types(including Droid and Starship) will be configured

const pattern = Pattern.forAllFieldNamesExcludeFieldNamesOfAllTypeNamesExcludeTypeNames(
  [Droid, Human],
  [id, name, friends]
)
console.log(pattern) // "@*TypeNames-[Droid,Human].@*FieldNames-[id,name,friends];"

PRs are welcomed

This started as a plugin but eventually we hope to make it way easier to use GraphQL in your Flutter apps.

For more advanced configuration, please refer to the plugin documentation.

For a different organization of the generated files, please refer to the “Generated files colocation” page.