Flutter HTTP Request with Dio Retrofit

Flutter HTTP Request could be fun

Madacode
6 min readJan 12, 2023

Making a HTTP Request is one core activities that we do as an App/Front End developer. It is what enables our app to talk to backend services, thus do its magic.

While at its core HTTP requests are simple, having many manually-hand-made simple HTTP requests could be a boomerang as in its sheer volume of code to maintain and vulnerability of having a human-error which increases as the number of backend services being consumed in our app.

Fortunately, it does not have to be this way in Flutter, we can have instead automate the heavy lifting of hand-making HTTP request function and instead just put our request and response model and its endpoint then let code generator do the rest for us with the help of retrofit, retrofit_generator and build_runner package .

In this article, we will explore and get our hands dirty on how to automate our mundane HTTP request tasks and have our time for more value-creating things to do 🙌.

First, prepare our repo to work on, let’s call it retrofit_dio_example

flutter create retrofit_dio_example

note: if you have fvm installed, use fvm flutter to run flutter instead

After our app had been ready, open your favorite editor in the retrofit_dio_example and add these lined in your pubspec.yaml file

dependencies:
#... some other dependencies
retrofit: '^2.0.1'
dio: ^4.0.6
dev_dependencies:
#... some other dependencies
json_annotation: ^4.1.0
retrofit_generator: '^2.0.1+2'
build_runner: '^2.1.4'
json_serializable: '>4.4.0'

The dependencies mentioned are:

  • retrofit is a HTTP client made around dio, used to generate dio functions from your API definition
  • dio is Flutter’s most popular HTTP client
  • json_annotation defines the annotations used by json_serializable (e.g. @JsonSerializable)
  • retrofit_generator code generator for retrofit
  • **build_runner the command line to run code genetators**
  • json_serializable to generate toJson and fromJson method of a class with @JsonSerializable annotation on top of it.

Save the pubspec.yaml and run flutter pub get on your Terminal.

With relevant packages installed, let’s start coding and get our hands dirty 🙌

Let’s start with the API, we are going to use JSON placeholder API to get dummy posts, the URL for such API is located https://jsonplaceholder.typicode.com/posts. If we open the API from the browser we can see the response format as such

[
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto"
},
{
"userId": 1,
"id": 2,
"title": "qui est esse",
"body": "est rerum tempore vitae\\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\\nqui aperiam non debitis possimus qui neque nisi nulla"
},
... other posts
]

Given such output from JSON Placeholder API, we can infer that each item has fields

  • userID whose value is a number/integer
  • id whose value is a number/integer
  • title whose value is a string
  • body whose value is a string

With that inference, we can start building out model, lets name it Post and save it in core/models/post.dart

import 'package:json_annotation/json_annotation.dart';

part 'post.g.dart';
@JsonSerializable()
class Post {
Post({
this.userId,
this.id,
this.title,
this.body,
});

factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);

int? userId;
int? id;
String? title;
String? body;

Map<String, dynamic> toJson() => _$PostToJson(this);
}

Notice that the fromJson and toJson function points to some private function with dollar sign. Those functions are generated later on by the build_runner.

Now, let’s move on to the API side. As we had known, the endpoint we want to call is GET /posts. To make Retrofit’s API definition, we need:

  • HTTP Method of given endpoint
  • Endpoint path
  • Data type of the Response

Let’s make the abstract class named PostApi containing our declaration of AP we want to call:

import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
import 'package:retrofit_dio_example/core/models/post.dart';

part 'post_api.g.dart';

@RestApi()
abstract class PostApi {
factory PostApi(Dio dio, {String baseUrl}) = _PostApi;

@GET('/posts')
Future<List<Post>> getPosts();
}
  • @GET is the endpoint’s HTTP method.
  • Endpoint name is placed inside the parentheses of @GET.
  • Future<List<Post>> is data type returned as a result/response from calling the API. All responses are wrapped by future because network calls are async by default. Result data type is defined as List<Post> because the response format (see above) is directly a list, not wrapped in some JSON object.

For API endpoints that may have path parameters, request body, query strings or custom header, follow this example:

import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
import 'package:retrofit_dio_example/core/models/post.dart';

part 'post_api.g.dart';

@RestApi()
abstract class PostApi {
factory PostApi(Dio dio, {String baseUrl}) = _PostApi;

@GET('/post/{id}')
Future<Post> getPostByID({
@Path('id') required int id,
});

@GET('/post/search')
Future<List<Post>> getPostByID({
@Query('query') required String query,
});

@POST('/posts')
Future<String> getPosts({
@Header('source') required String source,
@Body() required CreatePostRequest request,
});
}
  • For path param, {id} is used to denote path param to be passed, which later defined as function parameter with @Path('id') annotation, the name defined in annotation must match with the path param specified in the curly braces.
  • For query strings, @Query('query') annotation is used before the function parameter definition, the name specified in the annotation must match the API’s Endpoint’s query parameter name.
  • For extra custom header, @Header('source') annotation is used before the function parameter definition, the name specified in the annotation must match the API’s Endpoint’s custom header name.
  • To append request body, @Body() annotation is used before the function parameter definition. Request body usually are @JSONSerializable object.

With our response model and API definition set up, lets generate the code which does the heavy lifting for us by running

flutter pub run build_runner build --delete-conflicting-outputs

--delete-conflicting-outputs are used to automatically remove and regenerate *.g.dart files which may have its output conflicting due to code changes without stopping due to conflict (which we don’t care about since they are automatically generated anyways).

(base) armadanasar@Armadas-MacBook-Pro retrofit_dio_example % fvm flutter pub run build_runner build --delete-conflicting-outputs               
[INFO] Generating build script...
[INFO] Generating build script completed, took 575ms

[INFO] Initializing inputs
[INFO] Reading cached asset graph...
[INFO] Reading cached asset graph completed, took 97ms

[INFO] Checking for updates since last build...
[INFO] Checking for updates since last build completed, took 600ms

[INFO] Running build...
[INFO] 1.1s elapsed, 0/3 actions completed.
[INFO] 2.1s elapsed, 0/3 actions completed.
[INFO] 9.8s elapsed, 0/3 actions completed.
[INFO] 10.9s elapsed, 4/5 actions completed.
[INFO] Running build completed, took 11.0s

[INFO] Caching finalized dependency graph...
[INFO] Caching finalized dependency graph completed, took 30ms

[INFO] Succeeded after 11.0s with 2 outputs (6 actions)

If your Terminal output matches this example, then you’re good to go.

Let’s move on to the juiciest part, that is displaying the API call results. open lib/main.dart, and change the body part in build method in _MyHomePageState class to be:

body: _buildBody(context),

Then, create the function _buildBody to actually make the HTTP request and render the list of posts from the JSON Placeholder API

FutureBuilder<List<Post>> _buildBody(
BuildContext context,
) {
final PostApi client = PostApi(
Dio(
BaseOptions(
contentType: "application/json",
baseUrl: '<https://jsonplaceholder.typicode.com>'),
),
);

return FutureBuilder<List<Post>>(
future: client.getPosts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
final List<Post> posts = snapshot.data ?? <Post>[];
return _buildPosts(context, posts);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
);
}
Widget _buildPosts(BuildContext context, List<Post> posts) {
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final Post item = posts[index];
return Padding(
padding: EdgeInsets.all(16),
child: Text(
item.title ?? '',
),
);
},
);
}

For this post, Dio client is defined inside our _buildBody function, but there are actually better ways to declare Dio client and similar services which does not involve declaring it inside our View, thus making our code cleaner. Stay tune for the next post to see such implementation.

Our list is rendered with FutureBuilder which listens to the API call future’s state changes and reflecting it into the UI, that is a list view built by _buildPosts.

Now, lets run the app with flutter run and see the results

If you had done it correctly, then you would see list of posts like on the screenshot.

That’s all for this story, next on, we’d explore on how to make this code cleaner through usage of MVVM pattern and Provider state management, thereby separating business logic from UI.

Thanks for reading this story and see you all in the next post.

To see complete code repository for your reference, please kindly check it out at https://github.com/aramadsanar/flutter_retrofit_example

--

--

Madacode

Senior Software Engineer @ Pinhome with 5+ years experience in various technologies including Go and Flutter. All posts are of my own opinion.