DEV Community

Cover image for Keeping It Simple: Integrate GraphQL and MobX to your Flutter apps
Joshua de Guzman
Joshua de Guzman

Posted on

Keeping It Simple: Integrate GraphQL and MobX to your Flutter apps

Introduction

I stumbled upon the following questions and issues on the web when I was trying to do the same thing for my work---Integrate GraphQL and MobX in Flutter:

I found out... there are not many available resources that demonstrate how to request and send data back to the server, using GraphQL with MobX inside a Flutter app.

So I decided to write a tutorial on this topic with a slight touch of clean architecture. ✍️

Disclaimer

This is not an introduction to GraphQL or MobX. If you haven't heard or tried using either one of them, I suggest looking into ResoCoder's tutorial on MobX or reading my previous article regarding Flutter and GraphQL.

Project

Your goal is to create a simple Flutter app that retrieves a list of artists based on a search query sent to a Spotify GraphQL server.

Search Artists via Spotify API


Search Artists via Spotify API

You can download the final project here.

Overview

Overview of the project's structure


Overview of the project's structure

Setup the Flutter project

At the time of writing, I am using the Flutter SDK stable v1.17.5.

a. From your terminal, clone the repository:

https://github.com/joshuadeguzman/flutter-mobx-graphql-starter
Enter fullscreen mode Exit fullscreen mode

b. In the pubspec.yaml, add the following dependencies

dependencies:
  ...

  mobx: ^1.2.1+1
  flutter_mobx: ^1.1.0+1
  graphql: ^3.0.0
  either_option: ^1.0.6

dev_dependencies:
  ...

  mobx_codegen: ^1.1.0+1
  build_runner: ^1.10.0
Enter fullscreen mode Exit fullscreen mode

Few things to note 💡

  • mobx is the package the helps you write reactive applications following MobX's principles
  • mobx_codegen makes it super easy for you to use mobx and writing store files through annotations and code generation
  • either_option is a small library that allows you to have typed and safe error handling when invoking functions in your Dart code such as a network or API call

c. Retrieve dependencies of the project

flutter packages get
Enter fullscreen mode Exit fullscreen mode

Setup GraphQL client

a. In the main.dart file, add the following to your _MyAppState class

class _MyAppState extends State<MyApp> {
    HttpLink _httpLink;
    GraphQLClient _client;

    @override
    void initState() {
        _httpLink = HttpLink(
            uri: 'https://spotify-graphql-server.herokuapp.com/graphql',
        );
        _client = GraphQLClient(
            cache: InMemoryCache(),
            link: _httpLink,
        );

        /// Add other initializations here
    }
}
Enter fullscreen mode Exit fullscreen mode

b. Pass the GraphQLClient's instance to the data source class

class _MyAppState extends State<MyApp> {
    HttpLink _httpLink;
    GraphQLClient _client;

    /// Add the following
    ISpotifyApiDataSource _spotifyApiDataSource;
    ISpotifyApiRepository _spotifyApiRepository;

    @override
    void initState() {
        ...

        /// Pass the [GraphQLClient]'s instance
        _spotifyApiDataSource = SpotifyApiDataSource(_client);
        _spotifyApiRepository = SpotifyApiRepository(_spotifyApiDataSource);

        super.initState();
    }
}
Enter fullscreen mode Exit fullscreen mode

c. Provide the _spotifyApiRepository to the ArtistsStore

Provider(
  create: (_) => ArtistsStore(_spotifyApiRepository),
),
Enter fullscreen mode Exit fullscreen mode

In a real app, you shouldn't always be creating the instances of store classes at the top most part of the tree when providing it. Otherwise, they'll get exposed to other unrelated features of the app.

But for simplicitly, let's just leave it here. 🤷‍♂️

GraphQL Integration

GraphQL 101

  • In a typical setup of a GraphQL, you only have to connect your app to a single endpoint
  • GraphQLClient supports several caching mechanisms right out of the box
  • Placing the GraphQL client in the data source layer helps you separate the queries from the widget tree

Prepare the data source

GraphQL queries are placed in the data source layer of the app


GraphQL queries are placed in the data source layer of the app

a. In the file spotify_api_datasource.dart, create a query to retrieve the artists from the server

...
@override
Future<List<Artist>> getArtists(String name) async {
  try {
    const String query = r'''
      query getArtists($name: String!) {
          queryArtists(byName: $name) {
              name
              id
              image
              albums {
                id
                name
              }
          }
      }
    ''';

    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

This query variable is where you specify the data that you need to pass or retrieve data from the server.

Similarly, this is how you use the query in a GraphQL playground.

Spotify GraphQL API


https://spotify-graphql-server.herokuapp.com/

b. Add the following QueryOptions, and assign the query variable to the gql

final response = await _client.query(
  QueryOptions(
    documentNode: gql(query),
    variables: {
      'name': name, /// Search input of the user
    },
  ),
);
Enter fullscreen mode Exit fullscreen mode

gql turns your String query into a standard GraphQL AST (a.k.a. Abstract Syntax Tree). To put it simply, Stephen Schneider once mentioned that AST is just a fancy way of saying heavily nested objects from his video.

Prepare the repository

A single repository can retrieve data from one or multiple data sources, eg. local or remote


A single repository can retrieve data from one or multiple data sources, eg. local or remote

a. In the file spotify_api_repository.dart, call the getArtists from the data source

@override
Future<Either<Failure, List<Artist>>> getArtists(String name) async {
  try {
    /// Add the following
    final data = await dataSource.getArtists(name);
    return Right(data);
  } ... catch (e) {
    return Left(UnhandledFailure());
  }
}
Enter fullscreen mode Exit fullscreen mode

In the getArtists function, you are trying to catch the Exceptions returned after you query the data source.

try {
  ...
} on OperationException catch (e) {
  /// Handle exceptions that you receive from the [GraphQLClient]
  return Left(OperationFailure(e.graphqlErrors.first.message));
} on NoResultsFoundException {
  return Left(NoResultsFoundFailure());
} on Exception {
  return Left(ServerFailure());
} catch (e) {
  return Left(UnhandledFailure());
}
Enter fullscreen mode Exit fullscreen mode

This is where the package either_option comes in handy. Either is a result type that represents a value of one of the two possible types you assigned to it.

In this project, the function is returning either a:

  • type Failure, which is a custom class that swallows an Exception with a custom message
  • or a List<Artist> which is the result that you need to populate the artists' list

MobX Integration

MobX triad


Credits: https://pub.dev/packages/mobx

MobX in a nutshell

  • observables are the notifiers to your UI observing the changes in the values
  • actions are simply functions where you can to change the values of your observables
  • reactions are listeners where you handle the side-effects coming from your store---typically used when you need to process something outside the Observer widget

Prepare the store class

A store can call functions from one or more repositories


A store can call functions from one or more repositories

a. Add a function to retrieve artists from the repository and annotate it with @action

@action
searchArtists(String name) async {
  _artistsFuture = ObservableFuture(_repository.getArtists(name));

  final either = await _artistsFuture;

  artistsResult = either.fold(
    (left) {
      /// [Failure]s are handled here
      this.reset();
      errorMessage = left.message;
      return null;
    },
    (right) {
      /// Results are handled here,
      /// in this case it is of type [List<Artist>]
      return right;
    },
  );
}
Enter fullscreen mode Exit fullscreen mode

b. Generate the store file

Run the following command to generate a store file

flutter pub run build_runner build
Enter fullscreen mode Exit fullscreen mode

If you have encountered issues regarding conflicting files, just run

flutter pub run build_runner build --delete-conflicting-outputs
Enter fullscreen mode Exit fullscreen mode

c. In the artists.screen.dart, call the function searchArtists

class _ArtistsScreenState extends State<ArtistsScreen> {
  ...

  @override
  void didChangeDependencies() {
    ...

    _store.searchArtists(_name);
  }
}
Enter fullscreen mode Exit fullscreen mode

Observing values in the UI

Final step in integrating GraphQL and MobX is to observe changes in the UI


Final step in integrating GraphQL and MobX is to observe changes in the UI

a. Load the artistsResult to the ArtistList widget

Observer(
  builder: (_) {
    switch (_store.state) {
      case StoreState.loading:
        ...
        break;

      /// Add the following widgets to display the artists
      default:
        return Expanded(
          child: SingleChildScrollView(
            child: Container(
              margin: const EdgeInsets.symmetric(horizontal: 20),
              child: ArtistsList(
                artists: _store.artistsResult ?? [],
              ),
            ),
          ),
        );
    }
  },
),
Enter fullscreen mode Exit fullscreen mode

Few things to note 💡

  • Observer rebuilds itself whenever there are changes to the value/s it observes inside its build method
  • In this Observer, it rebuilds whenever _store.state or _store.artistsResult's value is changed
  • Introduce an Observer only to the widget that needs to rebuild whenever data changes

Unit testing

This article won't be complete without adding any unit tests!

One of the benefits of separating business logic from the UI is that you get to test a small chunk of the code that makes your whole app.

Here's a snippet from the store unit tests

test(
  "should SET the [artistsResult] when the repository returns the results",
  () {
    // Arrange
    final name = "Carpenters";
    final result = [
      Artist(name: "The Carpenters"),
      Artist(name: "Carpenters"),
    ];
    when(_repository.getArtists(name)).thenAnswer(
      (_) async => Right(result),
    );

    // Act
    _store.searchArtists(name);

    // Assert
    expect(_store.artistsResult, result);
    expect(_store.artistsResult.length, 2);
    expect(_store.artistsResult.first.name, result.first.name);
  },
);
Enter fullscreen mode Exit fullscreen mode

And a snippet from the repository unit tests

test(
    "should RETURN [List<Artist>] when the data source returns the results from the server",
    () async {
  // Arrange
  final name = "Carpenters";
  final result = [
    Artist(name: "The Carpenters"),
    Artist(name: "Carpenters"),
  ];
  when(_dataSource.getArtists(name)).thenAnswer((_) async => result);

  // Act
  final response = await _repository.getArtists(name);

  // Assert
  expect(response, isA<Right<Failure, List<Artist>>>());
  expect(response.isLeft, false);
  expect(response.isRight, true);
});
Enter fullscreen mode Exit fullscreen mode

You can view all the unit tests included in this project here.

Run the tests

flutter test
Enter fullscreen mode Exit fullscreen mode

Conclusion

For brevity, I focused on the missing parts of the app that matter when integrating GraphQL and MobX to your Flutter apps. You can always read the comments I left on the source code for more details.

In contrast to the use of Mutation and Query widget builders in the widget tree using graphql_flutter, this project setup makes your app less dependent on using GraphQL's implementation.

If you are planning to include data sources from traditional SOAP or RESTful APIs in the future, you only have to implement it differently on the data sources layer and continue what you are currently doing with the other parts of your app.

Connecting to multiple remote data sources


Connecting to multiple remote data sources

That's it for now, see you on the next one!

References

This was originally posted on my blog. Say hi! 👋

Top comments (0)