HTTP, DIO, or GraphQL: Finding the Perfect Fit for Your Dart Project
It’s a simple app where user can sign up/log in, create posts and leave comments to posts. As a backend we used an open source project, so in this article we will consider the features of building a client.
HTTP
Dart has a built-in package called http to handle all your HTTP needs.
Advantages
- Provided by the Dart developers;
- Standard for client-server interaction.
Disadvantages
- Provides only basic functionality. Additional functionality must be implemented independently;
- Requires writing additional functions for error handling.
How to Use
First, add this dependency to our pubspec.yaml
.
dependencies:
http: ^0.13.5
Now let's see how to perform CRUD operations using http (http_crud.dart
).
Consider an example of executing a post
request:
@override
Future<RestResponseWrapper> post(String path, dynamic data) async {
final response = await http.post(
Uri.parse('${ConnectivityStrings.baseUrl}$path'),
headers: _header,
body: jsonEncode(data),
);
return RestResponseWrapper(data: response.body, status: response.statusCode);
}
The method an object of the RestResponseWrapper
class.
This class is responsible for handling responses and, in the case of http, error handling.
client/data/utils/remote_utils.dart
class RestResponseWrapper {
final dynamic data;
final int status;
final Type _type =
ConnectivityStrings.networkModule == NetworkModule.http.name ? http.Response : dio.Response;
RestResponseWrapper({required this.data, this.status = 200});
bool isSuccessful() => status >= 200 && status < 400;
bool wrongUserData() => status >= 400 && status < 500;
T? retrieveResult<T>() {
if (isSuccessful()) {
if (data.isNotEmpty && data != 'OK') {
if (_type == http.Response) {
return _createFromJSON<T>(json.decode(data.toString()))!;
} else {
return _createFromJSON<T>(data)!;
}
} else {
return null;
}
} else if (wrongUserData()) {
throw WrongUserDataException();
} else {
throw Exception();
}
}
List<T> retrieveResultAsList<T>() {
if (isSuccessful()) {
if (_type == http.Response) {
return (json.decode(data.toString()) as List).map((e) => (_createFromJSON<T>(e)!)).toList();
} else {
return (data as List).map((e) => (_createFromJSON<T>(e)!)).toList();
}
} else {
throw Exception();
}
}
}
Using status
, we check whether our request was successfully completed. If an error has occurred, we throw the appropriate exception. Otherwise decode the request body and deserialize using the _create From JSON<T>
method.
T? _createFromJSON<T>(Map<String, dynamic> json) {
Type typeOf<V>() => V;
final type = typeOf<T>();
if (type == typeOf<User>()) {
return User.fromJson(json) as T;
} else if (type == typeOf<Post>()) {
return Post.fromJson(json) as T;
} else if (type == typeOf<Comment>()) {
return Comment.fromJson(json) as T;
} else if (typeOf<dynamic>() == type || typeOf<void>() == type) {
return null;
}
throw ArgumentError('Looks like you forgot to add processing for type ${type.toString()}');
}
DIO
Dio is a powerful HTTP client for Dart. It has support for interceptors, global configuration, FormData
, request cancellation, file downloading, and timeout, among others.
Advantages
- Provides a lot of features. In addition to performing basic network stuff, dio also provides some extra functionality like interceptors, log, cache, etc;
- Allows to implement client as fast as possible. It has additional abstractions that reduce the number of code and speed up the development process.
Disadvantages
- Reliability. Due to the large number of functions provided, the probability of a bug in the package is higher.
How to Use
First, add the Dio package to your pubspec.yaml
:
dependencies:
dio: ^4.0.6
The next step is Dio Initialization:
final _dio = Dio();
DioRestRemoteDataSource() {
_dio.options.baseUrl = ConnectivityStrings.baseUrl;
}
In this case, the execution of the request looks a little different:
@override
Future<RestResponseWrapper> post(String path, dynamic data) async {
try {
final response = await _dio.post(path, data: data.toJson());
return RestResponseWrapper(data: response.data);
} on DioError catch (e) {
throw _getExceptionByStatusCode(e);
}
}
Error handling is performed differently. We need to wrap the execution of the request in a try-catch. In order to determine the source of the error, we use the method _getExceptionByStatusCode
:
Exception _getExceptionByStatusCode(DioError e) {
return e.response?.statusCode == 401 || e.response?.statusCode == 409
? WrongUserDataException()
: Exception('Error: ${e.response?.statusMessage} \\n Error code: ${e.response?.statusCode}');
}
In this case, when calling the retrieveResult
method, we only perform deserialization.
http vs DIO
Given the peculiarities of these approaches, I can single out the following use cases:
By comparing the http package with Dio, Dio covers most of the standard networking cases with minimal effort, while http provides only basic functionality. So for the most projects where development speed is important, Dio is perfect.
But for large projects with long-term support, sometimes it’s better to choose http and then write missing methods yourself, since Dio is not from the Dart developers.
If you are still don’t know what to choose take a look at GraphQL.
GraphQL (bonus)
GraphQL is a query language for Web APIs, that was created by Facebook in 2012 and open-sourced in 2015.
Package GraphQL is a client for dart modeled on the apollo client, and is currently the most popular GraphQL client for dart.
Advantages
- No overfetching and underfetching. It can fetch only the information you need;
- Support subscriptions for real time data. Subscriptions are useful for notifying your client in real time about data changes, such as the creation of a new object or updates to some fields;
- Type safety. The response schema describes the type of each field and its mandatory nature in the request.
Disadvantages
- Large queries. If we have entities with a large number of fields, then writing queries becomes monotonous and takes extra time;
- Error reporting. Queries always return a HTTP status code of
200
, even if the request failed with an error. If your query is unsuccessful, your response JSON will have aerror
key with error message. This makes error handling more difficult.
How to Use
Add a dependency to your pubspec.yaml
:
dependencies:
graphql: ^5.1.1
Let's see what the query and mutation look like:
static const String getCommentsByPostId = r'''
query GetComments($postId: ID!) {
comments (postId: $postId) {
id,
authorName,
text,
date,
userId,
postId
}
}
''';
static const String createUser = r'''
mutation CreateUserMutation($name: String!, $password: String!) {
createUserMutation(input: { name: $name, password: $password }) {
error
}
}
''';
client/data/utils/graphql_query_strings.dart
file.Now let's take care of the client himself.
In the class constructor, initialize link and GraphQl client:
late final GraphQLClient _client;
GraphqlRemoteDataSource() {
final Link link = HttpLink(
ConnectivityStrings.baseUrl,
);
_client = GraphQLClient(
cache: GraphQLCache(),
defaultPolicies: DefaultPolicies(
query: Policies(
fetch: FetchPolicy.networkOnly,
),
),
link: link,
);
}
Using DefaultPolicies
, you can configure various caching usage options in your application.
Consider an example of performing a mutation:
@override
Future<void> createComment(Comment comment) async {
final MutationOptions options = MutationOptions(
document: gql(GraphQLQueryStrings.createComment),
variables: <String, dynamic>{
'postId': comment.postId,
'userId': comment.userId,
'authorName': comment.authorName,
'text': comment.text,
'date': comment.date.toString(),
},
);
final QueryResult result = await _client.mutate(options);
return result.retrieveResult();
}
We pass the mutation string as a document
parameter, and specify the necessary values in the variables
.
In this case, we use the extension to process responses:
extension ResponseTo on QueryResult {
T? retrieveResult<T>({String? tag}) {
T? result;
if (exception != null) {
throw Exception('Error: ${exception.toString()}');
} else {
if (tag != null) {
result = data![tag] == null
? null
: data![tag]['error'] != null
? throw WrongUserDataException()
: _createFromJSON<T>(data![tag]);
}
return result;
}
}
List<T> retrieveResultAsList<T>({required String tag}) {
List<T> result;
if (exception != null) {
throw Exception('Error: ${exception.toString()}');
} else {
result = (data![tag] as List).map((e) => (_createFromJSON<T>(e)!)).toList();
return result;
}
}
}
The difference between the previous examples is only in access to the necessary data.
HTTP REST vs GraphQL
HTTP REST is the most popular and used API building technology. However, it has a disadvantage, which in some cases is an important aspect - Overfetching and underfetching. Due to strictly defined endpoints, there are cases when we receive more data than necessary, or we need to execute several requests at once.
As we saw earlier, GraphQL solves this problem by returning only the necessary data, but it also has its own nuances.
The choice of technology depends on the size, complexity and features of the application.
For small applications, using graphql is not very convenient, the process can become more complicated and take more time. It is also not the best choice for small teams. GraphQL requires a large team capable of minimizing performance risks.
Anyway your team leader knows more than you and they’ll decide which technology would be better for the particular project 😁
Useful links: