目次

インターネットからのデータ取得

ほとんどのアプリケーションは、インターネットからの何らかの通信またはデータ取得を必要とします。多くのアプリは、URI(Uniform Resource Identifier)で識別されるリソースに対して特定のアクションを実行するために、クライアントからサーバーに送信されるHTTPリクエストを通じてそうします。

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の使用方法の詳細については、JSONの使用ガイドを参照してください。

HTTPリクエスト

#

HTTP(Hypertext Transfer Protocol)は、当初はWebクライアントとWebサーバー間でドキュメントを転送するために設計されたステートレスプロトコルです。ブラウザがWebサーバーからページの内容を取得するためにHTTP `GET`リクエストを使用するため、このページを読み込むためにこのプロトコルと対話しました。導入以来、HTTPプロトコルとそのさまざまなバージョンの使用はWeb以外のアプリケーションにも拡大しており、本質的にクライアントからサーバーへの通信が必要な場所であればどこでも使用されています。

サーバーと通信するためにクライアントから送信されるHTTPリクエストは、複数のコンポーネントで構成されています。package:httpなどのHTTPライブラリを使用すると、次の種類の通信を指定できます。

  • データの取得のための`GET`や新しいデータの送信のための`POST`など、目的のアクションを定義するHTTPメソッド。
  • URIによるリソースの場所。
  • 使用されているHTTPのバージョン。
  • サーバーに追加情報を提供するヘッダー。
  • オプションの本文。リクエストは、データを取得するだけでなく、サーバーにデータを送信することもできます。

HTTPプロトコルの詳細については、mdn Web docsの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のURLとは何か?を参照してください。

必要な依存関係の取得

#

package:httpライブラリは、オプションのきめ細かい制御を備えた、コンポジション可能なHTTPリクエストを行うためのクロスプラットフォームソリューションを提供します。

package:httpへの依存関係を追加するには、リポジトリの一番上から次のdart pub addコマンドを実行します。

$ dart pub add http

コードでpackage:httpを使用するには、インポートし、必要に応じてライブラリのプレフィックスを指定します。

dart
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つの方法を示しています。

dart
// 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形式の情報を文字列として取得し、それを出力します。

dart
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のブラウザでも確認できます。

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をデコードする際に必要となるため、データの構造(この場合はマップ)に注意してください。

ステータスコードやヘッダーなど、レスポンスから他の情報が必要な場合は、ステータスコードまたはヘッダーなど、FutureResponseを返すトップレベルのget関数を使用できます。

次のスニペットでは、getを使用してレスポンス全体を取得し、ステータスコードが200で示されるように、リクエストが成功しなかった場合は早期に終了します。

dart
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レスポンスステータスコードを参照してください。

認証やユーザーエージェント情報など、より多くの情報が必要なサーバーリクエストもあります。この場合、HTTPヘッダーを含める必要がある場合があります。キーと値のペアのMap<String, String>headersオプションの命名パラメーターとして渡すことで、ヘッダーを指定できます。

dart
await http.get(Uri.https('dart.dev', '/f/packages/http.json'),
    headers: {'User-Agent': '<product name>/<product-version>'});

複数リクエストの実行

#

同じサーバーに複数のリクエストを行う場合は、代わりにClientを使用して永続的な接続を維持できます。これは、トップレベルのものと同様のメソッドを持っています。終了したら、closeメソッドでクリーンアップすることを忘れないでください。

dart
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をインポートし、作成したClientRetryClientでラップします。

dart
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:convertライブラリのDart組み込みのjson.decode関数を使用して、生の文字列をDartオブジェクトを使用してJSON表現に変換できます。この場合、JSONデータはマップ構造で表され、JSONではマップキーは常に文字列であるため、json.decodeの結果をMap<String, dynamic>にキャストできます。

dart
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を除くすべてのフィールドは必須であり、常に提供されると想定しています。

dart
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オブジェクトに変換するメカニズムを追加する必要があります。

必要な型をキャストし、オプションのrepositoryフィールドを処理して、前のJSON形式に合わせて手動でfromJsonメソッドを作成することで、デコードされたJSONを変換します。

dart
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のシリアル化とデシリアル化の詳細については、JSONの使用ガイドを参照してください。

レスポンスを構造化クラスのオブジェクトに変換する

#

これで、データを格納するクラスと、デコードされたJSONオブジェクトをその型のオブジェクトに変換する方法ができました。次に、すべてをまとめる関数を作成できます。

  1. 渡されたパッケージ名に基づいてURIを作成します。
  2. そのパッケージのデータを取得するためにhttp.getを使用します。
  3. リクエストが成功しなかった場合は、Exception、またはできれば独自のExceptionサブクラスをスローします。
  4. リクエストが成功した場合は、json.decodeを使用してレスポンス本文をJSON文字列にデコードします。
  5. 作成したPackageInfo.fromJsonファクトリコンストラクターを使用して、デコードされたJSON文字列をPackageInfoオブジェクトに変換しました。
dart
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に移動できます。