Unleashing the power of GraphQL in Flutter development
Tim Lavreniuk
About me
Solutions Architect @ Widgetbook
Graduated as a graphic designer
Work with frontend, backend and the cloud
What is GraphQL?
Client
Client
Data Service
Auth Service
Payments Service
Data Service
Auth Service
Payments Service
HTTP
HTTP
REST API
REST API
REST API
https://example.com/api/v3/users
https://example.com/api/v3/products
https://example.com/api/v3/auth
type Book {
id: ID!
name: String!
authors: [Author!]!
}
type Author {
id: ID!
name: String!
age: Int
}
Schema
Strongly typed
Declarative
Self-documenting
Can request main data in one query
API have one endpoint
Its a specification, not a technology
Centers around data
API is self-documented
Response will be in JSON format
API Client
Flutter?
Apollo Cache
Why cache?
- Reduce response time
- Reduce state management use
- Offline work
Data Fetching
Fetch policies
- cache-first
- cache-and-network
- network-only
- no-cache
- cache-only
Data Normalization
{
"data":{
"episode":{
"id":"1",
"__typename":"Episode",
"characters":[
{
"id":"1",
"__typename":"Character"
},
{
"id":"2",
"__typename":"Character"
}
]
}
}
}
{
"ROOT_QUERY":{
"__typename":"Query",
"episode":{
"__ref":"Episode:1"
}
},
"Episode:1":{
"__typename":"Episode",
"id":"1",
"characters":[
{
"__ref":"Character:1"
},
{
"__ref":"Character:2"
}
]
},
"Character:1":{
"__typename":"Character",
"id":"1"
},
"Character:2":{
"__typename":"Character",
"id":"2"
}
}
Automatic updates
Cache won’t automatically update for you:
Data can be merged and updated automatically:
You’re editing a single entity and returning the same type in response
You’re editing entities and returning all entries of the same type
Query response data isn’t related to the changes you want to happen
You’re unable to return an entire set of changed objects
The response data has an added or removed item in it
Fragments
Apollo Devtools
Apollo Links
- add Auth headers
- retry failed mutations
- deduping results
- error logging
Code generation
How to get a schema?
and many more...
Why did you show me a non-Flutter tool?
A "Taste of Power" trope
Preview of the powers and skills we will be acquiring later
Forza Motorsport 3
Forza Motorsport 3
type User @model @auth(rules: [{allow: public}]) {
id: ID!
email: String!
}
type GroceryList @model @auth(rules: [{allow: public}]) {
id: ID!
name: String
items: [GroceryItem] @hasMany(indexName: "byList", fields: ["id"])
creator: User @hasOne
}
type GroceryItem @model @auth(rules: [{allow: public}]) {
id: ID!
name: String!
listID: ID! @index(name: "byList")
tagID: ID @index(name: "byTag")
isCompleted: Boolean! @default(value: "false")
pictureKey: String
}
Amplify DataStore
Why Amplify DataStore?
- Codegeneration for data models
- Persistent on-device storage
- Sync data to cloud
void observeQuery() {
_stream = Amplify.DataStore.observeQuery(
GroceryItem.classType,
where: GroceryItem.ID.eq('1')
).listen((QuerySnapshot<GroceryItem> snapshot) {
setState(() {
_items = snapshot.items;
});
});
}
Future<void> updateItem() async {
final oldItem = (await Amplify.DataStore.query(
GroceryItem.classType,
where: GroceryItem.ID.eq('1'),
)).first;
final newItem = oldPost.copyWith(name: 'Updated name');
try {
await Amplify.DataStore.save(newItem);
} on DataStoreException catch (e) {
safePrint('Something went wrong updating model: ${e.message}');
}
}
What if I have my own GraphQL API?
Libraries
- Operation
- Code Generation
- Links
- Cache
- Fetching Policies
- Type Policies
Basic
Advance
const String queryLists = r'''
query GetHomeScreenData {
listGroceryLists {
items {
id
name
}
}
}
''';
final QueryOptions options = QueryOptions(
document: gql(queryLists)
);
final QueryResult result = await client.query(options);
Code generation
#import ../fragment/GroceryList.fragment.graphql
query GetHomeScreenData {
listGroceryItemTags {
items {
id
name
}
}
listGroceryLists {
items {
...Base_GroceryList
}
}
}
fragment Base_GroceryList on GroceryList {
id
name
}
#import ../fragment/GroceryList.fragment.graphql
mutation CreateList($name: String!) {
createGroceryList(input: {
name: $name
}) {
...Base_GroceryList
}
}
mutation DeleteList($id: ID!) {
deleteGroceryList(input: {
id: $id
}) {
...Base_GroceryList
}
}
mutation UpdateList($id: ID!, $name: String!) {
updateGroceryList(input: {
id: $id,
name: $name,
}) {
...Base_GroceryList
}
}
const documentNodeQueryGetHomeScreenData = DocumentNode(definitions: [
OperationDefinitionNode(
type: OperationType.query,
name: NameNode(value: 'GetHomeScreenData'),
variableDefinitions: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FieldNode(
name: NameNode(value: 'listGroceryItemTags'),
alias: null,
arguments: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FieldNode(
name: NameNode(value: 'items'),
alias: null,
arguments: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
final Query<Query$GetHomeScreenData> result = await client.query$GetHomeScreenData();
final Mutation<Mutation$CreateList> result = await client.mutation$CreateList();
final Mutation<Mutation$UpdateList> result = await client.mutation$UpdateList();
final Mutation<Mutation$DeleteList> result = await client.mutation$DeleteList();
List<Query$GetHomeScreenData$listGroceryItemTags$items>
query GetHomeScreenData {
listGroceryItemTags {
items {
id
name
}
}
}
typedef ItemTag = Query$GetHomeScreenData$listGroceryItemTags$items
List<ItemTag> items = ...
Fetch policies
- cache-first
- cache-and-network
- network-only
- no-cache
- cache-only
Links
Cache
InMemory
Cache storage
Persistent
final data = client.readFragment$Base_GroceryList(
idFields: { 'id': 1 },
);
final updatedFragment = fragment.copyWith(name: "new_name")
client.writeFragment$Base_GroceryList(updatedFragment);
Direct Cache access
var updateListMutation = useMutation$UpdateList(WidgetOptions$Mutation$UpdateList(
update: (cache, result) {
if (result.hasException) {
print(result.exception);
} else {
if (result.hasException) {
print(result.exception);
} else {
final updated = {
...repository,
...extractRepositoryData(result.data),
};
cache.writeFragment(
Fragment(
document: documentNodeFragmentBase_GroceryList).asRequest(idFields: {
'__typename': updated['__typename'],
'id': updated['id'],
}),
data: updated,
broadcast: false,
);
}
}
}
));
How to debug a cache?
Conclusions
- Code generation
- API Client
- Links
- Cache
- Advanced code generation
- Advanced cache
- Local state
- Developer tools
Thanks
Unleashing the power of GraphQL in Flutter development
By Timofey Lavrenyuk
Unleashing the power of GraphQL in Flutter development
- 211