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 (opens in a new tab) 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 (opens in a new tab)
TL;DR
The flutter-freezed
plugin generates Freezed (opens in a new tab) 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 (opens in a new tab)
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 (opens in a new tab) handle the work while you chill with this flutter-freezed
plugin
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 (opens in a new tab)
- Merge InputTypes with ObjectType as union/sealed class union/sealed classes (opens in a new tab)
TODO:
- Support Queries, Mutations, and Subscription: make it way easier to use GraphQL in flutter without going through any complex process. Inspired by KitQL (opens in a new tab)
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:
-
Install freezed (opens in a new tab) in your flutter project
-
Install json_serializable (opens in a new tab) in your flutter project
-
Download your GraphQL schema in graphql format and place it at the root of your Flutter project using a tool like get-graphql-schema (opens in a new tab)
npm install -g get-graphql-schema
get-graphql-schema https://your-graphql-endpoint > schema.graphql
- Add the following to the
.gitignore
file:
# graphql-code-generator related
node_modules/
- Create a node project with
npm init -y
and add a script to run the generator:
{
"scripts": {
"generate": "graphql-codegen"
}
}
- Install the
graphql-code-generator
and theflutter-freezed
plugin
pnpm add graphql
pnpm add -D typescript @graphql-codegen/cli @graphql-codegen/flutter-freezed
- 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
- 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 theplugin-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:
-
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. -
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. UseAPPLIES_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:
- Parameters found in the default_factory are called
default_factory_parameter
. UseAPPLIES_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
- Parameters found on the merged_factory are called
merged_factory_parameter
. UseAPPLIES_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
- Parameters found in the union_factory are called
union_factory_parameter
. UseAPPLIES_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.