How to implement GraphQL with Flutter + GraphQL example + CI/CD (Codemagic)

How to implement GraphQL with Flutter + GraphQL example + CI/CD (Codemagic)

Originally posted in Codemagic

Have you ever been faced with the task of implementing restful API's in your mobile application and you find yourself calling multiple endpoints to populate data for a single screen? You probably wished to have more control over the data returned by the endpoint, it could be to have more data fetched with a single endpoint call or have only the necessary data fields returned by the call.

Follow me along to see how to achieve that with GraphQL. In this article, you'll be tasked with implementing GraphQL in an existing codebase. We will discuss what graphql is, why we chose graphql, how to implement graphql with flutter, and implement continuous integration/delivery in our completed app.

This article assumes familiarity with the flutter framework and experience with using REST APIs.

The project we'll be working on is named Music Mates, its functionality is a very simple one.

  • Users are asked to select favorite music artists from a predefined list during sign-up.
  • Users should see fellow users that share one or more 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

Set up

Run this command to clone the project from Github to your local, 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.

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

For adaptability of the ring widget that displays a user's music mates, we will be needing some math functions and that’s defined in the maths_mixin.dart. Below is the code snippet for MathsMixin.

mixin MathsMixin {

 /// This method computes the radius of each individual circles in relation
 /// 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 radian value
  double get radian => this * math.pi / 180;
}

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

The presentation folder contains all our widgets. The MatesRingWidget widget in the mates_ring.dart file is an interesting one. It's the widget that displays a user's music mates in a ring pattern. I've added some helpful comments explaining how that was achieved. Feel free to check out that file.

These are the dependencies added at this time

  • flutter_svg - Used to display .svg file
  • google_sign_in - To implement google sign in

We now have an overview of the current codebase, our task would be 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, it then sends the request from the client to the backend for resolving the query.

Why GraphQL?

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

Use of Single Endpoint

One of the advantages graphql has is the use of a single endpoint as opposed to different endpoints to fetch various data. For comparison purposes, when dealing with music mates, using REST APIs can have about 5 different endpoints in total:

  • 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 need just a single endpoint, say baseUrl/graphql. Weird isn't it? The question you'd probably be asking is, how does graphql know what data to return?

Well, rather than endpoints graphql organizes data in terms of types and its fields or properties in other to make sure clients ask for only what is possible. The fields can be a scalar (Int, Float, Boolean, String) or objects that contains properties of their own. An example of a graphql schema is 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 datatype of the field, the ! sign specifies that the field isn't nullable. A list datatype is specified by using square brackets [] and the list content datatype.

Fixes the issue of under-fetching/over-fetching

Fixing under/over fetching is one of the greatest strengths of graphql technology. To understand these concepts, take a look at the screen below, what data is needed to populate the screen?

App home page

Dealing with under-fetching of data

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

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

 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 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, the circle avatar widgets for displaying the user's image, we just 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, it just returns the whole data properties specified by the endpoint, this is termed as over-fetching, fetching data properties that aren’t needed.

Using graphql, we have control over what field should be returned by the API. Refactoring the above query to suit our current widgets need, 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 isn't it?

GraphQL is faster

Due to graphl flexibility in regards to retrieving specific fields when querying data, it results in it being 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, that 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 would be helpful to us. Head to lib/presentation folder and create query_document_provider.dart file. The goal here is to inject our queries document down the widget tree to be accessible to children widgets using InheritedWidget.

Before implementing the QueriesDocumentProvider, it is important to understand what InheritedWidget is. It is a widget that propagates data down to the widget tree. It utilizes the 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 finding the nearest Scaffold or Navigator widget, keep in mind that widget BuildContext has a Bottom-Up relationship with other widget's 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 on the BuildContext to provide our additional functionalities to the BuildContext class.

extension BuildContextExtension on BuildContext {

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

  /// Use context to show material banner by doing [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 implementation of 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 MyApp widget from a StatelessWidget to a StatefulWidget.

To connect our flutter app to the graphql server, we need to create GraphQLClient which requires a Link and GraphQLCache. The instance of GraphQLClient is passed to a ValueNotifier. It's also a good time to create an instance of MusicMateQueries.

The cache passed is what GraphQL uses to cache result, GraphQLCache by default uses InMemoryCache which does not persist to local storage. To persist cache to local storage, initialise 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 widget 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 when data changes, widgets can be notified of this.

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] by doing [context.graphQlClient]
  GraphQLClient get graphQlClient => GraphQLProvider.of(this).value;

  /// Take advantage of graphql cache, to cache the user's google Id 
  /// to be used across 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 cache directly (graphQLClient.cache) isn't ideal as changes won't be broadcasted immediately. Rather, prefer using client.writeQuery and client.writeFragment to those on the client.cache for automatic rebroadcasting. In my case, using cache directly is more of a hack, to cache google id coming 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, delete operations.

Below is a flowchart of the app's process, to better understand what is expected of us.

Flowchart of music mate

Query

Firstly, go to 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 it's a query request. The 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.

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 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 user's info, and then the googleId variable is passed along the query because the query requires it, the same thing applies to the musicMates field.

We have a glimpse of how the user json structure is, it's also a good time to add a fromJson() constructor in the UserModel class, 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 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 data we need at once, we need to have a central model for our home screen. In the HomeModel class in lib/data/model/user_model.dart, update it 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 from the onset of this article, it was mentioned that graphql is a contract/blueprint between the frontend and backend. So there must be a backend code.

The backend receives our request and resolves the query. Take a peep at the backend code for the queries below.

Backend Query Code

Note: I'm using Django with graphql for the backend due to my familiarity with python, you can use graphql technology with other backend technologies like Node.js. Also, the code might not be perfect, this is my first time writing backend code, so forgive any irregularities in the code above.

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 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 query
  final Map<String, dynamic>? variables;

  /// The query string document to request from 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 a 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 defined 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 clarifications 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, particularly we will be setting 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 the gql() which is provided by the graphql package and has the responsibility of parsing the string to a DocumentNode. >
  • The variables is a Map<String, dynamic> containing our query param, mapped to its value. >
  • The parserFn takes the json data format and parses it to a model class of a specified type. >
  • fetchPolicy parameter 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 prioritize cached data, and only fetches from remote when cached data isn't available. >
    • FetchPolicy.cacheAndNetwork queries for local data first, if available. Then fetches from remote data and returns it. >
    • FetchPolicy.cacheOnly as the name implies, fetches data from cached data and fails if no data is available. >
    • FetchPolicy.noCache fetches remote data and ignores caching any data returned. >
    • FetchPolicy.networkOnly prioritize fetching from remote data, but still saves data to 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 which 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 during processing 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 callback 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 us specify our mutation string in the lib/data/queries.dart file. Under 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 is their keyword, mutation uses the mutation keyword.

To see this in action, let's start by implementing creating a user. Head to lib/presentation/get_started_screen.dart file in _GetStartedScreenState state, 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 favourite 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.
    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 with QueryOptions and more, particularly we set document param which is mandatory, and the onCompleted callback method, that is called when a mutation request is done.

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 us implement the _googleButtonPressed() method.

  Future<void> _googleButtonPressed(
      BuildContext context, RunMutation runMutation) async {

    /// Use the google signin package to intiate the google sign in
    final googleUser = await googleSignin.signIn();

    if (googleUser == null) return;

    /// cache the google id to be used across app
    context.cacheGoogleId(googleUser.id);

    /// Trigger the mutation query by calling the [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 update, head over to _DoneButton widget in lib/presentation/select_favourite_artist.dart, 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 method _onButtonPressed() implementation is 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 favourite artist"),
      );
    }
  }

For updating the user favorite artist, we followed the same process 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 backend doing all the resolving of queries and mutation requests. Take a peep at the backend code below.

Backend Mutation Code

Note: I'm using Django with graphql for the backend due to my familiarity with python, you can use graphql technology with other backend technologies like Node.js. Also, the code might not be perfect, this is my first time writing backend code, so forgive any irregularities in the code above.

Subscription

While our app, won't be needing to implement a subscription, it's very important to create awareness for this. 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 keyword. It uses the Subscription widget as opposed to the Query widget for the query.

To use a subscription, the link has to be a different link. But then, not all our requests are WebSocket links, 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 and 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 you 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 a tool that automates deployment processes while the developer focuses on meeting up product requirements, quality of 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 our app to various platforms like google play store, apple store, test flight, firebase e.t.c.

Codemagic is a CI/CD tool and works excellently with flutter, with some of its cool features highlighted below.

  • Handles building both android (.apk/.aab) and IOS (.ipa) files.
  • It's very easy to set up.
  • It supports different git version control platforms like Github, Bitbucket, Gitlab.
  • Works with a variety of triggers.
  • Good error reporting when something went wrong during the build.

Codemagic set-up

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

Signin android application

Please note: if you already know how to sign in 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 signing is required during upload to play store, here you'll use the upload key. The second is done after deployment, the deployment key will be used to sign the .apk/.aab file.

To generate an upload key, run the command 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

The above command when run, requires you to input some information. Bear in mind that information like the store password and the key password would be needed. After you're done with that step, the command generates an upload-keystore.jks file on your home page.

You can decide 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 the <> 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 SHOULD'NT be committed to your public source control. Search for .gitignore add your key.properties file, this tells 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, 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 "buildTypes" code block and replace 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 and anytime the app is run or is built on release mode, the upload key would be use to sign the app. IOS application sign in can be done automatically on codemagic.

To create a codemagic account visit https://codemagic.io/signup and then choose an option. Preferably sign-up with your version control platform where this application codebase is stored 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 set up application

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 aspect deals with setting up git actions that will trigger codemagic to start building your application. You have the option of triggering a codemagic build when push occurrs, 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 to enable codemagic running 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 in Release.

Build arguments are where you pass arguments to run along with the build command. For our case we want to 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, 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 the Submit release as drafts, this build and upload to google play console as a draft, then we can manually release the app to internal testers, tap here to understand these each of these tracks.

The reason for selecting the internal track is because before releasing to production, you must have published an internal track application and answer series of questions about your application.

Take these steps to get the Credentials (.json). Note, you must have a google play account to proceed.

In Google Play Console

  1. Go to the Google Play Console, click on the Create app button.

Create App in google play console

  1. Click on the Api Access tab, search and click on the Create new service account button.
  2. On the dialog, tap on the Google Cloud Platform link.

In Google Cloud Platform

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

    From 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.

  4. Fill in the service account details, then click Create and continue
  5. Assign role to the service account.

Service account role

  1. Click Done.
  2. Click on the service account just created. Should look like the image below Created Service Account

  3. Click Keys > Add Keys > Create new key.

  4. You'll be prompted with a dialog 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 codemagic portal. Make sure to save your changes.

In IOS code signing

To enable this section, you'll have to connect your apple developer portal.

IOS code signing Tap on the Go to settings button and follow the instructions to connect your appstore 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, then click on the Grant permissions, if you don't see the service account,refresh the page.

Google play console services account

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

Right 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 buildScript code block.
  2. For the first upload, ensure that artifacts have been uploaded manually at least once. If not a 404-Package not found error would be thrown by codemagic.
  3. Each app version must be different from the previous app version.

To publish in the production track the same principles apply. But you have to complete a series of questions concerning your app on the google play dashboard.

Google play dashboard screenshot

At this stage we can only trigger the build manually, however, we can do the same automatically. The goal is to trigger codemagic to start build when there's a push to the master branch.

Webhooks allows you to send real-time data from one application to another in response to an event. In our codemagic app detail page, click on the tab named Webhooks. Copy the url shown there.

Head to your repository settings on Github. On the left panel, click Webhooks and insert the url you just copied to the Payload URL field. Then click the Add webhook button. Now anytime a push occurs on the master branch, codemagic triggers a build.

Github Webhooks screenshot

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

Conclusion

Hope you had fun building the Music Mates app? To see the completed code run git checkout master on your terminal. While this was probably a lot to take in, we've learnt much about graphql, what makes it powerful, how we can utilise graphql in flutter, and how to harness the reactive nature of flutter_graphql. We didn't stop there, we took our challenge two-steps the ladder by implementing continous integration/delivery, learning how to sign our applications and prepare for release. Hopefully, with this you have all you need to start building your applications with graphql and codemagic to handle your continous integration/delivery.