Flutter HTTP Request with Dio Retrofit
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 definitiondio
is Flutter’s most popular HTTP clientjson_annotation
defines the annotations used byjson_serializable
(e.g.@JsonSerializable
)retrofit_generator
code generator forretrofit
**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/integerid
whose value is a number/integertitle
whose value is a stringbody
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 asList<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