Categories:
Loading... Search articles

Search for articles

Sorry, but we couldn't find any matches...

But perhaps we can interest you in one of our more popular articles?
Follow this step-by-step tutorial to learn about GraphQL with Flutter

How to implement GraphQL with Flutter + GraphQL example

Apr 11, 2022

Jahswill Essien explains in this article everything you need to know about GraphQL.

Have you ever faced the task of implementing a REST API and had to call multiple endpoints to populate data for a single screen? You probably wished you had more control over the data returned by the endpoint so that you could fetch more data with a single endpoint call or have only the necessary data fields returned by the call.

Follow along to see how you can achieve this with GraphQL. In this article, we’ll be implementing GraphQL in an existing codebase. We will discuss what GraphQL is, why we chose it, how to implement it with Flutter, and how to implement continuous integration/delivery in our completed app.

This article assumes you are familiar with the Flutter framework and have experience using REST APIs.

We’ll be working on a project called Music Mates, which has a few very simple functionalities:

  • Users are asked to select their favorite music artists from a predefined list during signup
  • Users should see fellow users who share one or more of their favorite music artists
  • Users should see a list of their selected favorite artists

Below is the final result of the application.

A demo of the final app

Setup

Run this command to clone the project from GitHub to your local machine:

git clone -b widget_design https://github.com/JasperEssien2/music_mates_app

Next, we need to have a proper understanding of the codebase. Let’s walk through the codebase together. Below is the folder structure of the application:

lib/
|- core
|  |_ constants.dart
|  |_ maths_mixins.dart
|
|- data
|  |- model
|  |  |_ artist.dart
|  |  |_ user_model.dart
|  |  |_ home_model.dart
|  |
|  |_ queries.dart
|  |_ data_export.dart
|
|- presentation
|  |- widgets
|  |  |_ error_widget.dart
|  |  |_ google_button.dart
|  |  |_ item_artist.dart
|  |  |_ item_mate.dart
|  |  |_ item_select_artist.dart
|  |  |_ loading_spinner.dart
|  |  |_ mates_ring.dart
|  |  |_ export.dart
|  |
|  |_ get_started_screen.dart
|  |_ home.dart
|  |_ select_favourite_artist.dart
|  |_ presentation_export.dart 
|
|_ main.dart

Folder structure of the application.

The utility classes used in the codebase are contained in the core folder. The constants.dart file contains defined spacing widgets for padding between widgets.

We will need some math functions for the adaptability of the ring widget that displays a user’s music mates. These are defined in the maths_mixin.dart file. Below is the code snippet for MathsMixin:

mixin MathsMixin {
 
 /// This method computes the radius of each individual circle in relation
 /// to the number of circles required and the radius of the large circle.
 /// This helps the circles in the ring to be adaptable, regardless of the number of circles.
  double radiusOfSmallCicleInRelationToLargeCircle(
          double largeRad, int numberOfCircles) =>
      largeRad * math.sin(math.pi / numberOfCircles);

  /// Gets the exterior angle of one small circle 
  double unitAngle(int numberOfPoints) => 360 / numberOfPoints;
}

extension MathsHelper on num {
  /// Converts a degree value to a radian value
  double get radian => this * math.pi / 180;
}

The data folder contains our model classes and queries.dart file. The queries.dart file contains a queries string document (more on this soon).

The presentation folder contains all of our widgets. The MatesRingWidget widget in the mates_ring.dart file is particularly interesting: It displays a user’s music mates in a ring pattern. I’ve added some helpful comments explaining how this was achieved — feel free to check out that file.

These are the dependencies that have been added at this time:

  • flutter_svgused to display .svg files
  • google_sign_infor implementing Google Sign-In

Now that we have an overview of the current codebase, our task is to implement GraphQL in this application. But what is GraphQL?

What is GraphQL?

GraphQL is a query language that serves as a contract between the server and the client. It uses its own defined schema, Schema Definition Language (SDL), which was developed by Facebook.

A schema can be thought of as a blueprint that describes the way data is organized. It’s worth noting that GraphQL is not a database technology. Rather, it’s just a language that specifies what data is needed and then sends the request from the client to the back end to resolve the query.

Why GraphQL?

GraphQL has several advantages over traditional REST APIs. This section covers why we chose this technology for this project.

Uses a single endpoint

One of GraphQL’s advantages is its use of a single endpoint as opposed to different endpoints to fetch various data. In comparison, using REST APIs for the Music Mates app can result in a total of about five different endpoints being used:

  • baseUrl/create-user
  • baseUrl/all-artists
  • baseUrl/users/<id>
  • baseUrl/users/<id>/favourite-artists
  • baseUrl/users/<id>/music-mates

On the other hand, when using GraphQL, we only need a single endpoint, say baseUrl/graphql. Weird, isn’t it? The question you’re probably be asking is, “How does GraphQL know what data to return?”

Well, GraphQL organizes data in terms of types and their fields or properties, rather than endpoints, to make sure that clients ask for only what is possible. The fields can be scalars (Int, Float, Boolean, String) or objects that contain properties of their own. An example of a GraphQL schema is shown below:

type Artist{
  id: ID!
  name: String!
  imageUrl: String
  description: String!
}

type User{
  id: ID!
  name: String!
  googleId: String!
  imageUrl: String
  favouriteArtists: [Artist!]
}

Above, we specify the type of schema, the field, and the data type of the field. The ! sign indicates that the field isn’t nullable. A list data type is specified by using square brackets [] and the list content data type.

Fixes the issue of under-fetching/over-fetching

One of the greatest strengths of the GraphQL technology is that it fixes under-fetching and over-fetching. To understand these concepts, take a look at the image below. What data is needed to populate the screen?

App home page

Dealing with under-fetching of data

Data on the user’s info, favorite artists, and music mates is needed to populate this screen, right? Using REST APIs, we would have to call an endpoint to fetch the user’s info, another endpoint to fetch their favorite artists, and another to fetch their music mates. This can be referred to as under-fetching because a call for data does not return sufficient data at that point of request.

On the other hand, using GraphQL, we can specify all the required data at once — look at the query below. (For now, don’t worry about trying to understand what’s going on, as this will be explained in detail later.)

 query UserInfo(\$googleId: String!){

    userInfo(googleId: \$googleId){
      id
      googleId
      name
      imageUrl
      favouriteArtists {
        id
        name
        imageUrl
        description
      }
    }
    
    musicMates(googleId: \$googleId){
      id
      googleId
      name
      imageUrl
    }
  }

You can retrieve all of the data, including nested data (as shown in the userInfo block querying favouriteArtists), with just one call. Isn’t that beautiful? That’s the power of GraphQL.

Dealing with over-fetching of data

Take a look at our home screen again. For the circle avatar widget that displays the user’s image, we need only the user’s image property from the data. For the artist item, we need just the artist’s name, image, and description properties.

With REST APIs, we have no control over what field should be returned. Instead, all of the data properties specified by the endpoint are returned. This is called over-fetching — fetching data properties that aren’t needed.

GraphQL gives us control over what field should be returned by the API. When we refactor the above query to suit our current widget’s needs, we have:

 query UserInfo(\$googleId: String!){

    userInfo(googleId: \$googleId){
      imageUrl
      favouriteArtists {
        name
        imageUrl
        description
      }
    }
    
    musicMates(googleId: \$googleId){
      imageUrl
    }
  }

Querying data with the above query returns only the data needed to satisfy our widget. Incredible, right?

GraphQL is faster

Due to GraphQL’s flexibility in regard to retrieving specific fields when querying data, it is faster than its counterpart, REST.

Built-in state management mechanism

The plugin we’ll use for GraphQL in our Flutter app has a state management mechanism built in, which we can use to update our UI based on the current state of the query.


Before implementing GraphQL, let’s set up other classes that will be helpful to us. Head to the lib/presentation folder and create a file called query_document_provider.dart. The goal here is to inject our queries document down the widget tree to be accessible to children widgets using InheritedWidget.

Before implementing QueriesDocumentProvider, it is important to understand what InheritedWidget is. It is a widget that propagates data down the widget tree. It utilizes BuildContext to achieve this.

BuildContext is a class in Flutter that keeps track of “this” widget location in the widget tree. Each widget has its own BuildContext, but a widget BuildContext only keeps track of its direct parent BuildContext.

When you call Scaffold.of(context) or Navigator.of(context), Flutter moves up the widget tree using the context and finds the nearest Scaffold or Navigator widget. Keep in mind that the widget BuildContext has a bottom-up relationship with other widgets’ BuildContext.

import 'package:flutter/scheduler.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:music_mates_app/data/data_export.dart';


class QueriesDocumentProvider extends InheritedWidget {
  const QueriesDocumentProvider(
      {Key? key, required this.queries, required Widget child})
      : super(key: key, child: child);

  final MusicMateQueries queries;

  static MusicMateQueries of(BuildContext context) {
    final InheritedElement? element = context
        .getElementForInheritedWidgetOfExactType<QueriesDocumentProvider>();
    assert(element != null, 'No MusicMateQueries found in context');
    return (element!.widget as QueriesDocumentProvider).queries;
  }

  @override
  bool updateShouldNotify(QueriesDocumentProvider oldWidget) =>
      queries != oldWidget.queries;
}

We then add an extension to the BuildContext to provide our additional functionalities to the BuildContext class.

extension BuildContextExtension on BuildContext {

  /// Enables us to use context to access queries with [context.queries]
  MusicMateQueries get queries => AppProvider.of(this);

  /// Use context to show material banner with [context.showError()]
  void showError(ErrorModel error) {
    SchedulerBinding.instance?.addPostFrameCallback((timeStamp) {
      var theme = Theme.of(this);
      ScaffoldMessenger.of(this).showMaterialBanner(
        MaterialBanner(
          backgroundColor: theme.colorScheme.primary,
          contentTextStyle:
              theme.textTheme.headline5!.copyWith(color: Colors.white),
          content: Text(error.error),
          actions: [
            InkWell(
              onTap: () => ScaffoldMessenger.of(this).clearMaterialBanners(),
              child: const Icon(Icons.close, color: Colors.white),
            )
          ],
        ),
      );
    });
  }
}

Implementing GraphQL

To begin implementing GraphQL in our Flutter app, we need to add the GraphQL dependency for Flutter. To do that, run this command on your terminal:

flutter pub add graphql_flutter

In the main.dart file, change the MyApp widget from a StatelessWidget to a StatefulWidget.

To connect our Flutter app to the GraphQL server, we need to create a GraphQLClient, which requires a Link and a GraphQLCache. The instance of GraphQLClient is passed to a ValueNotifier. This is also a good time to create an instance of MusicMateQueries.

The cache passed is what GraphQL uses to cache the result. By default, GraphQLCache uses InMemoryCache, which does not persist to local storage. To persist the cache to local storage, initialize the GraphQLCache store parameter to HiveStore like this: GraphQLCache(store: HiveStore());

  final GraphQLClient client = GraphQLClient(
    link: HttpLink('https://music-mates-fun.herokuapp.com/graphql'),
    cache: GraphQLCache(),
  );

  late final ValueNotifier<GraphQLClient> clientNotifier =
      ValueNotifier<GraphQLClient>(client);

  final queries = MusicMateQueries();

Next, wrap the MaterialApp with GraphQLProvider. This is important because using Query or Mutation widgets requires a client, and this provider is responsible for passing an instance of GraphQLClient down the widget tree. Pass an instance of ValueNotifier<GraphQLClient> to GraphQLProvider so that widgets can be notified when data changes.

Also, wrap the GraphQLProvider with our own QueriesDocumentProvider. The build() method will be updated to the code snippet below:

  @override
  Widget build(BuildContext context) {
    return QueriesDocumentProvider(
      entity: queries,
      child: GraphQLProvider(
        client: clientNotifier,
        child: MaterialApp(
          title: 'Music Mates',
          theme: _themeData(context),
          home: const GetStartedScreen(),
          routes: {
            Routes.home: (context) => const HomeScreen(),
            Routes.selectArtist: (context) => const SelectFavouriteArtist(),
          },
        ),
      ),
    );
  }

Add the line of code below to the BuildContextExtension extension to easily access an instance of GrapQLClient by calling context.graphQLClient:

  /// Enables us to use context to access an instance of [GraphQLClient] with [context.graphQlClient]
  GraphQLClient get graphQlClient => GraphQLProvider.of(this).value;

  /// Take advantage of the GraphQL cache to cache the user's Google ID 
  /// to be used across the app
  void cacheGoogleId(String googleId) {
    graphQlClient.cache.writeNormalized('AppData', {'googleId': googleId});
  }

  /// Retrieves current user's Google ID from the cache
  String get retrieveGoogleId =>
      graphQlClient.cache.store.get('AppData')!['googleId'];
  

The docs state that using the cache directly (graphQLClient.cache) isn’t ideal, as changes won’t be broadcasted immediately. It’s preferable to use client.writeQuery and client.writeFragment to those on the client.cache for automatic rebroadcasting. In this case, using cache directly is more of a hack to cache the Google ID coming from outside our database. (It comes from the Google Sign-In package.)

Performing CRUD operations with GraphQL

GraphQL uses the query keyword for read operations and the mutation keyword for create, update, and delete operations.

Look at this flowchart of the app’s process to better understand what is expected of us. Flowchart of music mate

Query

Firstly, go to the lib/data/queries.dart file. Under fetchAllArtist(), return the query string below:

"""
    query {
      allArtists {
            id
            name
            imageUrl
            description
        }
    }
"""

We use the query keyword to signify that this is a query request. allArtists is the root field, and everything else that follows is the payload.

Add a fromJson constructor in the ArtistModel class. This will be responsible for parsing the artist JSON data.

You can locate the ArtistModel class in this file path: lib/data/model/artist.dart

ArtistModel.fromJson(Map<String, dynamic> data)
  : name = data['name'],
    id = data['id'] == null ? null : int.parse(data['id']),
    imageUrl = data['imageUrl'],
    description = data['description'];

Since we’ll be dealing with a list of artists, it makes sense to add another model class named ArtistList that will be responsible for parsing a list of artist JSON data to a list of ArtistModel.

class ArtistList {
  final List<ArtistModel> artists;

  ArtistList.allArtistFromJson(Map<String, dynamic> json)
      : artists = json['allArtists']
            .list
            .map((e) => ArtistModel.fromJson(e))
            .toList();
}

In fetchUserInfo(), return the string query below:

"""
   query UserInfo(\$googleId: String!){

    userInfo(googleId: \$googleId){
      name
      imageUrl
      favouriteArtists {
        name
        imageUrl
        description
      }
    }
    
    musicMates(googleId: \$googleId){
      name
      imageUrl
    }
   
  }
   """

The above is a query with variables. The UserInfo($googleId: String!) following the query keyword defines a variable (which is needed by that query) and its data type to be used across the nested payloads.

The nested userInfo(googleId: $googleId) is the root field for fetching the user’s info. Then, the googleId variable is passed along the query because the query requires it. The same thing applies to the musicMates field.

Now that we have a glimpse of the user JSON structure, it’s also a good time to add a fromJson() constructor in the UserModel class, which will be responsible for parsing user JSON data to the model class.

You can locate the UserModel class in this file path: lib/data/model/user_model.dart

UserModel.fromJson(Map<String, dynamic> data)
  : name = data['name'],
    imageUrl = data['imageUrl'],
    favouriteArtist = data['favouriteArtists'] == null
        ? []
        : (data['favouriteArtists'] as List<dynamic>?)
            ?.map((e) => ArtistModel.fromJson(e))
            .toList();

Also, add a UserList model to parse the list of users.

class UserList {
  final List<UserModel> users;

  UserList.musicMatesJson(Map<String, dynamic> data)
      : users = (data["musicMates"] as List)
            .map((e) => UserModel.fromJson(e))
            .toList();
}

Since the query on the home page fetches all the data we need at once, we need to have a central model for our home screen. Update the HomeModel class in lib/data/model/user_model.dart with the code snippet below:

import 'package:music_mates_app/data/data_export.dart';

class HomeModel {
  final UserModel currentUser;
  final List<UserModel> musicMates;

  HomeModel.fromJson(Map<String, dynamic> json)
      : currentUser = UserModel.fromJson(json['userInfo']),
        musicMates = UserList.musicMatesJson(json).users;
}

You might notice that something is amiss in the string queries above — where are those root fields (userInfo and musicMates) coming from? Or are they just arbitrary field names? Well, recall that in the beginning of this article, it was mentioned that graphql is a contract/blueprint between the front end and back end. So there must be back-end code.

The back end receives our request and resolves the query. Take a peek at the back-end code for the queries below:

Back-end query code

Note: I’m using Django with GraphQL for the back end due to my familiarity with Python. However, you can also use GraphQL technology with other back-end technologies, like Node.js. Additionally, the code above might not be perfect — this is my first time writing back-end code, so forgive any irregularities.

Next, head over to the lib/presentation/widgets folder, create a file named query_wrapper.dart, and insert the code below. The goal is to make a reusable widget that we can use across our app to avoid duplication of code. Make sure to read the helpful comments in the code snippet below.

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:music_mates_app/data/data_export.dart';
import 'package:music_mates_app/presentation/widgets/export.dart';

class QueryWrapper<T> extends StatelessWidget {

  const QueryWrapper({
    Key? key,
    required this.queryString,
    required this.contentBuilder,
    required this.dataParser,
    this.variables,
  }) : super(key: key);

  /// Query parameters meant to be passed alongside the query
  final Map<String, dynamic>? variables;

  /// The query string document to request from the GraphQL endpoint
  final String queryString;

  /// This callback method is responsible for building our UI and passing the data
  final Widget Function(T data) contentBuilder;

  /// This callback receives the JSON data in the form of a map and then
  /// parses the data to our model class
  final T Function(Map<String, dynamic> data) dataParser;

  @override
  Widget build(BuildContext context) {
    return Query(
      options: QueryOptions(
        fetchPolicy: FetchPolicy.cacheAndNetwork,
        document: gql(queryString),
        variables: variables ?? const {},
        parserFn: dataParser,
      ),
      builder: (QueryResult result,
          {VoidCallback? refetch, FetchMore? fetchMore}) {
        if (result.isLoading) {
          return const LoadingSpinner();
        }

        if (result.hasException) {
          return AppErrorWidget(
            error: ErrorModel.fromString(
              result.exception.toString(),
            ),
          );
        }

        return contentBuilder(result.parserFn(result.data ?? {}));
      },
    );
  }
}

In the above code, we define a generic parameter of type T for the widget. The generic parameter T is a placeholder for the type of data model we expect. (Read on for clarification on this.)

To harness the reactive nature of GraphQL queries with Flutter, we use the Query<TParsed> widget, which is a generic widget. It accepts two parameters: options and builder.

options

The options parameter is where configurations are set for “this” query call. It is an instance of the QueryOptions class, which accepts some parameters. Specifically, we will be setting the fetchPolicy, document, parserFn, and variables parameters.

  • The document parameter is where our query string will go, but it requires an instance of DocumentNode. We will have to use gql(), which is provided by the GraphQL package and has the responsibility of parsing the string to a DocumentNode.
  • variables is a Map<String, dynamic> containing our query param, mapped to its value.
  • parserFn takes the JSON data format and parses it to a model class of a specified type.
  • fetchPolicy is a parameter that requires an enum of type FetchPolicy. This dictates how fetching is done between cache and remote data. Below is an explanation of how each value affects the query process.
    • FetchPolicy.cacheFirst prioritizes cached data and only fetches from remote data when cached data isn’t available.
    • FetchPolicy.cacheAndNetwork queries for local data first, if it is available. It then fetches from remote data and returns it.
    • FetchPolicy.cacheOnly, as the name implies, fetches from cached data and fails if no data is available.
    • FetchPolicy.noCache fetches remote data but does not cache any data returned.
    • FetchPolicy.networkOnly prioritizes fetching from remote data but still saves data to the cache.

builder

The builder is a callback method that gets called when a query request is triggered, when the request is successful, and when the request fails. Its return type is a Widget.

The builder has three parameters:

  • result:- an instance of QueryResult that has properties for the current state.
    • QueryResult.isLoading is set to true when the request is still in progress. This is a good place to display a progress bar.
    • QueryResult.hasException is set to true when an error occurs while processing a request.
    • QueryResult.data holds the response data returned by the GraphQL endpoint. It’s of type Map<String, dynamic>.
    • QueryResult.parseFn parses the data in JSON format to the specified model.
  • refetch is a void callback method that should be called to instruct the query to make a request to the GraphQL endpoint again.
  • fetchMore is a callback that can be utilized to handle the pagination of data.

To utilize QueryWrapper, replace the ListView in the SelectFavouriteArtist widget with the code below:

This widget can be found in lib/presentation/select_favourite_artist.dart.

QueryWrapper<ArtistList>(
  queryString: context.queries.fetchAllArtist(),
  dataParser: (json) => ArtistList.allArtistFromJson(json),
  contentBuilder: (data) {
    final list = data.artists;

    return ListView.builder(
      itemCount: list.length,
      physics: const BouncingScrollPhysics(),
      itemBuilder: (c, index) {
        var artist = list[index];
        return ItemSelectArtist(
          artist: artist,
          onTap: () => onTap(artist.id!),
          isSelected: selectedArtist.contains(artist.id),
        );
      },
    );
  },
)

Also, replace the _HomeScreenState scaffold body with the code below.

QueryWrapper<HomeModel>(
  queryString: context.queries.fetchUserInfo(),
  dataParser: (json) => HomeModel.fromJson(json),
  variables: {
    'googleId': context.retrieveGoogleId,
  },
  contentBuilder: (data) {
    return _Content(model: data);
  },
)

Mutation

The mutation keyword is used to signify that a request is to create, update, or delete data. Let’s specify our mutation string in the lib/data/queries.dart file. Under the createAccount() method, return the following string:

"""
  mutation createUser(\$name: String!, \$googleId: String!, \$imageUrl: String!, \$favouriteArtists: [ID]){
      createUser(name: \$name, googleId: \$googleId, imageUrl: \$imageUrl, favouriteArtists: \$favouriteArtists){
        user{
          name
          imageUrl
          favouriteArtists {
            name
            description
            imageUrl
          }
        }
      }
  }
  """

Under updateUser(), return the string code snippet below.

"""
    mutation UpdateUser(\$name: String, \$googleId: String!, \$imageUrl: String, \$favouriteArtists: [ID]){
        updateUser(name: \$name, googleId: \$googleId, imageUrl: \$imageUrl, favouriteArtists: \$favouriteArtists){
          user{
            name
            imageUrl
            favouriteArtists {
              name
              description
              imageUrl
            }
          }
        }
    }
    """

We can see that the mutation string is very similar to the query string. The only difference between them is their keyword, as mutation uses the mutation keyword.

To see this in action, let’s start by implementing the creation of a user. Head to the lib/presentation/get_started_screen.dart file in the _GetStartedScreenState state, and wrap the GoogleButton with the code below:

Mutation(
  options: MutationOptions(
    document: gql(context.queries.createAccount()),
    onCompleted: (data) => _onCompleted(data, context),
  ),
  builder: (RunMutation runMutation, QueryResult? result) {
    if (result != null) {
      if (result.isLoading) {
        return const LoadingSpinner();
      }

      if (result.hasException) {
        context.showError(
          ErrorModel.fromGraphError(
            result.exception?.graphqlErrors ?? [],
          ),
        );
      }
    }

    return GoogleButton(
      onPressed: () => _googleButtonPressed(context, runMutation),
    );
  },
)

Below is the _onCompleted() method implementation:

 void _onCompleted(data, BuildContext context) {
    /// We get the user's favorite artist field from the data
    final favouriteArtists = data['createUser']['user']['favouriteArtists'];

    /// Check if the user has selected artists
    final bool hasSelectedArtist =
        favouriteArtists != null && favouriteArtists.isNotEmpty;
        
    /// If they do, move to home page. If not, take them to select artist page for them to select artists.
    Navigator.popAndPushNamed(
      context,
      hasSelectedArtist ? Routes.home : Routes.selectArtist,
    );
  }

Let’s analyze the code. Similar to the Query widget, the Mutation widget takes in two parameters, options and builder.

options

The options parameter is where configurations are set for “this” mutation calls. It is an instance of the MutationOptions class, which has some similar parameters to QueryOptions and more. Specifically, we set the document param, which is mandatory, and the onCompleted callback method, which is called when a mutation request is made.

builder

The builder is a callback method that gets called when a mutation request is triggered, when the request is successful, and when the request fails. Its return type is a Widget.

It has two parameters. runMutation accepts a Map<String, dynamic> parameter where you passed mutation query variables. It is an instance of RunMutation. On the other hand, result contains the current state of the request (this was discussed in the query section). It is an instance of QueryResult.

Calling runMutation triggers the request. To see this in action, let’s implement the _googleButtonPressed() method.

  Future<void> _googleButtonPressed(
      BuildContext context, RunMutation runMutation) async {
    
    /// Use the Google Sign-In package to intiate the Google Sign-In
    final googleUser = await googleSignin.signIn();

    if (googleUser == null) return;

    /// Cache the Google ID to be used across the app
    context.cacheGoogleId(googleUser.id);

    /// Trigger the mutation query by calling [runMutation] and passing in the mutation variable
    runMutation(
      {
        'name': googleUser.displayName,
        'googleId': googleUser.id,
        'imageUrl': googleUser.photoUrl!,
        'favouriteArtists': [],
      },
    );
  }

To see how to use mutation for updating, head over to the _DoneButton widget in lib/presentation/select_favourite_artist.dart, and return the code below in the build() method.

Mutation(
  options: MutationOptions(
    document: gql(context.queries.updateUser()),
    onCompleted: (_) => Navigator.popAndPushNamed(context, Routes.home),
  ),
  builder: (RunMutation runMutation, QueryResult? result) {
    if (result!.isLoading) return const LoadingSpinner();

    if (result.hasException) {
      context.showError(ErrorModel.fromString(result.exception.toString()));
    }

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 35),
      child: TextButton(
        style: ButtonStyle(
          enableFeedback: isEnabled,
          backgroundColor: MaterialStateProperty.all(
            Colors.grey[300],
          ),
        ),
        onPressed: () => _onButtonPressed(isEnabled, runMutation, context),
        child: SizedBox(
          width: double.infinity,
          child: Center(
            child: Opacity(
              opacity: isEnabled ? 1 : 0.2,
              child: const Text(
                "DONE",
                style: TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  },
)

The implementation of the method _onButtonPressed() is shown below:

 void _onButtonPressed(
      bool isEnabled, RunMutation<dynamic> runMutation, BuildContext context) {
    if (isEnabled) {
      runMutation(
        {
          'googleId': context.retrieveGoogleId,
          'favouriteArtists': selectedArtist,
        },
      );
    } else {
      context.showError(
        ErrorModel.fromString("Please select your favorite artist"),
      );
    }
  }

We followed the same process for updating the user’s favorite artist as for creating a user. So how does GraphQL know when to update or create data? You’ll probably answer correctly that the power lies in the back end doing all the work of resolving queries and mutation requests. Take a peek at the back-end code below:

Back-end mutation code

Note: I’m using Django with GraphQL for the back end due to my familiarity with python. However, you can also use GraphQL technology with other back-end technologies, like Node.js. Additionally, the code above might not be perfect — this is my first time writing back-end code, so forgive any irregularities.

Subscription

While we won’t need to implement a subscription for our app, it’s very important to know how this works. Subscribing to a GraphQL endpoint means listening to changes and reacting when a change occurs. To achieve this, it uses WebSockets and Dart Stream.

Subscriptions use a similar format with a query, but this type uses subscription as the keyword. It uses the Subscription widget as opposed to the Query widget for the query.

If we want to use a subscription, the link has to be different. But not all of our requests are WebSocket links. So how do we achieve this? We can do this by splitting the subscription-consuming link from the HttpLink.

link = Link.split((request) => request.isSubscription, websocketLink, link);

It accepts options, an instance of SubscriptionOptions. The builder callback has a result parameter, which we’ve discussed already.

Wrap the child widget with ResultAccumulator, which is a helper widget provided by the graphql_flutter package, to combine subscription results without having to do the work yourself.

ResultAccumulator.appendUniqueEntries(
    latest: result.data,
    builder: (context, {results}) => ChildWidget(
      reviews: results.reversed.toList(),
    ),
  )

Continous integration/delivery with Codemagic (CI/CD)

CI/CD is automates deployment processes so that developers can focus on product requirements and the quality of the code.

Continuous integration deals with building and testing applications when there’s an update in the codebase.

Continuous delivery automates the deployment or publishing of apps to various platforms, like Google Play Store, Apple App Store, TestFlight, and Firebase.

Codemagic is a CI/CD tool that works excellently with Flutter. Some of its cool features are highlighted below:

  • Handles building both Android (.apk/.aab) and iOS (.ipa) files
  • Very easy to set up
  • Supports different Git version control platforms, like GitHub, Bitbucket, and GitLab
  • Works with a variety of triggers
  • Good error reporting when something goes wrong during the build

Codemagic setup

Before setting up your app, you need to sign your Android application.

Sign in to Android application

Please note: If you already know how to sign in to an Android application, you can skip the steps below.

Android applications require two signing keys. Signing is a way of giving the app digital signatures. The first time you need to sign is when you are uploading to the Play Store — here, you’ll use the upload key. The second time is after deployment — you will need to use the deployment key to sign the .apk/.aab file.

To generate an upload key, run one of the commands below:

For Mac/Linux:

  keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias myAliasKey

For Windows:

  keytool -genkey -v -keystore c:\Users\USER_NAME\upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias myAliasKey

When you run one of these commands, you are required to input some information. Bear in mind that information like the store password and key password will be needed. After you’re done with this step, the command generates an upload-keystore.jks file on your home page.

You can choose to change the path to store the file, the .jks file name, and the alias key.

Under the android folder, create a file called key.properties. Then insert the code below, replacing <> with the actual value.

storePassword=<Store password>
keyPassword=<Key password>
keyAlias=<Alias key name>
storeFile=<Path to .jks file>

This file is a private file and SHOULDN’T be committed to your public source control. Search for .gitignore and add your key.properties file to tell Git to ignore this file.

The next step is to utilize the upload key to sign the app. Head over to the /android/app/build.gradle file and insert the code below:

/// This loads the key.properties file
def keystoreProperties = new Properties()

def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

Search for the “buildTypes” code block, and replace the content with the code below:

signingConfigs {
    release {
        storeFile file(keystoreProperties['storeFile'])
        keyAlias keystoreProperties['keyAlias']
        keyPassword keystoreProperties['keyPassword']
        storePassword keystoreProperties['storePassword']
    }
  }

buildTypes {
    release {
        signingConfig signingConfigs.release
    }
}

The above code utilizes the key.properties file. Any time the app is run or is built on release mode, the upload key is used to sign the app. The sign-in for an iOS application can be done automatically with Codemagic.

To create a Codemagic account, visit https://codemagic.io/signup, and then choose an option. Preferably, sign up with the version control platform where you store this application codebase so that Codemagic can easily pick it up.

You’ll be prompted to add an application. You can either input the Git URL of the project you want to set up or select a Git provider so that Codemagic can load your repository.

Codemagic application setup

Select your repository, and select Flutter App for the project type.

Multiple workflows can be configured. Workflows come in handy when you have different release environments, like production and development environments.

Build triggers setup

This section deals with setting up Git actions that will trigger Codemagic to start building your application. You have the option of triggering a Codemagic build on push, pull request update, or tag creation. You can attach these triggers to a specific branch target.

Build triggers

We want Codemagic to trigger on push and pull requests, and our target branch is the master branch.

Test setup

To ensure that all test cases pass before publishing your app, ensure that you allow Codemagic to run tests for each build by checking the Enable Flutter test field. You can also enable Static code analysis.

Build setup

Select Android App Bundle (AAB) as the Android build format. The build mode should be set to Release.

Build arguments are where you pass arguments to run along with the build command. In our case, we want the app version to increment for each build, so we set the build arguments for both iOS and Android as --build-name=1.0.$PROJECT_BUILD_NUMBER --build-number=$PROJECT_BUILD_NUMBER.

Distribution setup

Android code signing

Insert your upload signing configuration in the Android code signing tab. This includes the .jks file that was created, as well as the keystore password, key alias, and key password.

Google Play

In the Google Play tab, check the Enable Google Play publishing checkbox. Select the internal track and check Submit release as drafts to build and upload to the Google Play Console as a draft. Then we can manually release the app to internal testers. You can learn about each of these tracks here.

The reason you need to select the internal track is because before releasing to production, you must have published an internal track application and answered a series of questions about your application.

Follow these steps to get the credentials (.json). Note that you must have a Google Play account to proceed.

In Google Play Console

  1. Go to the Google Play Console and click on the Create app button. Create App in Google Play Console
  2. Click on the API access tab. Then click on the Create new service account button.
  3. In the dialog, tap on the Google Cloud Platform link.

In Google Cloud Platform

  1. Click on the menu, and go to IAM & Admins > Service Accounts.
  2. Select a project, and then select the Google Play Console Developer link in the dialog that pops up.
  3. Click Create service account.

According to the documentation, a service account credential is used to authenticate as a robot service account or to access resources on behalf of Google Workspace or Cloud Identity users through domain-wide delegation.

  1. Fill in the service account details. Then click Create and continue.
  2. Assign a role to the service account. Service account role
  3. Click Done.
  4. Click on the service account you just created. It should look like the image below. Created Service Account
  5. Click Keys > Add Keys > Create new key.
  6. You’ll be prompted with the dialog shown below. Select JSON, then click Create. Create JSON credentials

If you completed the above steps correctly, a .json file should be downloaded automatically. Then upload the .json file in the Credentials tab on the Codemagic portal. Make sure to save your changes.

iOS code signing

To enable this section, you’ll have to connect your Apple Developer Portal account. iOS code signing Tap on the Go to settings button and follow the instructions to connect your account.


Head back to the Google Play Console on the API access tab. Under Service accounts, confirm that the service account you just created was added, and then click on Grant permissions. If you don’t see the service account, refresh the page.

Google Play Console service account

Under Permission, tap on the App permissions tab, click on the Add app dropdown, and select the Google Console app you created.

Now you can manually trigger a build by clicking on the Start new build button. Before triggering, ensure that:

  1. The Kotlin version is equal to or above 1.6.0 by heading to /android/build.gradle under the buildScript code block.
  2. Artifacts have been uploaded manually at least once for the first upload. If not, a 404: Package not found error will be thrown by Codemagic.
  3. Each app version must be different from the previous app version.

The same principles apply to publishing in the production track, though you have to complete a series of questions concerning your app on the Google Play dashboard.

Google Play dashboard

At this stage, we can only trigger the build manually. However, we can also do this automatically. The goal is to trigger Codemagic to start a build when there’s a push to the master branch.

Webhooks allow you to send real-time data from one application to another in response to an event. On our Codemagic app details page, click on the Webhooks tab, and copy the URL shown there.

Head to your repository settings on GitHub. In the left panel, click Webhooks and insert the URL you just copied to the Payload URL field. Then click the Add webhook button. Now, any time a push occurs on the master branch, Codemagic triggers a build.

GitHub webhooks

That was a lot to process! This section focused on how to sign our apps, the purpose and benefits of CI/CD, and how to use and configure CI/CD with Codemagic.

Conclusion

I hope you had fun building the Music Mates app! To see the completed code, run git checkout master in your terminal. While this was probably a lot to take in, we’ve learned so much about GraphQL, what makes it powerful, how we can utilize it in Flutter, and how to harness the reactive nature of flutter_graphql. And we didn’t stop there — we took our challenge two steps further up the ladder by implementing continuous integration/delivery and learning how to sign our applications and prepare for release. Hopefully, this will give you all the knowledge you need to start building your applications with GraphQL and handling your continuous integration/delivery with Codemagic.


Jahswill Essien is a mobile developer, ardent on building awesome stuff with Flutter, he has been using Flutter for the past 2 years. Jahswill is a visual learner as such he strives to write articles that contain pictorial illustrations and/or analogies to drive home a concept explained. His favourite quote is: “We are constrained by our mind, what we feel is our limit isn’t our limit. It’s amazing how much we can learn in a short amount of time by perseverance alone”. Check out some things Jahswill built with flutter by visiting his Github profile.

How did you like this article?

Oops, your feedback wasn't sent

Latest articles

Show more posts