Dio
Dart/Flutter向けの強力なHTTPクライアントライブラリ。インターセプター、グローバル設定、FormData、リクエストキャンセル、ファイルダウンロード、タイムアウト機能を提供。豊富な機能と優れたドキュメントにより、Flutter開発で広く採用される。
GitHub概要
cfug/dio
A powerful HTTP client for Dart and Flutter, which supports global settings, Interceptors, FormData, aborting and canceling a request, files uploading and downloading, requests timeout, custom adapters, etc.
トピックス
スター履歴
ライブラリ
Dio
概要
DioはDart/Flutter向けの強力なHTTPクライアントライブラリです。グローバル設定、インターセプター、FormData、リクエストキャンセル機能、ファイルアップロード/ダウンロード、タイムアウト設定、カスタムアダプターなど、包括的なHTTP通信機能を提供します。「人間のためのHTTP」をコンセプトに、複雑なHTTP処理をシンプルで直感的なAPIで実現。Flutter開発において事実上の標準HTTPクライアントとして広く採用され、モバイルアプリからWebアプリまで幅広いプラットフォームでの開発を支援しています。
詳細
Dio 2025年版はDart/Flutterエコシステムで最も信頼される包括的HTTPクライアントライブラリとして確固たる地位を維持しています。豊富なインターセプター機能により認証、ログ、エラーハンドリングの統一管理が可能で、FormDataサポートによるファイルアップロード機能、進捗状況の追跡、リクエストキャンセル機能など、モダンなアプリケーション開発に必要な全機能を網羅。プラットフォーム固有のアダプター(IOHttpClientAdapter、BrowserHttpClientAdapter)により、ネイティブとWebの両環境で最適なパフォーマンスを発揮し、企業レベルのアプリケーション開発において必須のライブラリです。
主な特徴
- 包括的インターセプター: リクエスト、レスポンス、エラー処理の統一管理
- FormData完全サポート: マルチパートフォームデータとファイルアップロード対応
- プラットフォーム最適化: ネイティブ/Web環境別の専用アダプター
- 進捗追跡機能: アップロード/ダウンロードの詳細な進捗監視
- リクエストキャンセル: CancelTokenによる柔軟なリクエスト制御
- グローバル設定: BaseOptionsによる統一的な設定管理
メリット・デメリット
メリット
- Dart/Flutterエコシステムでの圧倒的な普及率と豊富な学習リソース
- インターセプター機能による認証、ログ、エラーハンドリングの統一管理
- FormDataとファイルアップロード機能の完全サポートによる実用性
- プラットフォーム固有アダプターによる最適なパフォーマンス
- 進捗追跡とキャンセル機能による優れたユーザーエクスペリエンス
- グローバル設定による開発効率の向上とコード一貫性確保
デメリット
- 多機能であるためシンプルなHTTPリクエストには複雑すぎる場合がある
- FormDataとMultipartFileの一回使用制限による再利用時の注意点
- 豊富な設定オプションによる学習コストの増加
- インターセプターチェーンの複雑化によるデバッグの困難さ
- 大量の依存関係によるアプリサイズへの影響
- プラットフォーム固有の制約による一部機能の制限
参考ページ
書き方の例
インストールと基本セットアップ
dependencies:
dio: ^5.4.0
# セキュリティ強化版(推奨)
dependencies:
dio: ^5.4.0
dio_certificate_pinning: ^6.0.0
# 追加プラグイン
dev_dependencies:
dio_compatibility_layer: ^3.0.1
dio_web_adapter: ^1.0.0
native_dio_adapter: ^1.2.0
import 'package:dio/dio.dart';
// 基本的なDioインスタンスの作成
final dio = Dio();
// 設定付きDioインスタンス
final dioWithOptions = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
void main() {
print('Dio HTTPクライアント初期化完了');
}
基本的なリクエスト(GET/POST/PUT/DELETE)
import 'package:dio/dio.dart';
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
headers: {'Authorization': 'Bearer your-token'},
));
// 基本的なGETリクエスト
Future<void> basicGetRequest() async {
try {
final response = await dio.get('/users');
print('ステータスコード: ${response.statusCode}');
print('レスポンスデータ: ${response.data}');
print('ヘッダー: ${response.headers}');
} catch (e) {
print('エラー: $e');
}
}
// クエリパラメータ付きGETリクエスト
Future<void> getWithQueryParams() async {
final response = await dio.get(
'/users',
queryParameters: {
'page': 1,
'limit': 10,
'sort': 'created_at',
'filter': 'active'
},
);
print('URL: ${response.requestOptions.uri}');
print('データ: ${response.data}');
}
// POSTリクエスト(JSON送信)
Future<void> postJsonData() async {
final userData = {
'name': '田中太郎',
'email': '[email protected]',
'age': 30,
'department': '開発部'
};
try {
final response = await dio.post(
'/users',
data: userData,
options: Options(
headers: {'X-Request-ID': 'req-${DateTime.now().millisecondsSinceEpoch}'},
),
);
if (response.statusCode == 201) {
final createdUser = response.data;
print('ユーザー作成成功: ID=${createdUser['id']}');
print('作成日時: ${createdUser['created_at']}');
}
} on DioException catch (e) {
print('リクエストエラー: ${e.message}');
if (e.response != null) {
print('ステータス: ${e.response!.statusCode}');
print('エラー詳細: ${e.response!.data}');
}
}
}
// PUTリクエスト(データ更新)
Future<void> updateUserData() async {
final updatedData = {
'name': '田中次郎',
'email': '[email protected]',
'department': '企画部'
};
final response = await dio.put(
'/users/123',
data: updatedData,
);
print('更新結果: ${response.data}');
}
// DELETEリクエスト
Future<void> deleteUser() async {
try {
final response = await dio.delete('/users/123');
if (response.statusCode == 204) {
print('ユーザー削除完了');
} else if (response.statusCode == 200) {
print('削除完了: ${response.data}');
}
} catch (e) {
print('削除エラー: $e');
}
}
// 複数リクエストの並列実行
Future<void> parallelRequests() async {
final results = await Future.wait([
dio.get('/users'),
dio.get('/posts'),
dio.get('/comments'),
]);
print('ユーザー数: ${results[0].data.length}');
print('投稿数: ${results[1].data.length}');
print('コメント数: ${results[2].data.length}');
}
高度な設定とカスタマイズ(インターセプター、認証、タイムアウト等)
import 'package:dio/dio.dart';
// 包括的なDio設定とインターセプター
class ApiClient {
late Dio _dio;
ApiClient() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com/v1',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 10),
sendTimeout: Duration(seconds: 10),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'MyFlutterApp/1.0.0',
},
));
_setupInterceptors();
}
void _setupInterceptors() {
// リクエストインターセプター
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
print('🚀 REQUEST[${options.method}] => PATH: ${options.path}');
print('📝 Headers: ${options.headers}');
print('📊 Data: ${options.data}');
// 認証トークンの自動追加
final token = getAuthToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
// リクエストIDの追加
options.headers['X-Request-ID'] = generateRequestId();
handler.next(options);
},
onResponse: (response, handler) {
print('✅ RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}');
print('⏱️ Duration: ${DateTime.now().difference(response.requestOptions.extra['start_time'] ?? DateTime.now())}');
handler.next(response);
},
onError: (error, handler) {
print('❌ ERROR[${error.response?.statusCode}] => PATH: ${error.requestOptions.path}');
print('💥 Message: ${error.message}');
// 401エラー時の自動トークンリフレッシュ
if (error.response?.statusCode == 401) {
_handleUnauthorizedError(error, handler);
} else {
handler.next(error);
}
},
),
);
// ログインターセプター(デバッグ用)
_dio.interceptors.add(
LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: (log) => print('📋 $log'),
),
);
}
// 認証エラーハンドリング
Future<void> _handleUnauthorizedError(
DioException error,
ErrorInterceptorHandler handler,
) async {
try {
final refreshed = await refreshAuthToken();
if (refreshed) {
// トークンリフレッシュ成功時、元のリクエストを再実行
final options = error.requestOptions;
options.headers['Authorization'] = 'Bearer ${getAuthToken()}';
final response = await _dio.fetch(options);
handler.resolve(response);
} else {
handler.next(error);
}
} catch (e) {
handler.next(error);
}
}
// カスタムタイムアウト設定
Future<Response> customTimeoutRequest(String path, {
Duration? customTimeout,
Map<String, dynamic>? data,
}) async {
return await _dio.get(
path,
data: data,
options: Options(
sendTimeout: customTimeout ?? Duration(seconds: 30),
receiveTimeout: customTimeout ?? Duration(seconds: 30),
),
);
}
// HTTPSクライアント証明書設定
void setupClientCertificate() {
(_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.badCertificateCallback = (cert, host, port) {
// 本番環境では適切な証明書検証を実装
return true; // 開発環境のみ
};
return client;
};
}
String? getAuthToken() => 'your-auth-token';
String generateRequestId() => 'req-${DateTime.now().millisecondsSinceEpoch}';
Future<bool> refreshAuthToken() async => true; // 実装は省略
}
// プロキシ設定
void setupProxy() {
final dio = Dio();
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.findProxy = (uri) {
return 'PROXY localhost:8888';
};
return client;
};
}
エラーハンドリングとリトライ機能
import 'package:dio/dio.dart';
// 包括的なエラーハンドリング
class ErrorHandler {
static Future<Response?> safeRequest(
Future<Response> Function() request, {
int maxRetries = 3,
Duration retryDelay = const Duration(seconds: 1),
}) async {
int attempts = 0;
while (attempts < maxRetries) {
try {
return await request();
} on DioException catch (e) {
attempts++;
print('試行 $attempts/$maxRetries 失敗');
// リトライ可能なエラーかチェック
if (_shouldRetry(e) && attempts < maxRetries) {
print('${retryDelay.inSeconds}秒後に再試行...');
await Future.delayed(retryDelay);
continue;
}
// エラー詳細分析
_analyzeError(e);
// 最大試行回数に達した場合またはリトライ不可能な場合
if (attempts >= maxRetries) {
print('最大試行回数に達しました');
}
rethrow;
} catch (e) {
print('予期しないエラー: $e');
rethrow;
}
}
return null;
}
static bool _shouldRetry(DioException error) {
// リトライ可能な条件
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout ||
error.type == DioExceptionType.sendTimeout) {
return true;
}
// 5xxサーバーエラーはリトライ
if (error.response?.statusCode != null) {
final statusCode = error.response!.statusCode!;
return statusCode >= 500 && statusCode < 600;
}
return false;
}
static void _analyzeError(DioException error) {
print('=== エラー詳細分析 ===');
print('タイプ: ${error.type}');
print('メッセージ: ${error.message}');
if (error.response != null) {
final response = error.response!;
print('ステータスコード: ${response.statusCode}');
print('ステータスメッセージ: ${response.statusMessage}');
print('レスポンスヘッダー: ${response.headers}');
print('レスポンスデータ: ${response.data}');
} else {
print('レスポンスなし(ネットワークエラーまたはタイムアウト)');
}
// エラータイプ別の詳細メッセージ
switch (error.type) {
case DioExceptionType.connectionTimeout:
print('💡 接続タイムアウト: サーバーへの接続に時間がかかりすぎています');
break;
case DioExceptionType.receiveTimeout:
print('💡 受信タイムアウト: データの受信に時間がかかりすぎています');
break;
case DioExceptionType.sendTimeout:
print('💡 送信タイムアウト: データの送信に時間がかかりすぎています');
break;
case DioExceptionType.badResponse:
print('💡 不正なレスポンス: サーバーが予期しないレスポンスを返しました');
break;
case DioExceptionType.cancel:
print('💡 キャンセル: リクエストがキャンセルされました');
break;
case DioExceptionType.connectionError:
print('💡 接続エラー: ネットワーク接続を確認してください');
break;
case DioExceptionType.unknown:
print('💡 不明なエラー: ${error.error}');
break;
}
}
}
// 使用例
Future<void> errorHandlingExample() async {
final dio = Dio();
// 安全なリクエスト実行
final response = await ErrorHandler.safeRequest(
() => dio.get('https://api.example.com/unstable-endpoint'),
maxRetries: 5,
retryDelay: Duration(seconds: 2),
);
if (response != null) {
print('リクエスト成功: ${response.data}');
} else {
print('リクエスト失敗');
}
}
// CancelTokenを使用したリクエストキャンセル
Future<void> cancellationExample() async {
final dio = Dio();
final cancelToken = CancelToken();
// 5秒後にキャンセル
Timer(Duration(seconds: 5), () {
cancelToken.cancel('ユーザーによるキャンセル');
});
try {
final response = await dio.get(
'https://api.example.com/slow-endpoint',
cancelToken: cancelToken,
);
print('レスポンス: ${response.data}');
} on DioException catch (e) {
if (CancelToken.isCancel(e)) {
print('リクエストがキャンセルされました: ${e.message}');
} else {
print('その他のエラー: ${e.message}');
}
}
}
FormDataとファイルアップロード機能
import 'package:dio/dio.dart';
import 'dart:io';
// FormDataとファイルアップロードの包括的な例
class FileUploadService {
final Dio _dio = Dio();
// 基本的なFormData送信
Future<void> basicFormDataUpload() async {
final formData = FormData.fromMap({
'user_name': '田中太郎',
'email': '[email protected]',
'age': '30',
'department': '開発部',
});
try {
final response = await _dio.post(
'https://api.example.com/users',
data: formData,
);
print('フォーム送信成功: ${response.data}');
} catch (e) {
print('フォーム送信エラー: $e');
}
}
// ファイル付きFormDataアップロード
Future<void> fileUploadWithFormData() async {
try {
final formData = FormData.fromMap({
'title': 'プロフィール画像',
'description': 'ユーザーのプロフィール写真',
'category': 'profile',
'file': await MultipartFile.fromFile(
'/path/to/profile.jpg',
filename: 'profile.jpg',
contentType: DioMediaType.parse('image/jpeg'),
),
});
final response = await _dio.post(
'https://api.example.com/upload',
data: formData,
onSendProgress: (sent, total) {
final progress = (sent / total * 100).toStringAsFixed(1);
print('アップロード進捗: $progress% ($sent/$total bytes)');
},
);
print('アップロード成功: ${response.data}');
} catch (e) {
print('アップロードエラー: $e');
}
}
// 複数ファイルの同時アップロード
Future<void> multipleFileUpload() async {
try {
final formData = FormData.fromMap({
'album_name': '旅行写真',
'description': '2024年夏の旅行で撮影した写真',
'files': [
await MultipartFile.fromFile(
'/path/to/photo1.jpg',
filename: 'photo1.jpg',
contentType: DioMediaType.parse('image/jpeg'),
),
await MultipartFile.fromFile(
'/path/to/photo2.jpg',
filename: 'photo2.jpg',
contentType: DioMediaType.parse('image/jpeg'),
),
await MultipartFile.fromFile(
'/path/to/photo3.jpg',
filename: 'photo3.jpg',
contentType: DioMediaType.parse('image/jpeg'),
),
],
});
final response = await _dio.post(
'https://api.example.com/upload-multiple',
data: formData,
onSendProgress: (sent, total) {
final progress = (sent / total * 100).toStringAsFixed(1);
print('複数ファイルアップロード進捗: $progress%');
},
);
print('複数ファイルアップロード成功: ${response.data}');
} catch (e) {
print('複数ファイルアップロードエラー: $e');
}
}
// バイトデータからのファイルアップロード
Future<void> bytesUpload() async {
final imageBytes = await File('/path/to/image.png').readAsBytes();
final formData = FormData.fromMap({
'title': 'バイトデータ画像',
'file': MultipartFile.fromBytes(
imageBytes,
filename: 'bytes_image.png',
contentType: DioMediaType.parse('image/png'),
),
});
final response = await _dio.post(
'https://api.example.com/upload-bytes',
data: formData,
);
print('バイトアップロード成功: ${response.data}');
}
// ストリームを使用した大容量ファイルアップロード
Future<void> streamUpload() async {
final file = File('/path/to/large-video.mp4');
final fileSize = await file.length();
final formData = FormData.fromMap({
'title': '大容量動画ファイル',
'file': MultipartFile.fromStream(
() => file.openRead(),
fileSize,
filename: 'large-video.mp4',
contentType: DioMediaType.parse('video/mp4'),
),
});
try {
final response = await _dio.post(
'https://api.example.com/upload-stream',
data: formData,
options: Options(
sendTimeout: Duration(minutes: 10), // 大容量ファイル用
),
onSendProgress: (sent, total) {
final progress = (sent / total * 100).toStringAsFixed(2);
final sentMB = (sent / 1024 / 1024).toStringAsFixed(2);
final totalMB = (total / 1024 / 1024).toStringAsFixed(2);
print('大容量ファイルアップロード: $progress% (${sentMB}MB/${totalMB}MB)');
},
);
print('大容量ファイルアップロード成功: ${response.data}');
} catch (e) {
print('大容量ファイルアップロードエラー: $e');
}
}
// 再利用可能なFormData作成関数
Future<FormData> createReusableFormData() async {
return FormData.fromMap({
'timestamp': DateTime.now().toIso8601String(),
'app_version': '1.0.0',
'platform': Platform.isAndroid ? 'android' : 'ios',
});
}
// FormDataの再利用例(注意:cloneが必要)
Future<void> reuseFormDataExample() async {
final baseFormData = await createReusableFormData();
// 1回目のリクエスト
try {
final response1 = await _dio.post(
'https://api.example.com/endpoint1',
data: baseFormData.clone(), // cloneして使用
);
print('1回目成功: ${response1.data}');
} catch (e) {
print('1回目エラー: $e');
}
// 2回目のリクエスト
try {
final response2 = await _dio.post(
'https://api.example.com/endpoint2',
data: baseFormData.clone(), // 再度cloneして使用
);
print('2回目成功: ${response2.data}');
} catch (e) {
print('2回目エラー: $e');
}
}
}
ファイルダウンロードと進捗追跡
import 'package:dio/dio.dart';
import 'dart:io';
// ファイルダウンロードサービス
class FileDownloadService {
final Dio _dio = Dio();
// 基本的なファイルダウンロード
Future<void> basicDownload() async {
try {
final response = await _dio.download(
'https://api.example.com/files/document.pdf',
'/downloads/document.pdf',
onReceiveProgress: (received, total) {
if (total != -1) {
final progress = (received / total * 100).toStringAsFixed(1);
print('ダウンロード進捗: $progress% ($received/$total bytes)');
} else {
print('ダウンロード中: ${received} bytes');
}
},
);
print('ダウンロード完了: ${response.statusCode}');
} catch (e) {
print('ダウンロードエラー: $e');
}
}
// 大容量ファイルのストリーミングダウンロード
Future<void> streamingDownload() async {
try {
final response = await _dio.get<ResponseBody>(
'https://api.example.com/files/large-dataset.zip',
options: Options(responseType: ResponseType.stream),
);
final file = File('/downloads/large-dataset.zip');
final sink = file.openWrite();
int downloaded = 0;
final contentLength = int.tryParse(
response.headers.value('content-length') ?? '0'
) ?? 0;
await for (final chunk in response.data!.stream) {
sink.add(chunk);
downloaded += chunk.length;
if (contentLength > 0) {
final progress = (downloaded / contentLength * 100).toStringAsFixed(2);
final downloadedMB = (downloaded / 1024 / 1024).toStringAsFixed(2);
final totalMB = (contentLength / 1024 / 1024).toStringAsFixed(2);
print('ストリーミングダウンロード: $progress% (${downloadedMB}MB/${totalMB}MB)');
}
}
await sink.close();
print('ストリーミングダウンロード完了');
} catch (e) {
print('ストリーミングダウンロードエラー: $e');
}
}
// 断続的ダウンロード(レジューム機能)
Future<void> resumableDownload() async {
final filePath = '/downloads/resumable-file.zip';
final file = File(filePath);
int startByte = 0;
if (await file.exists()) {
startByte = await file.length();
print('既存ファイル検出: ${startByte} bytes からレジューム');
}
try {
final response = await _dio.get<ResponseBody>(
'https://api.example.com/files/large-file.zip',
options: Options(
responseType: ResponseType.stream,
headers: {
'Range': 'bytes=$startByte-',
},
),
);
final sink = file.openWrite(mode: FileMode.append);
int downloaded = startByte;
// Content-Range ヘッダーから全体サイズを取得
final contentRange = response.headers.value('content-range');
final totalSize = contentRange != null
? int.tryParse(contentRange.split('/').last) ?? 0
: 0;
await for (final chunk in response.data!.stream) {
sink.add(chunk);
downloaded += chunk.length;
if (totalSize > 0) {
final progress = (downloaded / totalSize * 100).toStringAsFixed(2);
print('レジュームダウンロード: $progress% ($downloaded/$totalSize bytes)');
}
}
await sink.close();
print('レジュームダウンロード完了');
} catch (e) {
print('レジュームダウンロードエラー: $e');
}
}
// 複数ファイルの並列ダウンロード
Future<void> parallelDownloads() async {
final downloadTasks = [
'https://api.example.com/files/file1.pdf',
'https://api.example.com/files/file2.jpg',
'https://api.example.com/files/file3.mp4',
];
final futures = downloadTasks.asMap().entries.map((entry) {
final index = entry.key;
final url = entry.value;
final filename = url.split('/').last;
return _dio.download(
url,
'/downloads/$filename',
onReceiveProgress: (received, total) {
if (total != -1) {
final progress = (received / total * 100).toStringAsFixed(1);
print('ファイル${index + 1} ダウンロード: $progress%');
}
},
);
});
try {
await Future.wait(futures);
print('全ファイルダウンロード完了');
} catch (e) {
print('並列ダウンロードエラー: $e');
}
}
}