Retrofit в Flutter с Dio & JSON Serializable

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 автоматизирует этот процесс.

  1. Аннотируйте свою модель с помощью @JsonSerializable()
  2. Запустите build_runner → генерирует код fromJson и toJson.
  3. 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;
}
}

Как это работает внутри:

  1. Вы вызываете метод API через Retrofit.
  2. Интерсептор запроса запускается первым — добавляет заголовки, логирует запрос.
  3. Запрос достигает сервера.
  4. Интерсептор ответа запускается — логирует данные, преобразовывает при необходимости.
  5. Если возникает ошибка, 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 шагов.!

Как всё работает вместе:

  1. Вызов API через Retrofit → 'client.getPosts()'
  2. Интерсептор Dio запускается → логирует запрос, добавляет заголовки.
  3. Запрос достигает сервера.
  4. Интерсептор ответа запускается → логирует ответ.
  5. JSON-ответ преобразуется в объекты Dart — post.fromJson().
  6. Provider обновляет интерфейс с этими объектами.

Заключение

Комбинируя Retrofit + Dio + Interceptors + JSON Serializable + Provider, мы получаем чистый, масштабируемый и легко поддерживаемый сетевой слой:

  • Чистый интерфейс API
  • Автоматическое разбор JSON
  • Централизованное логирование и обработка ошибок
  • Простое управление состоянием

Этот подход экономит время и снижает количество ошибок в любом проекте Flutter, работающем с API.

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

Report Page