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.yamldependencies セクションに dio を追記し、flutter pub get を実行します。

Yaml
dependencies: flutter: sdk: flutter dio: ^5.3.2

これだけで dio のインポートが可能になります。

ステップ 2: Dio のインスタンスを設定

共通ヘッダーやタイムアウト設定は、アプリ全体で使い回す Dio インスタンスにまとめます。lib/network/api_client.dart を作成し、以下のように記述します。

Dart
import '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.dartrunApp 前に ApiClient.addInterceptors(); を呼び出すことで、全リクエストにロギングが付与されます。

ステップ 3: データ取得用リポジトリの実装

lib/data/user_repository.dart に、GET リクエストでユーザー一覧を取得するメソッドを用意します。

Dart
import '../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 で描画します。

Dart
import '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.dataMap<String, dynamic> ではなく List<dynamic> になるケースがあるため、直接 as List<dynamic> とキャストしました。もし dynamic が予期しない構造の場合は、List<dynamic>.from(response.data) を使用すると安全です。

  • UI での setState 呼び出し忘れ
    FutureBuilder を使用しているため setState は不要ですが、別途データ更新処理を追加した際に setState(() { _futureUsers = _repo.fetchUsers(); }); を忘れたことで UI が更新されませんでした。データリフレッシュ時は必ず setState を呼び出すように注意してください。

解決策まとめ

  1. Dio のバージョン互換性を公式ドキュメントで確認し、エラータイプ名を最新版に合わせる。
  2. レスポンスデータの型を明示的にキャストし、ListMap の違いを意識する。
  3. データ取得後の UI 更新は FutureBuilder の再ビルドか setState を使い、状態管理を明確に保つ。

まとめ

本記事では、Flutter プロジェクトに dio を導入し、API からユーザー情報を取得して ListView に表示するフルサンプルを通じて、以下のポイントを解説しました。

  • dio のインストールと共通設定(タイムアウト・インターセプター)
  • GET リクエストの実装とエラーハンドリングのベストプラクティス
  • UI 側での非同期データ表示と実装時に陥りやすいエラーの対処法

これにより、読者は 信頼性の高い HTTP 通信基盤を自アプリに組み込み、エラーに強いコードを書けるようになります。次回は、認証トークンの自動付与やファイルアップロードなど、dio のさらに高度な機能を取り上げる予定です。

参考資料