Zenn
📶

画像のダウンロードができないときにエラー処理をしたい

2024/12/11に公開

🤔やってみたいこと

インターネットから画像をダウンロードするアプリを作る。メモリに保存するだけなので後で消えますけどね。Pythonの本で例外処理のページを呼んでいてインターネットにつながっていないときに例外処理が出てきてるのを見て、ネットにつながってない処理は書いたことないのに気づいた😅

Dart3.0のsealed class使えないかと試してみた。KotlinだとよくAPI通信の例外処理でsealed classを使うのを見かける。

たまたまFeedに流れてきた記事が気になり作ってみた。
https://zenn.dev/tko1975/articles/a64c66b1056e91

🚀やってみたこと

dioを使って画像をインターネットからダウンロードするだけの機能を実装してみた。

完成品
https://youtube.com/shorts/XtbiI8dJPs4

機能

  • pixabayから画像をダウンロード
  • ダウンロードした画像をアプリ内で表示
  • エラーハンドリング(接続エラー、タイムアウトなど)

技術スタック

  • Flutter/Dart
  • dio - HTTPクライアント
  • Material Design 3

アーキテクチャ

アプリケーションは以下の3つの主要なファイルで構成されています:

  • main.dart - アプリケーションのUIとメインロジック
  • image_download_service.dart - 画像ダウンロード処理を担当
  • api_response.dart - APIレスポンスの型定義(Success/Failure)

エラーハンドリング

以下のエラーケースに対応しています:

  • インターネット接続なし
  • 接続タイムアウト
  • サーバーエラー
  • その他の予期せぬエラー

使い方

  1. アプリを起動
  2. 画面右下のFloating Action Buttonをタップ
  3. ランダムな画像がダウンロードされ、画面中央に表示されます

注意点

  • ダウンロードした画像はメモリ上にのみ保存され、アプリ終了時に消去されます
  • 永続的な保存機能は実装されていません

sealed classでAPIレスポンスの型定義(Success/Failure)をする。

api_response.dart
sealed class ApiResponse<T> {
  const ApiResponse();
}

final class Success<T> extends ApiResponse<T> {
  const Success(this.data);
  final T data;
}

final class Failure extends ApiResponse<Never> {
  const Failure(this.message);
  final String message;
}

画像をダウンロード処理のビジネスロジックを定義する。

image_download_service.dart
import 'package:dio/dio.dart';
import 'api_response.dart';

class ImageDownloadService {
  final Dio _dio = Dio();

  Future<ApiResponse<List<int>>> downloadImage(String url) async {
    try {
      final response = await _dio.get<List<int>>(
        url,
        options: Options(responseType: ResponseType.bytes),
      );
      
      return Success(response.data!);
    } on DioException catch (e) {
      switch (e.type) {
        case DioExceptionType.connectionTimeout:
        case DioExceptionType.sendTimeout:
        case DioExceptionType.receiveTimeout:
          return const Failure('接続がタイムアウトしました');
        case DioExceptionType.connectionError:
          return const Failure('インターネット接続がありません');
        case DioExceptionType.badResponse:
          return Failure('サーバーエラー: ${e.response?.statusCode}');
        default:
          return Failure('ダウンロードに失敗しました: ${e.message}');
      }
    } catch (e) {
      return Failure('予期せぬエラーが発生しました: $e');
    }
  }
}

ユーザーが画像をダウンロードする操作をする機能を実装したコード。

main.dart
import 'package:flutter/material.dart';
import 'image_download_service.dart';
import 'api_response.dart';
import 'dart:typed_data';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: '画像ダウンローダー',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: '画像ダウンローダー'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final ImageDownloadService _service = ImageDownloadService();
  Uint8List? _imageBytes;
  String? _errorMessage;

  Future<void> _downloadImage() async {
    setState(() {
      _imageBytes = null;
      _errorMessage = null;
    });

    final response = await _service.downloadImage(
      'https://cdn.pixabay.com/photo/2024/09/19/07/30/wild-horse-9057944_1280.jpg',
    );

    switch (response) {
      case Success(data: final bytes):
        setState(() {
          _imageBytes = Uint8List.fromList(bytes);
        });
      case Failure(message: final message):
        setState(() {
          _errorMessage = message;
        });
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            if (_imageBytes != null)
              Image.memory(_imageBytes!)
            else if (_errorMessage != null)
              Text(
                _errorMessage!,
                style: const TextStyle(
                  color: Colors.red,
                  fontSize: 16,
                  fontWeight: FontWeight.w500,
                ),
                textAlign: TextAlign.center,
              )
            else
              const Text('画像をダウンロードするにはボタンを押してください')
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _downloadImage,
        tooltip: 'Download Image',
        child: const Icon(Icons.download),
      ),
    );
  }
}

🙂最後に

API通信をするときに画像のダウンロードをする処理は実装したことなかったのでハードコーディングですが実装してみました。シンプルなデモアプリですがエラー処理の学習に最適かなと思い試しに作ってみました。

Discussion

ログインするとコメントできます