Retrofit в Flutter с Dio & JSON Serializable
FlutterPulseЭта статья переведена специально для канала FlutterPulse. В этом канале вы найдёте много интересных вещей, связанных с Flutter. Не забывайте подписываться! 🚀

Пошаговое руководство для начинающих:
Пошаговое руководство для начинающих:
Если вы разрабатываете приложения в Flutter, скорее всего, вам потребуется вызывать API, разбирать JSON и управлять состоянием сети. Вместо написания повторяющегося шаблонного кода, мы можем усилить наш сетевой слой с помощью:Retrofit: определение API с чистыми аннотациями
- Dio: мощные HTTP-запросы + перехватчики
- JSON Serializable: для генерации моделей
- Provider: управление состоянием
В этом руководстве мы создадим простой клиент Post API, используя jsonplaceholder.typicode.com.
Шаг 1: Добавление зависимостей
В вашем pubspec.yaml добавьте
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
Выполните:
flutter pub get
Шаг 2: Создание модели с JSON Serializable
Вместо ручного написания 'fromJson' и 'toJson'', мы сгенерируем их.
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);
}
Затем выполните:
flutter pub run build_runner build --delete-conflicting-outputs
Это автоматически создаст файл 'PostModel.g.dart' таким образом.
// 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,
};
Как работает JSON Serializable?
Когда API возвращает JSON, нам нужны объекты Dart. JSON Serializable автоматизирует этот процесс.
- Аннотируйте свою модель с помощью @JsonSerializable()
- Запустите build_runner → генерирует код fromJson и toJson.
- Retrofit автоматически преобразует JSON в объекты Dart с помощью Post.fromJson().
Преимущества:
- Нет ручного разбора
- Меньше ошибок
- Легко обновлять, если API изменится
Шаг 3: Определение API с Retrofit
Retrofit позволяет нам объявлять API с помощью аннотаций, и автоматически генерирует сетевой код:
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);
}
Сгенерируйте код снова, выполните эту команду:
flutter pub run build_runner build --delete-conflicting-outputs
Теперь, 'rest_client.g.dart' генерируется автоматически так, и будет обрабатывать запросы Dio для вас 🎉
// 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();
}
}
Как работает Retrofit?
Retrofit — это как умный ярлык для Dio.
Вместо ручного написания HTTP-логики:
- Вы определяете конечные точки API с помощью @GET, @POST, и т.д.
- Retrofit генерирует _RestClient который использует Dio.
- Когда вы вызываете getPost() , он отправляет запрос и автоматически разбирает ответ.
Преимущества:
- Меньше шаблонного кода
- Легко читать и поддерживать
- Типобезопасные ответы ('List<Post>' вместо 'dynamic')
Шаг 4: Настройка Dio с перехватчиками
Подумайте о перехватчиках как о промежуточном ПО для ваших сетевых запросов. Они позволяют перехватывать запросы перед их отправкой, ответы перед их обработкой и ошибки, когда они происходят.
- При запросе: Изменять заголовки, добавлять токены или логировать запросы.
- При ответе: Логировать данные ответа, преобразовывать их или обрабатывать специальные случаи.
- При ошибке: Повторять запросы, показывать пользователю дружелюбные сообщения или логировать ошибки.
Пример:
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;
}
}
Как это работает внутри:
- Вы вызываете метод API через Retrofit.
- Интерсептор запроса запускается первым — добавляет заголовки, логирует запрос.
- Запрос достигает сервера.
- Интерсептор ответа запускается — логирует данные, преобразовывает при необходимости.
- Если возникает ошибка, Error interceptor её обрабатывает.
Шаг 5: Использование Provider для управления состоянием
Простой 'PostProvider' для получения постов:
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();
}
}Шаг 6: Отображение в интерфейсе
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),
),
);
}
}Здесь мы рассмотрели, как работает Retrofit, с dio, интерсептором и json Serializeable, в 6 шагов.!
Как всё работает вместе:
- Вызов API через Retrofit → 'client.getPosts()'
- Интерсептор Dio запускается → логирует запрос, добавляет заголовки.
- Запрос достигает сервера.
- Интерсептор ответа запускается → логирует ответ.
- JSON-ответ преобразуется в объекты Dart — post.fromJson().
- Provider обновляет интерфейс с этими объектами.
Заключение
Комбинируя Retrofit + Dio + Interceptors + JSON Serializable + Provider, мы получаем чистый, масштабируемый и легко поддерживаемый сетевой слой:
- Чистый интерфейс API
- Автоматическое разбор JSON
- Централизованное логирование и обработка ошибок
- Простое управление состоянием
Этот подход экономит время и снижает количество ошибок в любом проекте Flutter, работающем с API.

Я открыт для фриланс-работы, полной или частичной занятости, не стесняйтесь обращаться ко мне по адресу Linkedin, спасибо!