インターネットからデータを取得する
ほとんどのアプリケーションは、インターネットとの通信またはデータ取得を必要とします。多くのアプリは、HTTP リクエストを介してこれを実行します。HTTP リクエストは、クライアントからサーバーに送信され、URI (Uniform Resource Identifier) によって識別されるリソースに対して特定のアクションを実行します。
HTTP を介して通信されるデータは技術的にはどのような形式でも構いませんが、JSON (JavaScript Object Notation) を使用することは、人間が読みやすく、言語に依存しない性質から人気のある選択肢です。Dart SDK とエコシステムは、アプリの要件に最適な複数のオプションで JSON を幅広くサポートしています。
このチュートリアルでは、HTTP リクエスト、URI、JSON について詳しく学びます。次に、package:http および dart:convert ライブラリの Dart の JSON サポートを使用して、HTTP サーバーから取得した JSON フォーマットのデータを取得、デコード、そして使用する方法を学びます。
背景の概念
#次のセクションでは、サーバーからのデータ取得を容易にするために、チュートリアルで使用されるテクノロジーと概念に関する追加の背景情報と情報を提供します。チュートリアルコンテンツに直接スキップするには、必要な依存関係を取得するを参照してください。
JSON
#JSON (JavaScript Object Notation) は、アプリケーション開発およびクライアントサーバー通信でユビキタスになったデータ交換フォーマットです。軽量でありながら、テキストベースであるため、人間が読み書きしやすいです。JSON を使用すると、さまざまなデータ型と、リストやマップなどの単純なデータ構造をシリアル化し、文字列で表すことができます。
ほとんどの言語には多くの実装があり、パーサーは非常に高速になっているため、相互運用性やパフォーマンスについて心配する必要はありません。JSON フォーマットの詳細については、Introducing JSON を参照してください。Dart で JSON を操作する方法については、Using JSON ガイドを参照してください。
HTTP リクエスト
#HTTP (Hypertext Transfer Protocol) は、ドキュメントを、元々は Web クライアントと Web サーバー間で送信するために設計されたステートレス プロトコルです。このページを読み込むためにプロトコルを操作しましたが、ブラウザは HTTP GET リクエストを使用して Web サーバーからページのコンテンツを取得します。導入以来、HTTP プロトコルとそのさまざまなバージョンの使用は、Web 以外のアプリケーションにも拡張されており、本質的にクライアントからサーバーへの通信が必要な場所であればどこでも使用されます。
サーバーと通信するためにクライアントから送信される HTTP リクエストは、複数のコンポーネントで構成されます。package:http のような HTTP ライブラリを使用すると、次の種類の通信を指定できます。
- データの取得のための GETや新しいデータの送信のためのPOSTなど、目的のアクションを定義する HTTP メソッド。
- URI を介したリソースの場所。
- 使用されている HTTP のバージョン。
- サーバーに余分な情報を提供するヘッダー。
- リクエストがデータを取得するだけでなくサーバーに送信できるように、オプションのボディ。
HTTP プロトコルについて詳しく知りたい場合は、mdn web docs の An overview of HTTP を参照してください。
URI と URL
#HTTP リクエストを行うには、リクエストされたリソースまたはアクセスされるエンドポイントを識別する URI (Uniform Resource Identifier) を提供する必要があります。URI はリソースを一意に識別する文字列です。URL (Uniform Resource Locator) は、リソースの場所も提供する URI の一種です。Web 上のリソースの URL には 3 つの情報が含まれます。この現在のページの場合、URL は次のように構成されます。
- 使用されるプロトコルを決定するために使用されるスキーム: https
- サーバーの権限またはホスト名: dart.dev
- リソースへのパス: /tutorials/server/fetch-data.html
現在のページでは使用されていないその他のオプション パラメーターもあります。
- 追加の動作をカスタマイズするためのパラメーター: ?key1=value1&key2=value2
- サーバーに送信されない、リソース内の特定の位置を指すアンカー: #uris
URL について詳しく知りたい場合は、mdn web docs の What is a URL? を参照してください。
必要な依存関係を取得する
#package:http ライブラリは、オプションのきめ細かな制御を備えた、 composable な HTTP リクエストを作成するためのクロスプラットフォーム ソリューションを提供します。
package:http への依存関係を追加するには、リポジトリの先頭から次の dart pub add コマンドを実行します。
dart pub add httpコードで package:http を使用するには、それをインポートし、オプションで ライブラリ プレフィックスを指定します。
import 'package:http/http.dart' as http;package:http の詳細については、pub.dev サイトの ページとその API ドキュメントを参照してください。
URL を構築する
#前述のように、HTTP リクエストを行うには、まずリクエストされたリソースまたはアクセスされるエンドポイントを識別する URL が必要です。
Dart では、URL は Uri オブジェクトで表されます。Uri を構築する方法はたくさんありますが、その柔軟性から、Uri.parse で文字列を解析して作成するのが一般的なソリューションです。
次のスニペットは、このサイトでホストされている package:http に関するモック JSON フォーマット情報を示す Uri オブジェクトを 2 つの方法で作成する方法を示しています。
// Parse the entire URI, including the scheme
Uri.parse('https://dart.dokyumento.jp/f/packages/http.json');
// Specifically create a URI with the https scheme
Uri.https('dart.dev', '/f/packages/http.json');URI の構築と操作の他の方法については、URI ドキュメントを参照してください。
ネットワーク リクエストを行う
#リクエストされたリソースの文字列表現をすばやく取得する必要がある場合は、package:http にあるトップレベルの read 関数を使用できます。これは Future<String> を返します。リクエストが成功しなかった場合は ClientException をスローします。次の例では、read を使用して package:http に関するモック JSON フォーマット情報を文字列として取得し、それを表示します。
void main() async {
  final httpPackageUrl = Uri.https('dart.dev', '/f/packages/http.json');
  final httpPackageInfo = await http.read(httpPackageUrl);
  print(httpPackageInfo);
}これにより、次の JSON フォーマットの出力が得られます。これは、ブラウザで /f/packages/http.json でも確認できます。
{
  "name": "http",
  "latestVersion": "1.1.2",
  "description": "A composable, multi-platform, Future-based API for HTTP requests.",
  "publisher": "dart.dev",
  "repository": "https://github.com/dart-lang/http"
}データの構造(この場合はマップ)に注意してください。これは、後で JSON をデコードするときに必要になります。
ステータス コードや ヘッダーなど、レスポンスから他の情報が必要な場合は、代わりにトップレベルの get 関数を使用できます。これは Future を Response オブジェクトで返します。
次のスニペットでは、get を使用して完全なレスポンスを取得し、リクエストが成功しなかった場合に早期に終了できるようにします。これは、ステータス コード **200** で示されます。
void main() async {
  final httpPackageUrl = Uri.https('dart.dev', '/f/packages/http.json');
  final httpPackageResponse = await http.get(httpPackageUrl);
  if (httpPackageResponse.statusCode != 200) {
    print('Failed to retrieve the http package!');
    return;
  }
  print(httpPackageResponse.body);
}**200** 以外にも多くのステータス コードがあり、アプリはそれらを異なる方法で処理する場合があります。さまざまなステータス コードの意味について詳しく知りたい場合は、mdn web docs の HTTP response status codes を参照してください。
一部のサーバーリクエストでは、認証やユーザーエージェント情報など、追加情報が必要になる場合があります。その場合、キーと値のペアの Map<String, String> を headers オプションの名前付きパラメーターとして渡すことで、HTTP ヘッダーを含める必要がある場合があります。
await http.get(
  Uri.https('dart.dev', '/f/packages/http.json'),
  headers: {'User-Agent': '<product name>/<product-version>'},
);複数のリクエストを行う
#同じサーバーに複数のリクエストを行う場合は、代わりに Client を使用して永続的な接続を維持できます。これはトップレベルのメソッドと同様のメソッドを持っています。終了したら、必ず close メソッドでクリーンアップしてください。
void main() async {
  final httpPackageUrl = Uri.https('dart.dev', '/f/packages/http.json');
  final client = http.Client();
  try {
    final httpPackageInfo = await client.read(httpPackageUrl);
    print(httpPackageInfo);
  } finally {
    client.close();
  }
}クライアントが失敗したリクエストを再試行できるようにするには、package:http/retry.dart をインポートし、作成した Client を RetryClient でラップします。
import 'package:http/http.dart' as http;
import 'package:http/retry.dart';
void main() async {
  final httpPackageUrl = Uri.https('dart.dev', '/f/packages/http.json');
  final client = RetryClient(http.Client());
  try {
    final httpPackageInfo = await client.read(httpPackageUrl);
    print(httpPackageInfo);
  } finally {
    client.close();
  }
}RetryClient は、再試行回数と各リクエスト間の待機時間に関するデフォルトの動作を持っていますが、その動作は RetryClient() または RetryClient.withDelays() コンストラクタのパラメーターを通じて変更できます。
package:http にはさらに多くの機能とカスタマイズがあるため、pub.dev サイトの ページとその API ドキュメントを確認してください。
取得したデータをデコードする
#ネットワークリクエストを行い、返されたデータを文字列として取得できましたが、文字列から情報の特定の部分にアクセスすることは困難な場合があります。
データはすでに JSON フォーマットであるため、Dart の組み込みの json.decode 関数を dart:convert ライブラリで使用して、生の文字列を Dart オブジェクトを使用した JSON 表現に変換できます。この場合、JSON データはマップ構造で表され、JSON ではマップキーは常に文字列であるため、json.decode の結果を Map<String, dynamic> にキャストできます。
import 'dart:convert';
import 'package:http/http.dart' as http;
void main() async {
  final httpPackageUrl = Uri.https('dart.dev', '/f/packages/http.json');
  final httpPackageInfo = await http.read(httpPackageUrl);
  final httpPackageJson = json.decode(httpPackageInfo) as Map<String, dynamic>;
  print(httpPackageJson);
}データを格納するための構造化されたクラスを作成する
#デコードされた JSON により構造を与え、扱いやすくするために、取得したデータを特定の型を使用して格納できるクラスを作成します。これは、データのスキーマによって異なります。
次のスニペットは、要求したモック JSON ファイルから返されたパッケージ情報を格納できるクラスベースの表現を示しています。この構造では、repository 以外のすべてのフィールドが必要であり、毎回提供されると想定しています。
class PackageInfo {
  final String name;
  final String latestVersion;
  final String description;
  final String publisher;
  final Uri? repository;
  PackageInfo({
    required this.name,
    required this.latestVersion,
    required this.description,
    required this.publisher,
    this.repository,
  });
}クラスにデータをエンコードする
#データを格納するクラスができたので、デコードされた JSON を PackageInfo オブジェクトに変換するメカニズムを追加する必要があります。
以前の JSON フォーマットに一致する fromJson メソッドを手動で記述し、必要に応じて型をキャストし、オプションの repository フィールドを処理して、デコードされた JSON を変換します。
class PackageInfo {
  // ···
  factory PackageInfo.fromJson(Map<String, dynamic> json) {
    final repository = json['repository'] as String?;
    return PackageInfo(
      name: json['name'] as String,
      latestVersion: json['latestVersion'] as String,
      description: json['description'] as String,
      publisher: json['publisher'] as String,
      repository: repository != null ? Uri.tryParse(repository) : null,
    );
  }
}前の例のような手書きのメソッドは、比較的単純な JSON 構造では十分であることが多いですが、より柔軟なオプションもあります。JSON シリアライゼーションとデシリアライゼーション、および変換ロジックの自動生成について詳しく知りたい場合は、Using JSON ガイドを参照してください。
レスポンスを構造化されたクラスのオブジェクトに変換する
#これで、データを格納するクラスと、デコードされた JSON オブジェクトをその型のオブジェクトに変換する方法ができました。次に、すべてをまとめる関数を作成できます。
- パッケージ名が渡されたものに基づいて URIを作成します。
- http.getを使用して、そのパッケージのデータを取得します。
- リクエストが成功しなかった場合は、Exception、またはできれば独自のカスタムExceptionサブクラスをスローします。
- リクエストが成功した場合は、json.decodeを使用してレスポンスボディを JSON 文字列にデコードします。
- 作成した PackageInfo.fromJsonファクトリコンストラクタを使用して、デコードされた JSON 文字列をPackageInfoオブジェクトに変換します。
Future<PackageInfo> getPackage(String packageName) async {
  final packageUrl = Uri.https('dart.dev', '/f/packages/$packageName.json');
  final packageResponse = await http.get(packageUrl);
  // If the request didn't succeed, throw an exception
  if (packageResponse.statusCode != 200) {
    throw PackageRetrievalException(
      packageName: packageName,
      statusCode: packageResponse.statusCode,
    );
  }
  final packageJson = json.decode(packageResponse.body) as Map<String, dynamic>;
  return PackageInfo.fromJson(packageJson);
}
class PackageRetrievalException implements Exception {
  final String packageName;
  final int? statusCode;
  PackageRetrievalException({required this.packageName, this.statusCode});
}変換されたデータを利用する
#データを取り込み、より簡単にアクセスできる形式に変換できたので、好きなように使用できます。可能性としては、CLI に情報を出力したり、Web または Flutter アプリに表示したりすることがあります。
http および path パッケージに関するモック情報を取得、デコード、表示する、完全で実行可能な例を次に示します。
import 'dart:convert';
import 'package:http/http.dart' as http;
void main() async {
  await printPackageInformation('http');
  print('');
  await printPackageInformation('path');
}
Future<void> printPackageInformation(String packageName) async {
  final PackageInfo packageInfo;
  try {
    packageInfo = await getPackage(packageName);
  } on PackageRetrievalException catch (e) {
    print(e);
    return;
  }
  print('Information about the $packageName package:');
  print('Latest version: ${packageInfo.latestVersion}');
  print('Description: ${packageInfo.description}');
  print('Publisher: ${packageInfo.publisher}');
  final repository = packageInfo.repository;
  if (repository != null) {
    print('Repository: $repository');
  }
}
Future<PackageInfo> getPackage(String packageName) async {
  final packageUrl = Uri.https('dart.dev', '/f/packages/$packageName.json');
  final packageResponse = await http.get(packageUrl);
  // If the request didn't succeed, throw an exception
  if (packageResponse.statusCode != 200) {
    throw PackageRetrievalException(
      packageName: packageName,
      statusCode: packageResponse.statusCode,
    );
  }
  final packageJson = json.decode(packageResponse.body) as Map<String, dynamic>;
  return PackageInfo.fromJson(packageJson);
}
class PackageInfo {
  final String name;
  final String latestVersion;
  final String description;
  final String publisher;
  final Uri? repository;
  PackageInfo({
    required this.name,
    required this.latestVersion,
    required this.description,
    required this.publisher,
    this.repository,
  });
  factory PackageInfo.fromJson(Map<String, dynamic> json) {
    final repository = json['repository'] as String?;
    return PackageInfo(
      name: json['name'] as String,
      latestVersion: json['latestVersion'] as String,
      description: json['description'] as String,
      publisher: json['publisher'] as String,
      repository: repository != null ? Uri.tryParse(repository) : null,
    );
  }
}
class PackageRetrievalException implements Exception {
  final String packageName;
  final int? statusCode;
  PackageRetrievalException({required this.packageName, this.statusCode});
  @override
  String toString() {
    final buf = StringBuffer();
    buf.write('Failed to retrieve package:$packageName information');
    if (statusCode != null) {
      buf.write(' with a status code of $statusCode');
    }
    buf.write('!');
    return buf.toString();
  }
}
次は何をしますか?
#インターネットからデータを取得、解析、使用できるようになったので、Dart の並行性についてさらに学ぶことを検討してください。データが大きく複雑な場合は、インターフェイスが応答しなくなるのを防ぐために、取得とデコードを別の isolate に移動してバックグラウンド ワーカーとして使用できます。