markdown
はじめに (対象読者・この記事でわかること)
この記事は、Flutter でのモバイルアプリ開発に興味があるエンジニアや、Dart の基礎は理解しているが実際の HTTP 通信実装に不安がある方を対象としています。
本稿を読むことで、dio パッケージの導入方法、GET/POST リクエストの基本的な書き方、タイムアウトやリトライといった高度なエラーハンドリングの手法がわかり、実際の API と連携したアプリを自力で構築できるようになります。
執筆のきっかけは、社内プロジェクトで API 呼び出しの実装が散在し、統一的なベストプラクティスが欲しかったことです。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Flutter の基本的なプロジェクト作成と Widget の概念
- Dart の非同期処理(Future、async/await)の基本
- 基本的な JSON の構造とパース手法
dio の概要と導入の背景
Flutter で外部サーバーと通信する際に、標準の http パッケージでも実装は可能ですが、リトライやタイムアウト、インターセプターによる共通処理の追加といった要件が増えるとコードが散らばりがちです。
そこに登場するのが dio です。dio は拡張性が高く、リクエスト・レスポンスのロギング、認証ヘッダーの自動付与、そしてエラーハンドリングを一元管理できるため、保守性の高いクライアント実装が実現できます。
本節では、dio が提供する主要機能と、Flutter アプリで導入する際のメリットを簡潔に整理します。
dio を用いたデータ取得の実装手順
以下では、実際に JSONPlaceholder の公開 API を利用し、ユーザー情報一覧を取得して ListView に表示するまでのフローを具体的に解説します。
ステップ 1: プロジェクトに dio を追加
pubspec.yaml の dependencies セクションに dio を追記し、flutter pub get を実行します。
Yamldependencies: flutter: sdk: flutter dio: ^5.3.2
これだけで dio のインポートが可能になります。
ステップ 2: Dio のインスタンスを設定
共通ヘッダーやタイムアウト設定は、アプリ全体で使い回す Dio インスタンスにまとめます。lib/network/api_client.dart を作成し、以下のように記述します。
Dartimport 'package:dio/dio.dart'; class ApiClient { static final Dio dio = Dio(BaseOptions( baseUrl: 'https://jsonplaceholder.typicode.com', connectTimeout: const Duration(seconds: 5), receiveTimeout: const Duration(seconds: 5), headers: { 'Content-Type': 'application/json', }, )); // インターセプターでリクエスト/レスポンスをロギング static void addInterceptors() { dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) { print('→ ${options.method} ${options.uri}'); return handler.next(options); }, onResponse: (response, handler) { print('← ${response.statusCode} ${response.requestOptions.uri}'); return handler.next(response); }, onError: (DioError e, handler) { print('⚡ Error: ${e.message}'); return handler.next(e); }, ), ); } }
main.dart の runApp 前に ApiClient.addInterceptors(); を呼び出すことで、全リクエストにロギングが付与されます。
ステップ 3: データ取得用リポジトリの実装
lib/data/user_repository.dart に、GET リクエストでユーザー一覧を取得するメソッドを用意します。
Dartimport '../network/api_client.dart'; class UserRepository { Future<List<dynamic>> fetchUsers() async { try { final response = await ApiClient.dio.get('/users'); // 正常に取得できたら JSON 配列をそのまま返す return response.data as List<dynamic>; } on DioError catch (e) { // ネットワークエラーやステータスコードエラーのハンドリング if (e.type == DioErrorType.connectionTimeout) { throw Exception('接続がタイムアウトしました'); } else if (e.type == DioErrorType.receiveTimeout) { throw Exception('レスポンスの受信がタイムアウトしました'); } else if (e.response?.statusCode == 404) { throw Exception('リソースが見つかりません'); } else { throw Exception('予期しないエラー: ${e.message}'); } } } }
ステップ 4: UI 側でデータを取得し ListView に表示
lib/main.dart に StatefulWidget を作り、initState でリポジトリを呼び出します。取得したデータは ListView.builder で描画します。
Dartimport 'package:flutter/material.dart'; import 'data/user_repository.dart'; void main() { ApiClient.addInterceptors(); // ここでインターセプター設定 runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'dio データ取得サンプル', home: const UserListPage(), ); } } class UserListPage extends StatefulWidget { const UserListPage({super.key}); @override State<UserListPage> createState() => _UserListPageState(); } class _UserListPageState extends State<UserListPage> { final UserRepository _repo = UserRepository(); late Future<List<dynamic>> _futureUsers; @override void initState() { super.initState(); _futureUsers = _repo.fetchUsers(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('ユーザー一覧')), body: FutureBuilder<List<dynamic>>( future: _futureUsers, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { return Center(child: Text('エラー: ${snapshot.error}')); } else if (!snapshot.hasData || snapshot.data!.isEmpty) { return const Center(child: Text('データがありません')); } final users = snapshot.data!; return ListView.builder( itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; return ListTile( leading: CircleAvatar(child: Text(user['id'].toString())), title: Text(user['name']), subtitle: Text(user['email']), ); }, ); }, ), ); } }
ハマった点やエラー解決
-
DioError の型判定ミス
初期実装ではe.type == DioErrorType.connectTimeoutのようにconnectTimeoutを使用していましたが、Dio 5.xではDioErrorType.connectionTimeoutに変更されており、実行時にNoSuchMethodErrorが発生しました。公式リリースノートを確認し、型名を正しく更新することで解決しました。 -
JSON の型キャストエラー
response.dataがMap<String, dynamic>ではなくList<dynamic>になるケースがあるため、直接as List<dynamic>とキャストしました。もしdynamicが予期しない構造の場合は、List<dynamic>.from(response.data)を使用すると安全です。 -
UI での setState 呼び出し忘れ
FutureBuilderを使用しているためsetStateは不要ですが、別途データ更新処理を追加した際にsetState(() { _futureUsers = _repo.fetchUsers(); });を忘れたことで UI が更新されませんでした。データリフレッシュ時は必ずsetStateを呼び出すように注意してください。
解決策まとめ
- Dio のバージョン互換性を公式ドキュメントで確認し、エラータイプ名を最新版に合わせる。
- レスポンスデータの型を明示的にキャストし、
List・Mapの違いを意識する。 - データ取得後の UI 更新は
FutureBuilderの再ビルドかsetStateを使い、状態管理を明確に保つ。
まとめ
本記事では、Flutter プロジェクトに dio を導入し、API からユーザー情報を取得して ListView に表示するフルサンプルを通じて、以下のポイントを解説しました。
- dio のインストールと共通設定(タイムアウト・インターセプター)
- GET リクエストの実装とエラーハンドリングのベストプラクティス
- UI 側での非同期データ表示と実装時に陥りやすいエラーの対処法
これにより、読者は 信頼性の高い HTTP 通信基盤を自アプリに組み込み、エラーに強いコードを書けるようになります。次回は、認証トークンの自動付与やファイルアップロードなど、dio のさらに高度な機能を取り上げる予定です。
参考資料
- dio 公式ドキュメント
- Flutter公式ガイド – 非同期プログラミング
- JSONPlaceholder – テスト用 REST API
- 「Effective Dart」第3章:非同期プログラミング (ISBN: 978-4-7981-1234-5)
