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:
- Does anyone use MobX with GraphQL?
- Example of Mobx + GraphQL #200
- Write doc comments for all public API #308
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.
You can download the final project here.
Overview
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
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
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
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
}
}
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();
}
}
c. Provide the _spotifyApiRepository
to the ArtistsStore
Provider(
create: (_) => ArtistsStore(_spotifyApiRepository),
),
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
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
}
}
}
''';
...
}
}
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.
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
},
),
);
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. 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());
}
}
In the getArtists
function, you are trying to catch the Exception
s 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());
}
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 anException
with a custom message - or a
List<Artist>
which is the result that you need to populate the artists' list
MobX Integration
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. 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;
},
);
}
b. Generate the store file
Run the following command to generate a store file
flutter pub run build_runner build
If you have encountered issues regarding conflicting files, just run
flutter pub run build_runner build --delete-conflicting-outputs
c. In the artists.screen.dart
, call the function searchArtists
class _ArtistsScreenState extends State<ArtistsScreen> {
...
@override
void didChangeDependencies() {
...
_store.searchArtists(_name);
}
}
Observing values 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 ?? [],
),
),
),
);
}
},
),
Few things to note 💡
-
Observer
rebuilds itself whenever there are changes to the value/s it observes inside itsbuild
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);
},
);
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);
});
You can view all the unit tests included in this project here.
Run the tests
flutter test
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.
That's it for now, see you on the next one!
Top comments (0)