[読書会]インターネットからのデータ取得

(本頁はdocs.flutter.dev(https://docs.flutter.dev/cookbook/networking/fetch-data)のリファレンスブログです)

目次 index【閲覧時間3分】 click !!>>

1.概要

 アプリ開発において、インターネットからデータを取得することは一般的なタスクです。
Dart と Flutter は、このようなネットワーク通信を行うためのツールを提供しており、特に便利なツールとして http パッケージがあります。

 このチュートリアルの目的は、ネットワーク通信を行い、アプリ内でデータを表示するまでの一連の流れを習得することです。Flutter で効率的かつクロスプラットフォームで動作するネットワーク処理を実現するための基本スキルを学ぶ内容になっています。

(1)注意点

  • dart:io や dart:html を直接使用しないこと!
    • これらのライブラリを使って HTTP リクエストを実行することは避けるべきです。
    • 理由は、これらのライブラリがプラットフォーム依存であり、単一の実装に縛られるためです
      (例えば、dart:html は Web 専用、dart:io はモバイル/デスクトップ専用)。

(2)処理概要

① http パッケージを追加する

  • ネットワーク通信をシンプルに実現するために、http パッケージをプロジェクトに追加します。
  • パッケージは、pubspec.yaml ファイルで依存関係として宣言します。

② http パッケージを使ってネットワークリクエストを実行する

  • http.get() などのメソッドを使用して、API からデータを取得します。
  • 例: REST API にリクエストを送り、レスポンスを取得する。

⓷ レスポンスをカスタム Dart オブジェクトに変換する

  • JSON 形式のデータを Dart のオブジェクト(例えば、クラス)に変換します。
  • これにより、アプリケーション内で扱いやすいデータ構造に変換できます。

④ Flutter を使ってデータを取得・表示する

  • 取得したデータを Flutter のウィジェットで表示します。
  • 非同期操作を扱うために、FutureBuilder などのウィジェットを使用することが一般的です。

2.http パッケージの追加

 http パッケージは、インターネット通信を簡単に実装できる基本ツールです。
プラットフォーム毎に必要な権限設定を行うことで、ネットワークリクエストがエラーなく動作します。

(1)http パッケージを依存関係として追加

(1)http パッケージを依存関係として追加します:
flutter pub add http

(2)http パッケージをインポート

(2)http パッケージをインポートします:
import 'package:http/http.dart' as http;

(3)Android デプロイの場合: AndroidManifest.xml に権限を追加

 Android でインターネットを使用するには、インターネット権限をアプリに付与する必要があります。

(android/app/src/main/AndroidManifest.xml ファイル)
<!-- インターネットからデータを取得するために必要 -->
<uses-permission android:name="android.permission.INTERNET" />

(4)macOS デプロイの場合: Entitlements ファイルを編集

 macOS でネットワークリクエストを許可するには、以下の設定を追加します。

(macos/Runner/DebugProfile.entitlements ファイル と  macos/Runner/Release.entitlements ファイル)
<!-- インターネットからデータを取得するために必要 -->
<key>com.apple.security.network.client</key>
<true/>

3.ネットワークリクエストを行う

 このステップでは、http.get() メソッドを使用してサンプルデータ
JSONPlaceholder から提供されるアルバムデータ

サンプルデータ( JSONPlaceholder )から提供されるアルバムデータ)(例):
{
  "userId": 1,
  "id": 1,
  "title": "quidem molestiae enim"
}

を取得する方法を学びます。

http.get() メソッドを使用してサンプルデータ ( JSONPlaceholder から提供されるアルバムデータ)を取得する(例):
Future<http.Response> fetchAlbum() {

  //http.get()メソッド:
  //( ネットワークリクエストを行います。
  //  ・指定した URL に GET リクエストを送り、結果を取得します。
  //  ・URL は Uri.parse() を使い URI オブジェクトに変換します。
  return http.get(
   Uri.parse(
    'https://jsonplaceholder.typicode.com/albums/1'));
}

//<http.Response>:
// 非同期処理が完了すると、http.Response オブジェクトが返されます。
// このオブジェクトには、リクエストの結果として取得したデータが含まれています
// (通常は、HTTP ステータスコード、ヘッダー、レスポンスボディなど)。

4.レスポンスをカスタムDart オブジェクトに変換する

 ネットワークリクエスト自体は簡単に実行できますが、Future を直接扱うのは不便です。
そこで、http.Response を Dart のカスタムオブジェクトに変換することで、コードをより扱い易くします。

(1)アルバムクラスの作成

 最初に、ネットワークリクエストで取得するデータ(JSON)を表現する為の Album クラスを作成します。

 ・パターンマッチング規則(pattern matching

(1)アルバムクラスの作成(例)
//Album クラス:
//( ネットワークリクエストで取得するアルバムデータ(userId、id、title)
//  をプロパティとして持つクラス。
//  final 修飾子を使うことで、クラスのインスタンスを不変(イミュータブル)
//  にしています。)

class Album {
  final int userId;
  final int id;
  final String title;

  const Album({
    required this.userId,
    required this.id,
    required this.title,
  });

  //Album.fromJson ファクトリコンストラクタ:
  //( JSON データ(Map<String, dynamic> 型)受取後、Album インスタンス作成。
  //  switch 文を使用して、JSON データが期待どおりの構造であるかを確認します。
  //  構造が異なる場合は、FormatException をスローします。)
  factory Album.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {
        'userId': int userId,
        'id': int id,
        'title': String title,
      } =>
        Album(
          userId: userId,
          id: id,
          title: title,
        ),
      _ => throw const FormatException('Failed to load album.'),
    };
  }
}

(2)http.Response をアルバムに変換する(FetchAlbum 関数の更新)

 次に、fetchAlbum 関数を更新し、Future を返すようにします。

(2)http.Response をアルバムに変換する(FetchAlbum 関数の更新)(例)
Future<Album> fetchAlbum() async {
  final response = await http
    .get(
      Uri.parse(
        'https://jsonplaceholder.typicode.com/albums/1'));

  if (response.statusCode == 200) {
    //サーバーが 200 OK を返した場合、JSON を解析
    return Album.fromJson(
      //jsonDecode:
      //( response.body(JSON形式の文字列)を jsonDecode を使用して
      //  Dart の Map<String, dynamic> に変換します。)
      jsonDecode(
        response.body  //JSON形式の文字列
      ) as Map<String, dynamic>
    );
  } else {
    //サーバーが 200 OK を返さなかった場合、例外をスロー
    throw Exception('Failed to load album');
  }
}

(3)動作例

⓵ サーバーからのレスポンス(例: JSON Placeholder API)

⓵ サーバーからのレスポンス(例: JSON Placeholder API)(例):
{
  "userId": 1,
  "id": 1,
  "title": "quidem molestiae enim"
}

⓶ このレスポンスは Album.fromJson によって以下のような Album オブジェクトに変換されます。

⓶ このレスポンスは Album.fromJson によって以下のような Album オブジェクトに変換されます: dart コードをコピーする(例):
Album(
  userId: 1,
  id: 1,
  title: "quidem molestiae enim"
)

 これでインターネットからデータを取得し、カスタムオブジェクトに変換する準備が整いました。
次は、このデータを Flutter ウィジェットに表示する方法を学びます。

5.データの取得

 このステップでは、fetchAlbum() メソッドを使用してネットワークデータを取得します。
この操作は、StatefulWidget のライフサイクルメソッドである initState() または didChangeDependencies() で実行するのが一般的です。

(1)ライフサイクルメソッドの選択

① initState()

特徴
 initState() は、ウィジェットの状態が初期化されるときに一度だけ呼び出されます。

利用ケース
 ウィジェットが一度だけデータをロードすればよい場合に適しています。

注意点
 再度 API をリロードする必要がある場合、このメソッドでは適応できません。

initState() 使用例:
@override
void initState() {
  super.initState();
  futureAlbum = fetchAlbum();
}

⓶ didChangeDependencies()

特徴
 InheritedWidget が変更された場合などに再度呼び出されます。

例用ケース
 API をリロードする必要がある場合や、親ウィジェットからの依存関係が変化する可能性がある場合に適しています。

注意点
 過剰に呼び出される可能性があるため、適切に設計する必要があります。

didChangeDependencies() 使用例:
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  futureAlbum = fetchAlbum();
}

(2)コードの詳細

initState() 使用例:
class _MyAppState extends State<MyApp> {
  late Future<Album> futureAlbum;

  @override
  void initState() {
    super.initState();
    //fetchAlbum() を初期化時に呼び出す
    //( データ取得の非同期処理結果(Future<Album>)を保持するための変数
    //  を late キーワードで定義します。)
    futureAlbum = fetchAlbum();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Fetch Data Example')),
      //FutureBuilder:
      //( futureAlbum を渡して、非同期処理の状態(読み込み中、成功、失敗)
      //  に応じて UI を動的に切り替えます。)
      body: FutureBuilder<Album>(
        future: futureAlbum,  //非同期処理結果を渡す。
        builder: (context, snapshot) {
          //snapshot.connectionState(現在の接続状態)で状態を判断します。
          
          //データが未だロード中:
          if (snapshot.connectionState == ConnectionState.waiting) {
            //読み込み中のインジケータ
            return Center(child: CircularProgressIndicator());
          //エラーが発生した場合
          } else if (snapshot.hasError) {
            //エラー表示
            return Center(child: Text('Error: ${snapshot.error}'));
          //データが正常取得された場合:
          } else if (snapshot.hasData) {
            final album = snapshot.data!;
            //データを表示
            return Center(child: Text('Title: ${album.title}'));
          //予期しないケース:
          } else {
            //データがない場合
            return Center(child: Text('No data available'));
          }
        },
      ),
    );
  }
}

(3)注意点

⓵ 適切なメソッドの選択

  • データを一度だけロードする場合は initState() を使用します。
  • 状態変更に応じてデータを再取得する必要がある場合は didChangeDependencies() を選びます。

⓶ エラーハンドリング

  • ネットワークリクエストには失敗の可能性があるため、エラー状態を適切に処理する必要があります
    (例: ステータスコードの確認、例外処理など)。

⓷ パフォーマンスへの配慮

  • 不必要に頻繁に API を呼び出さないよう、ライフサイクルメソッドの設計を慎重に行います。

6.データの表示

 データを画面に表示するには、FutureBuilder ウィジェット を使用します。
FutureBuilder は非同期データソースを扱う際に便利なウィジェットで、非同期処理の状態(読み込み中、成功、エラー)に応じた UI を構築できます

(1)FutureBuilder の基本的な仕組み

 FutureBuilder には次の2つの必須パラメータがあります

⓵ future

  •  非同期処理(Future)を渡します。
    (この例では、fetchAlbum() 関数から返される Future (FutureAlbum)を指定します。)

⓶ builder

  • 非同期処理の現在の状態(snapshot)に基づいて、どの UI を表示するかを定義する関数です。
  • 状態には主に以下があります。
    • データがロード中(loading):
      • デフォルトの読み込み中インジケータ(例: CircularProgressIndicator)を表示。
    • データの取得に成功(success):
      • 取得したデータを UI に表示。
    • エラーが発生(error):
      • エラーメッセージを表示。

7.initState() で fetchAlbum() が呼び出される理由

 fetchAlbum() を initState() 内で呼び出す理由は、Flutter の build() メソッドの特性とパフォーマンスに関係しています。

(1)build() メソッドに API 呼び出しを置くべきではない理由

  1. build() メソッドの頻繁な呼び出し
    • Flutter は、ビューに何か変更が必要な場合に build() メソッドを呼び出します。
    • (例えば、画面サイズの変更や、setState() がトリガーされたときに再び build() が呼ばれます。
       このように頻繁に再ビルドが発生するため、build() 内に API 呼び出しがあると、同じリクエストが何度も実行されてしまいます。)
  2. アプリのパフォーマンス低下
    • 再ビルドごとに API を呼び出すと、過剰なネットワークリクエストが発生し、アプリが遅くなるだけでなく、サーバーへの負荷も増加します。
      (必要のないネットワークリクエストを防ぐために、build() 内で非同期処理を行うことは避けるべきです。)

(2)initState() での呼び出しの利点

  1. 一度だけ実行
    • initState() は State オブジェクトのライフサイクル中に 1 回だけ 実行されるメソッドです。
      この特性を活かして、非同期処理を初期化時にのみ実行し、その結果をキャッシュすることができます。
  2. キャッシュ機能
    • API 呼び出しの結果を Future 型の状態変数(例: futureAlbum)に保存することで、build() が再び呼ばれたときにも、結果を再利用できます。
      この仕組みにより、ネットワークリクエストが不要な再実行を防ぎます。

8.テスト

(1)Introduction to unit testing

 ユニットテストの基本的な概念と、Flutter での実装方法を説明したガイドです。

  • テスト対象となる関数やクラスが、単体で正しく動作するかを検証する方法。
  • 外部依存関係を取り除いた状態でのテストを重視。

(2)Mock dependencies using Mockito

 Mockito パッケージを使用して依存関係をモック(仮想的に再現)する方法を解説。

  • ネットワーク要求のテストでは、実際の API を呼び出さずにモックデータを返す様に依存関係を置換する方法を説明。
  • サーバーのレスポンスやエラーをシミュレートして、関数が適切に動作するかを検証可能。

 (例)http.get() の呼び出しをモック化して、予測可能なデータやエラーを用いたテストが可能になります。

9.サンプルコード

・起動後の画面内容:

・上記で取得して表示している JSONPlaceholder API (https://jsonplaceholder.typicode.com/albums/1) の内容:

JSONPlaceholder API (https://jsonplaceholder.typicode.com/albums/1) の内容:
{
  "userId": 1,
  "id": 1,
  "title": "quidem molestiae enim"
}

・サンプルコード:

Dart
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

//非同期関数 fetchAlbum を定義
//この関数は HTTP リクエストを実行し、Album オブジェクトを返す
Future<Album> fetchAlbum() async {
  //JSONPlaceholder API に GET リクエストを送信
  final response = await http
    .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));

  //HTTP ステータスコードが 200 (成功) の場合
  if (response.statusCode == 200) {
    //レスポンスの JSON データを Dart の Map にデコードし、
    //Album オブジェクトに変換して返す
    return Album.fromJson(
      jsonDecode(response.body) as Map<String, dynamic>
    );
  } else {
    //ステータスコードが 200 以外の場合は例外をスロー
    throw Exception('Failed to load album');
  }
}

//Album クラスの定義
//このクラスは取得したアルバムデータを表現
class Album {
  final int userId; //アルバムを作成したユーザーの ID
  final int id;     //アルバムの ID
  final String title; //アルバムのタイトル

  //コンストラクタ(不変なプロパティを設定)
  const Album({
    required this.userId,
    required this.id,
    required this.title,
  });

  //ファクトリコンストラクタ: JSON から Album オブジェクトを作成
  factory Album.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {
        'userId': int userId,
        'id': int id,
        'title': String title,
      } =>
        Album(
          userId: userId, //JSON の userId を設定
          id: id,         //JSON の id を設定
          title: title,   //JSON の title を設定
        ),
      //期待する形式でない場合は例外をスロー
      _ => throw const FormatException('Failed to load album.'),
    };
  }
}

//アプリのエントリポイント
void main() => runApp(const MyApp());

//StatefulWidget を作成(非同期処理を管理するため)
class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

//StatefulWidget の状態クラス
class _MyAppState extends State<MyApp> {
  //Future 型の変数を定義(非同期処理の結果を保持)
  late Future<Album> futureAlbum;

  @override
  void initState() {
    super.initState();
    //アプリ初期化時に fetchAlbum() を呼び出し、結果を futureAlbum に保存
    futureAlbum = fetchAlbum();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fetch Data Example', //アプリのタイトル
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple
        ),
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Fetch Data Example'), //AppBar のタイトル
        ),
        body: Center(
          //FutureBuilder ウィジェットを使用して非同期処理の結果を表示
          child: FutureBuilder<Album>(
            future: futureAlbum, //fetchAlbum() の結果を渡す
            builder: (context, snapshot) {
            
              //データが取得できた場合
              if (snapshot.hasData) {
                //Album のタイトルを表示
                return Text(snapshot.data!.title);
              }
              
              //エラーが発生した場合
              else if (snapshot.hasError) {
                //エラーメッセージを表示
                return Text('${snapshot.error}');
              }

              //データ取得中はローディングスピナーを表示
              return const CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}

コメントを残す