Retrofit in Flutter with Dio & JSON Serializable
FlutterPulseThis article was translated specially for the channel FlutterPulseYou'll find lots of interesting things related to Flutter on this channel. Don't hesitate to subscribe!🚀

A Step-by-Step Guide for Beginners:
A Step-by-Step Guide for Beginners:
If you're building apps in Flutter, chances are you'll need to call APIs, parse JSON, and manage network states. Instead of writing repetitive boilerplate code, we can supercharge our networking layer using:Retrofit: define APIs with clean annotations
- Dio: powerful HTTP requests + interceptors
- JSON Serializable: for model generation
- Provider: state management
In this guide, we'll create a simple Post API client using jsonplaceholder.typicode.com.
Step 1: Add Dependencies
In your pubspec.yaml add
dependencies:
dio: ^5.0.0
retrofit: ^4.0.0
json_annotation: ^4.9.0
provider: ^6.0.5
dev_dependencies:
retrofit_generator: ^8.0.0
json_serializable: ^6.7.1
build_runner: ^2.4.8
Run:
flutter pub get
Step 2: Create the Model with JSON Serializable
Instead of writing 'fromJson' and 'toJson' manually, we'll generate them.
import 'package:json_annotation/json_annotation.dart';
part 'postModel.g.dart';
@JsonSerializable()
class Post {
final int userId;
final int id;
final String title;
final String body;
Post({
required this.userId,
required this.id,
required this.title,
required this.body,
});
factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
Map<String, dynamic> toJson() => _$PostToJson(this);
}
Then Run:
flutter pub run build_runner build --delete-conflicting-outputs
This creates 'PostModel.g.dart' automatically, like this.
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'postModel.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Post _$PostFromJson(Map<String, dynamic> json) => Post(
userId: (json['userId'] as num).toInt(),
id: (json['id'] as num).toInt(),
title: json['title'] as String,
body: json['body'] as String,
);
Map<String, dynamic> _$PostToJson(Post instance) => <String, dynamic>{
'userId': instance.userId,
'id': instance.id,
'title': instance.title,
'body': instance.body,
};
How JSON Serializable Works?
When the API returns JSON, we need Dart objects. JSON Serializable automates this.
- Annotate your model with @JsonSerializable()
- Run build_runner → generates fromJson & toJson code.
- Retrofit automatically converts JSON into Dart objects using Post.fr0mJson().
Benefits:
- No manual parsing
- Fewer bugs
- Easy to update if API changes
Step 3: Define API with Retrofit
Retrofit lets us declare APIs using annotations, and generates network code automatically:
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
import '../model/postModel.dart';
part 'rest_client.g.dart';
@RestApi(baseUrl: "https://jsonplaceholder.typicode.com")
abstract class RestClient {
factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
@GET("/posts")
Future<List<Post>> getPosts();
@GET("/posts/{id}")
Future<Post> getPost(@Path("id") int id);
@POST("/posts")
Future<Post> createPost(@Body() Post post);
@PUT("/posts/{id}")
Future<Post> updatePost(@Path("id") int id, @Body() Post post);
@PATCH("/posts/{id}")
Future<Post> patchPost(@Path("id") int id, @Body() Map<String, dynamic> fields);
@DELETE("/posts/{id}")
Future<void> deletePost(@Path("id") int id);
@GET("/posts")
Future<List<Post>> getPostsByUser(@Query("userId") int userId);
@GET("/posts")
Future<List<Post>> getPostsWithAuth(@Header("Authorization") String token);
}
Generate code again, Run this command:
flutter pub run build_runner build --delete-conflicting-outputs
Now, 'rest_client.g.dart' generate automatically like this, & will handle the Dio requests for you 🎉
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'rest_client.dart';
// **************************************************************************
// RetrofitGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element
class _RestClient implements RestClient {
_RestClient(
this._dio, {
this.baseUrl,
this.errorLogger,
}) {
baseUrl ??= 'https://jsonplaceholder.typicode.com';
}
final Dio _dio;
String? baseUrl;
final ParseErrorLogger? errorLogger;
@override
Future<List<Post>> getPosts() async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<List<Post>>(Options(
method: 'GET',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/posts',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
final _result = await _dio.fetch<List<dynamic>>(_options);
late List<Post> _value;
try {
_value = _result.data!
.map((dynamic i) => Post.fromJson(i as Map<String, dynamic>))
.toList();
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
return _value;
}
@override
Future<Post> getPost(int id) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<Post>(Options(
method: 'GET',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/posts/${id}',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late Post _value;
try {
_value = Post.fromJson(_result.data!);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
return _value;
}
@override
Future<Post> createPost(Post post) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(post.toJson());
final _options = _setStreamType<Post>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/posts',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late Post _value;
try {
_value = Post.fromJson(_result.data!);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
return _value;
}
@override
Future<Post> updatePost(
int id,
Post post,
) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(post.toJson());
final _options = _setStreamType<Post>(Options(
method: 'PUT',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/posts/${id}',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late Post _value;
try {
_value = Post.fromJson(_result.data!);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
return _value;
}
@override
Future<Post> patchPost(
int id,
Map<String, dynamic> fields,
) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(fields);
final _options = _setStreamType<Post>(Options(
method: 'PATCH',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/posts/${id}',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late Post _value;
try {
_value = Post.fromJson(_result.data!);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
return _value;
}
@override
Future<void> deletePost(int id) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<void>(Options(
method: 'DELETE',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/posts/${id}',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
await _dio.fetch<void>(_options);
}
@override
Future<List<Post>> getPostsByUser(int userId) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{r'userId': userId};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<List<Post>>(Options(
method: 'GET',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/posts',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
final _result = await _dio.fetch<List<dynamic>>(_options);
late List<Post> _value;
try {
_value = _result.data!
.map((dynamic i) => Post.fromJson(i as Map<String, dynamic>))
.toList();
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
return _value;
}
@override
Future<List<Post>> getPostsWithAuth(String token) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{r'Authorization': token};
_headers.removeWhere((k, v) => v == null);
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<List<Post>>(Options(
method: 'GET',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/posts',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
final _result = await _dio.fetch<List<dynamic>>(_options);
late List<Post> _value;
try {
_value = _result.data!
.map((dynamic i) => Post.fromJson(i as Map<String, dynamic>))
.toList();
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
return _value;
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
return requestOptions;
}
String _combineBaseUrls(
String dioBaseUrl,
String? baseUrl,
) {
if (baseUrl == null || baseUrl.trim().isEmpty) {
return dioBaseUrl;
}
final url = Uri.parse(baseUrl);
if (url.isAbsolute) {
return url.toString();
}
return Uri.parse(dioBaseUrl).resolveUri(url).toString();
}
}
How Retrofit Works?
Retrofit is like a smart shortcut for Dio.
Instead of writing HTTP logic manually:
- You define API endpoints using @ GET, @POST, etc.
- Retrofit generates _RestClient that uses Dio.
- When you call getPost() , it sends the request and parses the response automatically.
Benefits:
- Less boilerplate
- Easy to read & maintain
- Type-safe responses ('List<Post>' instead of 'dynamic')
Step 4: Setup Dio with Interceptors
Think of interceptors as middleware for your network requests. They let you intercept requests before they are sent, responses before they are processed, and errors when they happen.
- On Request: Modify headers, add tokens, or log requests.
- On Response: Log response data, transform it, or handle special cases.
- On Error: Retry requests, show user-friendly messages, or log errors.
Example:
import 'package:dio/dio.dart';
class DioClient {
static Dio createDio() {
final dio = Dio(BaseOptions(
baseUrl: "https://jsonplaceholder.typicode.com/posts/1",
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
));
dio.interceptors.addAll([
LogInterceptor(
request: true,
requestBody: true,
responseBody: true,
error: true,
),
InterceptorsWrapper(
onRequest: (options, handler) {
options.headers["Content-Type"] = "application/json";
return handler.next(options);
},
onError: (DioException e, handler) {
if (e.response?.statusCode == 401) {
}
return handler.next(e);
},
),
]);
return dio;
}
}
How it works internally:
- You call an API method via Retrofit.
- Request interceptor runs first- adds headers, logs request.
- The request hits the server.
- Response interceptor runs- logs data, transforms if needed.
- If an error occurs, Error interceptor handles it.
Step 5: Use Provider for State Management
Simple 'PostProvider' to fetch posts:
class PostProvider with ChangeNotifier {
final RestClient client = RestClient(Dio());
List<Post> _posts = [];
bool _loading = false;
List<Post> get posts => _posts;
bool get loading => _loading;
Future<void> fetchPosts() async {
_loading = true;
notifyListeners();
try {
_posts = await client.getPosts();
} catch (e) {
print("Error fetching posts: $e");
}
_loading = false;
notifyListeners();
}
}Step 6: Display in UI
class PostScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final provider = Provider.of<PostProvider>(context);
return Scaffold(
appBar: AppBar(title: Text("Posts")),
body: provider.loading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: provider.posts.length,
itemBuilder: (context, index) {
final post = provider.posts[index];
return ListTile(
title: Text(post.title),
subtitle: Text(post.body),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => provider.fetchPosts(),
child: Icon(Icons.refresh),
),
);
}
}Here we coverd how Retrofit work, with dio, interceptor & json Serializeable, in 6 step.!
How Everything Works Together:
- Call API via Retrofit → 'client.getPosts()'
- Dio interceptor runs → logs request, adds headers.
- Request hits the server.
- Response interceptor runs → logs response.
- JSON response is converted to Dart objects-post.fromJson().
- Provider updates the UI with these objects.
Conclusion
By combining Retrofit + Dio + Interceptors + JSON Serializable + Provider, we get a clean, scalable, and maintainable networking layer:
- Clean API interface
- Automatic JSON parsing
- Centralized logging & error handling
- Easy state management
This approach saves time and reduces bugs in any Flutter project that works with APIs.

I am open to freelance, full-time, or part-time roles, feel free to reach out at Linkedin, thank you!