2020/10/28 Fetch data from the internetの訳

 

null safety article

Contents

Fetch data from the internet

ほとんどのアプリでは、インターネットからデータを取得する必要があります。幸いなことに、DartとFlutterは、このタイプの作業用のパッケージとしてhttpパッケージなどのツールを提供しています 。

このレシピでは、次の手順を使用します。

  1. httpパッケージを追加します。
  2. httpパッケージを使用してネットワークリクエストを行います。
  3. レスポンス(応答)をカスタムDartオブジェクトに変換します。
  4. Flutterでデータを取得して表示します。

1. Add the http package

このhttpパッケージは、インターネットからデータをフェッチする最も簡単な方法を提供します。

httpパッケージをインストールするには、pubspec.yamlファイルの依存関係セクションにパッケージを追加します。httpパッケージの最新バージョンは pub.devにあります。

dependencies:
  http: <latest_version>

 


httpパッケージをインポートします。

//main.dartなど
import 'package:http/http.dart' as http;

 


さらに、AndroidManifest.xmlファイルにインターネット権限を追加します。

<!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET" />

 




2. Make a network request

このレシピでは、http.get()メソッドを使用してJSONPlaceholderからサンプルアルバムをフェッチする方法について説明します。

Future<http.Response> fetchAlbum() {
  return http.get(Uri.https('jsonplaceholder.typicode.com', 'albums/1'));
}

http.get()メソッドはFuture<http.Response>型の返り値を返します。

  • Futureは、非同期操作を操作するためのコアDartクラスです。Futureオブジェクトは、将来のある時点で利用可能になる可能性のある値またはエラーを表します。
  • このhttp.Responseクラスには、成功したhttp呼び出し(call)から受信したデータが含まれています。



3. Convert the response into a custom Dart object

ネットワークリクエストを行うのは簡単ですが、受け取ったFuture<http.Response>をそのまま使用するのはあまり便利ではありません。手間を少なくするためhttp.Response型をDartオブジェクト(モデルクラス)に変換します。


Create an Album class

まず、ネットワークリクエストからのデータを受け取るためのAlbumクラスを作成します。これには、JSONからAlbumインスタンスを作成するファクトリ(factory)コンストラクターが含まれています。

JSONを手動で変換することは1つのオプションにすぎません。

詳細については、JSON & serializationに関する記事全文を参照してください 。


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

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

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
    );
  }
}

 


Convert the http.Response to an Album

ここで、次の手順を使用してfetchAlbum() 関数を修正して、Future<Album>を返すようにします。

dart:convertパッケージを使って、レスポンスのbody(ボディ)をJSON Mapに変換します。

サーバーがステータスコード200のOKレスポンスを返してきた場合(通信が成功してデータ(JSON Map)を受け取ることができた場合)、JSON Mapから、fromJson()(factoryコンストラクタ)を使ってAlbum型インスタンスを生成します。

サーバーがステータスコード200のOK応答を返さない場合は、例外をスローします。「404NotFound」サーバー応答の場合でも、例外をスローします。

nullを返さないでください。これは、この後説明するsnapshotでデータを調べる時に重要なことです。

 

import 'dart:convert';

Future<Album> fetchAlbum() async {
  //↓リクエストを送ってレスポンスを受け取る非同期メソッドhttp.get()
  //awaitキーワードを使って非同期処理が完了するまで(あるいはエラーが来るまで)待つ。
  final response = await http.get('https://jsonplaceholder.typicode.com/albums/1');

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    //↓サーバーが200 OK レスポンスを返してきた場合は、JSONをパースする。
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    //↓200 OK レスポンスが返ってこなかった場合は例外をスローする。
    throw Exception('Failed to load album');
  }
}

やりましたね。インターネットからアルバムをfetch(フェッチ)する関数を定義することができました。

fetch:取ってくる、呼んでくる、連れてくる、引き出す


4. Fetch the data

fetchAlbum()メソッドをinitState()メソッド、あるいはdidChangeDependencies()メソッド内のどちらかで呼び出します(実行します)。

このinitState()メソッドは一度だけ呼び出され、その後は二度と呼び出されません。

InheritedWidgetが保持する状態変数の変更に応じてAPIをリロードするオプションが必要な場合は didChangeDependencies()メソッド内でfetchAlbum()メソッドを呼び出します。

さらに詳しい情報はStateクラスをご覧ください。

class _MyAppState extends State<MyApp> {
  Future<Album> futureAlbum;

  @override
  void initState() {
    super.initState();
    futureAlbum = fetchAlbum();
  }

 


5. Display the data

画面にデータを表示するには、FutureBuilderウィジェットを使用します。

FutureBuilderウィジェットはFlutterフレームワークに用意されているウィジェットで、非同期のデータソースを簡単に扱うことができます。

FutureBuilderコンストラクタには二つの引数を渡す必要があります。

  1. 扱いたいFutureインスタンス。今回はfetchAlbum()メソッドの返り値のfuture。
  2. Flutterに対して何をレンダーすべきかを示すbuilder関数。builder関数内でFutureから受け取る値を使います。

snapshotがnullでない値を持っている場合のみ、snapshot.hasDataはtrueを返します。

(= 404の時にfetchAlbumメソッドがnullを返すようにしてしまうと、snapshot.hasDataはfalseを返す。その時に一つ下のサンプルのような条件分岐にすると、404の時いつまでもスピナーが回り続けることになる。だから一つ前のサンプルコードで、404の時もfetchAlbumメソッドが例外をスローするようにしている。)

 

Because fetchAlbum can only return non-null values, the function should throw an exception even in the case of a “404 Not Found” server response.

fetchAlbumメソッドはnullでない値のみを返します。ですから、fetchAlbumメソッドは、サーバーレスポンスが“404 Not Found”の時も例外をスローする必要があります。(そのように実装した。)

 

Throwing an exception sets the snapshot.hasError to true which can be used to display an error message.

例外がスローされた時は、snapshot.hasErrorにtrueがセットされます。その結果(画面に)エラーメッセージが表示されます。

 

Otherwise, the spinner will be displayed.

それ以外の場合はスピナー(CircularProgressIndicator)が表示されます。

サーバーレスポンスが”404 Not Found”の場合も例外をスローすべき理由がこれです。(2021/8/19、削除されていることを確認)

(レスポンスが404の時)fetchAlbumがnullを返した場合、スピナー(プログレスインジケータ)はいつまでも回り続けることになります。(2021/8/19、削除されていることを確認)

上記の説明の通り、”404 Not Found”の時にfetchAlbumメソッドがnullを返した場合、snapshot.hasDataがfalseになり、snapshot.hasErrorもfalseになるので、ifブロック、if-elseブロックは無視され、スピナーが表示される(いつまでも)。

FutureBuilder<Album>(
  future: futureAlbum,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Text(snapshot.data!.title);
    } else if (snapshot.hasError) {
      return Text("${snapshot.error}");
    }

    // By default, show a loading spinner.
    return CircularProgressIndicator();
  },
);

 


Why is fetchAlbum() called in initState()?

buildメソッド内でAPIコールを呼び出す(fetchAlbumメソッドを呼び出す)ことは便利ですが、お勧め出来ません。

Flutterは、ビュー内(画面表示)の何かを変更する必要があるたびにbuild()メソッドを呼び出しますが、これは驚くほど頻繁に発生します。

fetchメソッドをbuild()メソッド内に置くと、不要なAPIの呼び出しで溢れ、アプリの速度が低下します。


Testing

今回のこの機能をテストする方法については、次のレシピを参照してください。


Complete example

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

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

Future<Album> fetchAlbum() async {
  final response = await http
      .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

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

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

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
    );
  }
}

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late Future<Album> futureAlbum;

  @override
  void initState() {
    super.initState();
    futureAlbum = fetchAlbum();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fetch Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Fetch Data Example'),
        ),
        body: Center(
          child: FutureBuilder<Album>(
            future: futureAlbum,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text(snapshot.data!.title);
              } else if (snapshot.hasError) {
                return Text('${snapshot.error}');
              }

              // By default, show a loading spinner.
              return const CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}

 

 


fetchAlbumメソッド内にprint文を追加して、型変換部分の型を確認する。

Future<Album> fetchAlbum() async {
  final response =
  await http.get(Uri.https('jsonplaceholder.typicode.com', 'albums/1'));
  print(response);
  print(response.runtimeType);
  print(response.body.runtimeType);
  print(response.body);
  print(jsonDecode(response.body).runtimeType);

  if (response.statusCode == 200) {

    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

 

//結果

flutter: Instance of 'Response'  ←print(response);
flutter: Response                ←print(response.runtimeType);
flutter: String                  ←print(response.body.runtimeType);
flutter: {                       ←print(response.body);
  "userId": 1,
  "id": 1,
  "title": "quidem molestiae enim"
}
flutter: _InternalLinkedHashMap<String, dynamic>
          ↑print(jsonDecode(response.body).runtimeType);

参考

https://flutter.dev/docs/cookbook/networking/fetch-data

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です