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?

vs
vs

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

TOOLING

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

What do we have in the
ecosystem
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