Пакет dio во Flutter: запросы, ошибки, перехватчики, скачивание и отправка файлов
Егор Долгов · 12.11.2026Сначала выберите систему — команды подставятся под неё.
dio — пакет для HTTP-запросов в Dart и Flutter, как и
http
из
Урока 7
. Он умеет больше: общие настройки для всех запросов, перехватчики, скачивание и отправку файлов с прогрессом. Отменять запросы умеют оба пакета. В этой статье разберём, что это даёт и как этим пользоваться.
Содержание:
- Когда брать dio, а когда хватит http
- Подключение и запуск примеров
- Первый запрос
- Ошибки: код 404 — уже исключение
- Общие настройки: baseUrl и таймауты
- POST: отправляем JSON
- Перехватчики: код перед каждым запросом
- Отмена запроса
- Скачивание файла с прогрессом
- Отправка файлов на сервер
- dio во Flutter-приложении
- http и dio: отличия по пунктам
- Частые ошибки
- Итог
1. Когда брать dio, а когда хватит http
Задача: приложение ходит к одному серверу десятками запросов. Каждому нужен один и тот же адрес сервера, токен авторизации и ограничение по времени, а ошибки хочется обрабатывать в одном месте. В пакете
http
нет общих настроек и перехватчиков — их пришлось бы писать самому. В dio это встроено:
- общие настройки — адрес сервера, таймауты и заголовки задаются один раз для всех запросов;
- перехватчики — код, который выполняется перед каждым запросом и после каждого ответа;
-
отмена запроса
— например, когда пользователь ушёл с экрана (в
httpдля этого естьAbortableRequest); - скачивание и отправка файлов с отчётом о прогрессе.
Если запросов пара и всё это не нужно, достаточно
http
из Урока 7. Официальные руководства Flutter тоже используют
http
: например,
Fetch data from the internet
. Отдельного руководства по dio в документации Flutter нет.
dio выпускает команда CFUG — издатель flutter.cn на pub.dev, исходники — на GitHub .
2. Подключение и запуск примеров
Во Flutter-проекте в папке проекта выполните:
flutter pub add dio
В
pubspec.yaml
появится строка
dio: ^5.11.1
. В коде пакет подключают так:
import 'package:dio/dio.dart';
Примеры в разделах 3–10 — короткие программы для консоли: так виден результат каждого запроса. Отдельный Flutter-проект для них не нужен, а команда
dart
у вас уже есть —
Flutter SDK включает полный Dart SDK
. Создайте консольный проект и подключите dio:
dart create dio_demo
cd dio_demo
dart pub add dio
Каждый пример — отдельная программа: вставьте его в
bin/dio_demo.dart
вместо того, что там есть, и запустите командой
dart run
. Пример во Flutter-приложении — в разделе 11.
3. Первый запрос
Задача: получить с сервера запись номер 1. Запросим запись с тестового сервера JSONPlaceholder , как в Уроке 7:
import 'package:dio/dio.dart';
Future<void> main() async {
final dio = Dio();
final response = await dio.get(
'https://jsonplaceholder.typicode.com/posts/1',
);
print(response.statusCode);
print(response.data.runtimeType);
print(response.data['title']);
}
Результат:
200
_Map<String, dynamic>
sunt aut facere repellat provident occaecati excepturi optio reprehenderit
-
Dio()— клиент, через который идут запросы. -
dio.get(адрес)— GET-запрос. Адрес — обычная строка, в отличие отhttp, где нуженUri. -
response.data— ответ, уже разобранный из JSON : этоMap, поэтомуresponse.data['title']работает сразу, безjsonDecode(). dio разбирает ответ, если сервер прислал его с типомapplication/json.
4. Ошибки: код 404 — уже исключение
Это первое отличие, которое вы заметите после Урока 7. Там ответ с кодом 404 приходил как обычный ответ, и код проверяли сами. dio по умолчанию считает успешными только коды от 200 до 299 (это меняет параметр
validateStatus
), а на любой другой бросает исключение
DioException
:
import 'package:dio/dio.dart';
Future<void> load(Dio dio, String url) async {
try {
final response = await dio.get(url);
print('OK ${response.statusCode}');
} on DioException catch (e) {
print('${e.type}: ${e.response?.statusCode}');
}
}
Future<void> main() async {
final dio = Dio();
await load(dio, 'https://jsonplaceholder.typicode.com/posts/0');
await load(dio, 'https://no-such-host.invalid/posts/1');
}
Результат:
DioExceptionType.badResponse: 404
DioExceptionType.connectionError: null
-
on DioException catch (e)ловит любую ошибку запроса. -
e.type— вид ошибки.badResponse— сервер ответил кодом вне 200–299,connectionError— до сервера не достучались (здесь — такого сервера нет). -
e.response?.statusCode— код ответа. Когда ответа нет вовсе,e.responseравенnull.
Ещё виды ошибок:
connectionTimeout
,
sendTimeout
,
receiveTimeout
— вышло время,
cancel
— запрос отменили,
unknown
— другая ошибка, исходное исключение лежит в
e.error
. Так приходит, например, ошибка сертификата: у просроченного или самоподписанного сертификата
e.type
—
unknown
, а в
e.error
—
HandshakeException
. Вид
badCertificate
бывает, только если вы задали свою проверку сертификата. Полный список — в справке
DioExceptionType
.
5. Общие настройки: baseUrl и таймауты
Задача: не повторять в каждом запросе адрес сервера и ограничения по времени. Их задают один раз в
BaseOptions
:
import 'package:dio/dio.dart';
final dio = Dio(
BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 10),
),
);
Future<void> main() async {
final post = await dio.get('/posts/1');
final user = await dio.get('/users/1');
print(post.data['title']);
print(user.data['name']);
}
Результат:
sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Leanne Graham
-
baseUrl— начало адреса. Запросdio.get('/posts/1')уйдёт наhttps://jsonplaceholder.typicode.com/posts/1. -
connectTimeout— сколько ждать соединения с сервером. -
receiveTimeout— сколько ждать данных: первых байтов ответа и каждой следующей порции. Это не предел на весь ответ: большой файл может качаться дольше, если данные идут без пауз. - По умолчанию dio не ограничивает время ожидания — запрос может висеть очень долго. Поэтому таймауты стоит задать.
-
Здесь же задают заголовки для всех запросов — параметр
headers.
Когда время вышло, dio бросает
DioException
с соответствующим видом. Проверим с нарочно маленьким
receiveTimeout
:
import 'package:dio/dio.dart';
Future<void> main() async {
final dio = Dio(
BaseOptions(receiveTimeout: const Duration(milliseconds: 1)),
);
try {
await dio.get('https://jsonplaceholder.typicode.com/posts');
} on DioException catch (e) {
print(e.type);
}
}
Результат:
DioExceptionType.receiveTimeout
.
Авторы dio советуют держать в приложении один экземпляр `Dio` : тогда настройки едины для всех запросов. В примере выше он объявлен один раз на верхнем уровне файла.
6. POST: отправляем JSON
Задача: отправить на сервер новую запись.
import 'package:dio/dio.dart';
Future<void> main() async {
final dio = Dio(BaseOptions(baseUrl: 'https://jsonplaceholder.typicode.com'));
final response = await dio.post(
'/posts',
data: {'title': 'Привет', 'body': 'Мой первый пост', 'userId': 1},
);
print(response.statusCode);
print(response.requestOptions.contentType);
print(response.data);
}
Результат:
201
application/json
{title: Привет, body: Мой первый пост, userId: 1, id: 101}
-
data:— тело запроса.Mapdio сам превращает в JSON —jsonEncode()не нужен. -
Заголовок
Content-Type: application/jsondio тоже ставит сам — это видно во второй строке результата. - В ответ — код 201 «создано» и запись с номером 101 (JSONPlaceholder ничего не сохраняет, а только отвечает так, будто сохранил).
7. Перехватчики: код перед каждым запросом
Задача: видеть в консоли каждый запрос и ответ, а к каждому запросу добавлять токен авторизации — например, полученный после входа в приложение. Постоянный заголовок проще задать в
BaseOptions
, а здесь нужен код, который выполняется при каждом запросе. Это перехватчик (interceptor): код, который dio вызывает перед каждым запросом и после каждого ответа.
import 'package:dio/dio.dart';
Future<void> main() async {
final dio = Dio(BaseOptions(baseUrl: 'https://jsonplaceholder.typicode.com'));
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer my-token';
print('→ ${options.method} ${options.uri}');
handler.next(options);
},
onResponse: (response, handler) {
print('← ${response.statusCode}');
handler.next(response);
},
),
);
await dio.get('/posts/1');
await dio.get('/users/1');
}
Результат:
→ GET https://jsonplaceholder.typicode.com/posts/1
← 200
→ GET https://jsonplaceholder.typicode.com/users/1
← 200
-
onRequestвызывается перед каждым запросом: здесь мы добавляем заголовокAuthorizationи печатаем адрес. -
onResponse— после каждого ответа,onError— при каждой ошибке. -
handler.next(...)передаёт запрос или ответ дальше — следующему перехватчику. Вызывайте его в конце каждого обработчика, как в примере.
Для журнала запросов в dio есть готовый перехватчик
`LogInterceptor`
. Тела запросов и ответов он по умолчанию не печатает, а во Flutter авторы dio советуют выводить его журнал через
debugPrint
:
LogInterceptor(logPrint: (o) => debugPrint(o.toString()))
.
8. Отмена запроса
Задача: пользователь ушёл с экрана, а запрос ещё идёт — его результат уже не нужен. Запрос можно отменить через
CancelToken
:
import 'package:dio/dio.dart';
Future<void> main() async {
final dio = Dio();
final cancelToken = CancelToken();
final request = dio.get(
'https://jsonplaceholder.typicode.com/posts',
cancelToken: cancelToken,
);
cancelToken.cancel('Пользователь ушёл с экрана');
try {
await request;
} on DioException catch (e) {
print('${e.type}: ${e.error}');
}
}
Результат:
DioExceptionType.cancel: Пользователь ушёл с экрана
.
-
cancelToken:передаётся в запрос. -
cancelToken.cancel(причина)отменяет его: запрос завершается исключениемDioExceptionс видомcancel, а вe.error— причина. -
Одним
CancelTokenможно отменить сразу несколько запросов.
9. Скачивание файла с прогрессом
Задача: скачать большой файл и показывать, сколько уже скачано. Для этого есть
dio.download
— он сохраняет ответ в файл и по ходу сообщает о прогрессе:
import 'package:dio/dio.dart';
Future<void> main() async {
final dio = Dio();
await dio.download(
'https://jsonplaceholder.typicode.com/photos',
'photos.json',
onReceiveProgress: (received, total) {
if (total > 0) {
print('${(received / total * 100).toStringAsFixed(0)}%');
} else {
print('$received байт');
}
},
);
print('Готово');
}
Результат (начало и конец):
602 байт
9309 байт
…
1039898 байт
Готово
-
Второй аргумент — путь к файлу, куда сохранить. В консоли это просто имя файла в текущей папке, а в приложении путь берут в папке приложения — например, через
getTemporaryDirectory()из пакета path_provider, как в справке `download` . -
onReceiveProgressвызывается по мере скачивания:received— сколько байт получено,total— полный размер. -
Если сервер не сообщил размер (заголовок
content-length) или прислал данные сжатыми (заголовокcontent-encoding: gzip, как здесь),totalравен −1. Тогда показать проценты нельзя, только объём скачанного. -
В вебе браузер не даёт писать файлы куда угодно: там
downloadскачивает файл в память и предлагает сохранить его, а путь становится предлагаемым именем файла .
Большой файл и память.
На телефоне и компьютере
download
не держит файл в памяти целиком: он записывает на диск каждую пришедшую порцию и только потом принимает следующую. В нашей проверке память программы не росла вместе с размером файла: при скачивании файла в 3 ГБ она была такой же, как при скачивании средствами самого Dart (
dart:io
). Поэтому большие файлы — видео, архивы, обновления — скачивайте через
download
.
10. Отправка файлов на сервер
Задача: отправить на сервер фотографию — например, в профиль или в альбом. Чтобы отправить файл вместе с полями формы, используют формат
multipart/form-data
, в dio для этого есть
FormData
.
Для этого примера нужны две вещи:
-
сервер, который принимает файлы.
В коде вместо его адреса стоит заглушка
https://example.com/upload— подставьте адрес своего сервера. Какой адрес и какие имена полей он ждёт, написано в его документации; -
картинка
photo.jpgв папке проектаdio_demo, рядом сpubspec.yaml: путь к файлу отсчитывается от папки, где вы запускаетеdart run. Если файла нет, программа остановится с ошибкойPathNotFoundException.
import 'package:dio/dio.dart';
const uploadUrl = 'https://example.com/upload';
Future<void> main() async {
final dio = Dio();
final formData = FormData.fromMap({
'title': 'Отпуск',
'photo': await MultipartFile.fromFile('photo.jpg'),
});
final response = await dio.post(uploadUrl, data: formData);
print(response.statusCode);
print(response.data);
}
Что придёт в ответ, решает сервер. Наш тестовый сервер ответил кодом 200 и списком того, что получил:
200
{fields: {title: Отпуск}, files: [{field: photo, filename: photo.jpg, size: 150000}]}
-
FormData.fromMapсобирает форму: обычные поля ('title') и файлы ('photo') — вместе. -
MultipartFile.fromFile(путь)— файл с диска. Читается он по частям , а не целиком: в нашей проверке память программы при отправке файла в 1 ГБ почти не выросла. -
data: formData— форма уходит телом POST-запроса, заголовокContent-Typedio ставит сам. -
В вебе путей к файлам нет, и
fromFileтам бросаетUnsupportedError. Файл передают байтами — черезMultipartFile.fromBytes.
Несколько файлов
кладут списком в одно поле, а прогресс отправки показывает
onSendProgress
:
import 'package:dio/dio.dart';
const uploadUrl = 'https://example.com/upload';
Future<void> main() async {
final dio = Dio();
final formData = FormData.fromMap({
'photos': [
await MultipartFile.fromFile('photo1.jpg'),
await MultipartFile.fromFile('photo2.jpg'),
],
});
final response = await dio.post(
uploadUrl,
data: formData,
onSendProgress: (sent, total) {
print('${(sent / total * 100).toStringAsFixed(0)}%');
},
);
print(response.data);
}
Результат (начало и конец, ответ нашего тестового сервера):
0%
13%
…
100%
{fields: {}, files: [{field: photos, filename: photo1.jpg, size: 200000}, {field: photos, filename: photo2.jpg, size: 300000}]}
-
В поле со списком каждый файл уходит под одним и тем же именем —
photos, это видно в ответе сервера. В README dio написано, что имя станетfiles[], но dio 5.11.1 отправляет его без скобок. -
Если сервер ждёт имя со скобками —
photos[], так принято, например, в PHP, — передайте вторым аргументомListFormat.multiCompatible:FormData.fromMap({...}, ListFormat.multiCompatible). -
onSendProgressустроен какonReceiveProgressиз раздела 9:sent— сколько байт отправлено,total— сколько всего. УFormDataразмер известен заранее, поэтому проверка на −1 здесь не нужна. -
FormDataиMultipartFileгодятся для одного запроса. Чтобы отправить ещё раз, создайте их заново, как советует README dio , или сделайте копию черезformData.clone().
11. dio во Flutter-приложении
Вот функция
fetchPost
из Урока 7, переписанная на dio, вместе с экраном, который загружает запись при открытии. В проекте из Урока 7 выполните
flutter pub add dio
и замените
lib/main.dart
целиком. Разрешения на сеть из Урока 7 (раздел 2) нужны и здесь.
import 'package:dio/dio.dart';
import 'package:material_ui/material_ui.dart';
final dio = Dio(
BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 10),
),
);
class Post {
const Post({required this.id, required this.title, required this.body});
final int id;
final String title;
final String body;
factory Post.fromJson(Map<String, dynamic> json) {
return switch (json) {
{'id': int id, 'title': String title, 'body': String body} => Post(
id: id,
title: title,
body: body,
),
_ => throw const FormatException('Не удалось прочитать пост'),
};
}
}
Future<Post> fetchPost(int id) async {
final response = await dio.get('/posts/$id');
return Post.fromJson(response.data as Map<String, dynamic>);
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('dio')),
body: const PostView(),
),
);
}
}
class PostView extends StatefulWidget {
const PostView({super.key});
@override
State<PostView> createState() => _PostViewState();
}
class _PostViewState extends State<PostView> {
late Future<Post> _post;
@override
void initState() {
super.initState();
_post = fetchPost(1);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: FutureBuilder<Post>(
future: _post,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Ошибка: ${snapshot.error}');
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final post = snapshot.data!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(post.title, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text(post.body),
],
);
},
),
);
}
}
Что изменилось по сравнению с Уроком 7:
-
адрес сервера и таймауты заданы один раз в
BaseOptions; -
нет
jsonDecode()—response.dataужеMap; -
нет проверки
statusCode: при коде не из 200–299 dio сам броситDioException, иFutureBuilderпокажет «Ошибка: …».
12. http и dio: отличия по пунктам
-
Ответ с кодом 404 или 500.
http — обычный ответ, код проверяете сами. dio — исключение
DioExceptionс видомbadResponse. -
JSON в ответе.
http —
jsonDecode(response.body). dio —response.dataуже разобран. -
JSON в запросе.
http —
jsonEncode()и заголовокContent-Typeвручную. dio —data: {...}, остальное сам. -
Таймаут.
http —
.timeout()уFuture. dio —connectTimeout,receiveTimeout,sendTimeoutв настройках. - Общие заголовки и журнал. http — свой класс-обёртка над `Client` . dio — перехватчики.
-
Отмена.
http —
AbortableRequest(с версии 1.5.0). dio —CancelToken. -
Скачивание с прогрессом.
Готовый метод есть только в dio —
downloadсonReceiveProgress. -
Отправка файлов.
http —
MultipartRequest. dio —FormDataвdata:, прогресс —onSendProgress. - Кто выпускает. http — команда Dart ( dart.dev ). dio — CFUG ( flutter.cn ).
13. Частые ошибки
-
DioErrorустарел. Класс ошибки с версии dio 5.2.0 называетсяDioException,DioErrorоставлен как устаревший. В старых примерах из интернета заменитеDioErrorнаDioException. -
Ждут код 404 в ответе, а получают исключение.
Если вы привыкли к
http, ответ с кодом ошибки нужно ловить вon DioException catch (e)и смотретьe.response?.statusCode. -
Новый
Dio()на каждый запрос. Тогда настройки и перехватчики приходится задавать каждый раз заново. Создайте один экземпляр на приложение. -
Запрос висит очень долго.
Таймауты по умолчанию не заданы — укажите их в
BaseOptions.
Ошибка сертификата (
HandshakeException
внутри
DioException
) и как доверять своему сертификату — в отдельной статье о HTTPS и сертификатах (готовится).
14. Итог
-
dio — HTTP-клиент с общими настройками (
BaseOptions), перехватчиками, отменой (CancelToken), скачиванием (download) и отправкой файлов (FormData). -
На телефоне и компьютере большие файлы
downloadпишет на диск порциями, аMultipartFile.fromFileчитает по частям — файл целиком в память не попадает. -
response.dataуже разобран из JSON, аMapвdata:отправляется как JSON. -
По умолчанию ответ с кодом вне 200–299 — это исключение
DioException, вид ошибки — вe.type. - Таймауты по умолчанию не заданы — задайте их сами.
-
Для пары простых запросов достаточно пакета
http— Урок 7 .
Вопросы и предложения по статье присылайте на contact@flutter.su.