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 | VerifyOTPInputUsing the following config:
schema: demo-schema.graphql
generates:
./lib/data/models/app_models.dart:
plugins:
- flutter-freezedThis 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 in your flutter project
-
Install json_serializable 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
npm install -g get-graphql-schema
get-graphql-schema https://your-graphql-endpoint > schema.graphql- Add the following to the
.gitignorefile:
# graphql-code-generator related
node_modules/- Create a node project with
npm init -yand add a script to run the generator:
{
"scripts": {
"generate": "graphql-codegen"
}
}- Install the
graphql-code-generatorand theflutter-freezedplugin
npm i graphql
npm i -D typescript @graphql-codegen/cli @graphql-codegen/flutter-freezed- Create a
codegen.tsfile 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 generateConfiguring 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 | StarshipWith 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-configmodule 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_FACTORYto configure these blocks.@2.ato@5.aare 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.mergeTypesabove. UseAPPLIES_ON_MERGED_FACTORYto configure these block@2.band@4.bare 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_FACTORYto configure these factories@6.a,@6.band@6.care 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
classblock 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_PARAMETERSto configure these parameters The following are all default factory parameters:
@2.a.ito@2.a.iii@3.a.ito@3.a.iii@4.a.ito@4.a.v@5.a.ito@5.a.v
- Parameters found on the merged_factory are called
merged_factory_parameter. UseAPPLIES_ON_MERGED_FACTORY_PARAMETERSto configure these parameters The following are all merged factory parameters:
@2.b.ito@2.b.v@4.b.iand@4.b.ii
- Parameters found in the union_factory are called
union_factory_parameter. UseAPPLIES_ON_UNION_FACTORY_PARAMETERSto configure these parameters. The following are all merged factory parameters:
@6.a.ito@6.a.v@6.b.ito@6.b.v@6.c.ito@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.