Dio

Dart/Flutter向けの強力なHTTPクライアントライブラリ。インターセプター、グローバル設定、FormData、リクエストキャンセル、ファイルダウンロード、タイムアウト機能を提供。豊富な機能と優れたドキュメントにより、Flutter開発で広く採用される。

HTTPクライアントDartFlutterインターセプターFormDataファイルアップロード

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.

ホームページ:https://dio.pub
スター12,731
ウォッチ163
フォーク1,547
作成日:2018年4月20日
言語:Dart
ライセンス:MIT License

トピックス

adaptercancellabledartdioflutterhttpinterceptormiddlewarenetworktimeouttransformer

スター履歴

cfug/dio Star History
データ取得日時: 2025/10/22 09:55

ライブラリ

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');
    }
  }
}