採用はこちら!

Shinonome Tech Blog

株式会社Shinonomeの技術ブログ
6 min read

【Flutter】APIからデータを取得しよう

今回はFlutter公式のParse JSON in the backgroundの内容をベースにFlutterでAPIからデータを取得・表示する方法について初心者でもわかりやすいように解説していきます。

こんにちは!

PlayGroundでFlutterの学習をしているyo, ko_chaです!

今回はFlutter公式のParse JSON in the backgroundの内容をベースにFlutterでAPIからデータを取得・表示する方法について初心者でもわかりやすいように解説していきます。

今回のゴール

「FlutterでAPIからデータを取得」

そもそもAPIって?

Web APIとは、Webを通してWebサーバとデータのやり取りを行うことができる仕組みのことです。

例えば、スマホアプリからWebサーバにデータを要求した場合、要求されたデータがWebサーバからスマホアプリに返されます。

このとき、Webサーバにデータを要求することを「リクエスト」、Webサーバから返されるデータのことを「レスポンス」と呼びます。

また、Webサーバと通信するアプリのことを「クライアント」と呼びます。

難しい単語がたくさん出てきましたね。

ですが、今は全て理解できなくても大丈夫です。

スマホアプリから「リクエスト」を送信し、Webサーバから「レスポンス」というデータが返ってくる。

そこさえ理解できていればOKです。

使用するAPI

世の中にはたくさんのAPIが存在します。

ニュース記事を表示するAPIや飲食店を表示するAPI、マップを表示してくれるAPIなど種類は様々です。

今回はそんなAPIの中から「JSON Placeholder」という練習にピッタリなAPIを使用していきます。

APIドキュメントを読んでみよう

APIドキュメントとは、各APIに用意されているAPIの取扱説明書のようなものです。

APIからデータを取得すると言っても、Webサーバのどこにデータが存在し、どのような種類のデータが返ってくるかなんてわかりませんよね。

そこで役に立つのがAPIドキュメントです。

JSON PlaceholderにもAPIドキュメントが用意されているため、一緒に読み進めていきましょう。

https://jsonplaceholder.typicode.com/

このような画面が表示されたかと思われます。

画面に表示された英語を見た瞬間、ブラウザを閉じたくなった方がいらっしゃるかもしれませんが、早まらないでください。

今回はAPIドキュメントを一緒に読み進めて行くので安心してください。

URL

URLとは、インターネット上における住所のような役割を果たしています。

恐らく、みなさん一度はURLを目にしたことがあると思います。

例えば、YouTubeで動画を見たいとき、ブラウザでhttps://www.youtube.com/にアクセスし、動画を視聴(取得)しますよね。

これと同じように、APIでデータを取得する際もURLを指定する必要があります。

先ほど開いてもらったJSON Placeholderを下の方へスクロールすると、「Resources」という項目があるかと思います。

これは、APIが提供しているデータの項目です。

今回は写真を含むデータを取得したいので、/photosを選択します。

APIドキュメントの/photosをクリックしてみると、以下のような画面が表示されます。

※怪しい呪文のようなものが大量に出現しますが、このあと詳しく説明するので一旦スルーしてください。

この赤枠で囲った部分が「リクエストURL」というリクエストを送る際の宛先となるURLです。

リクエストURLは以下の構成になっています。(先頭のhttps://は省略されています)

JSONって?

JSONはデータ形式の一種です。

先ほど、怪しい呪文のようなものが表示されたかと思いますが、あの怪しい呪文の正体はJSON形式のレスポンスです。

JSON Placeholderを含む多くのAPIにおいて、レスポンスのデータはJSON形式が返されます。

JSONの読み方

基本

Map(辞書型)と同じように、”key”: 値の形式でデータが入っています。

文字列は””で囲みます。

"title": "accusamus beatae ad facilis cum similique qui sunt",

整数や小数はそのまま数値を記入します。

"albumId": 1,

オブジェクト

Classと同じように{}の中にデータが入っています。

{
  "albumId": 1,
  "id": 1,
  "title": "accusamus beatae ad facilis cum similique qui sunt",
  "url": "https://via.placeholder.com/600/92c952",
  "thumbnailUrl": "https://via.placeholder.com/150/92c952"
},

リスト

Listと同じように[]の中にカンマ区切りで値を入れます。

複数のオブジェクトが[]で囲まれていることから、このJSONは複数のオブジェクトのリストであることがわかります。

[
  {
    "albumId": 1,
    "id": 1,
    "title": "accusamus beatae ad facilis cum similique qui sunt",
    "url": "https://via.placeholder.com/600/92c952",
    "thumbnailUrl": "https://via.placeholder.com/150/92c952"
  },
  {
    "albumId": 1,
    "id": 2,
    "title": "reprehenderit est deserunt velit ipsam",
    "url": "https://via.placeholder.com/600/771796",
    "thumbnailUrl": "https://via.placeholder.com/150/771796"
  },
  {
    "albumId": 1,
    "id": 3,
    "title": "officia porro iure quia iusto qui ipsa ut modi",
    "url": "https://via.placeholder.com/600/24f355",
    "thumbnailUrl": "https://via.placeholder.com/150/24f355"
  },
	(長すぎるので省略)
]

Photoクラスの作成

レスポンスのJSONデータはそのままだと使用できません。

そのため、レスポンスのJSONデータをFlutterで使用できるようにPhotoクラスを作成します。

クラスはint型やString型と同じように、型として扱うことができます。

JSONデータを受け取ってPhotoという型に流し込むというイメージを持っていただくと想像しやすいかもしれません。

fromJsonメソッドを定義することにより、JSONデータをFlutterで利用できる型(int型やString型など)に変換することができます。

class Photo {
  final int albumId;
  final int id;
  final String title;
  final String url;
  final String thumbnailUrl;

  const Photo({
    required this.albumId,
    required this.id,
    required this.title,
    required this.url,
    required this.thumbnailUrl,
  });

  factory Photo.fromJson(Map<String, dynamic> json) {
    return Photo(
      albumId: json['albumId'] as int,
      id: json['id'] as int,
      title: json['title'] as String,
      url: json['url'] as String,
      thumbnailUrl: json['thumbnailUrl'] as String,
    );
  }
}

HTTP通信

いよいよリクエストを送信してみましょう。

fetchPhotosメソッドでは、APIサーバーへのリクエストの送信を行なっています。

受け取ったレスポンスをparsePhotosメソッドに渡し、List型に変換します。

// A function that converts a response body into a List<Photo>.
List<Photo> parsePhotos(String responseBody) {
  final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();

  return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}

Future<List<Photo>> fetchPhotos(http.Client client) async {
  final response = await client
      .get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));

  // Use the compute function to run parsePhotos in a separate isolate.
  return parsePhotos(response.body);
}

取得したデータを表示してみよう

さて、お次は画面にデータを表示していきます。ここで核となっているのが、FutureBuilder で、これはFuture関数を実行し、値が返ってくるまでの間は別のWidgetを表示しておいてくれるWidgetです。

骨組みとなるパーツ

//(斜体にしてある部分は筆者が書いた日本語の説明)
FutureBuilder<*型*>(
	future: *Future<型>を戻り値として持つ関数*,
	builder: (context, snapshot) {
		return *ここで指定したWidgetが画面に表示される*;
	}
)

future:で指定した関数が実行される⇒データが返ってきたらsnapshotという変数に格納される

snapshotのデータがあるorないorエラーで条件分岐をし、表示させるWidgetを切り替えます。

今回FutureBuilderが使われている部分

FutureBuilder<List<Photo>>(
        future: fetchPhotos(http.Client()),
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            //snapshotがエラーだった場合
            return const Center(
              child: Text('An error has occurred!'),
              //エラー文を画面に表示
            );
          } else if (snapshot.hasData) {
            //snapshotにデータが入っていたら
            return PhotosList(photos: snapshot.data!);
						//PhotoListを表示
          } else {
            return const Center(
              child: CircularProgressIndicator(),
              //待機画面のぐるぐる
            );
          }
        },
      ),

データ取得成功時に表示するStatelessWidget:PhotoList

class PhotosList extends StatelessWidget {
  //①StatelessWidget classを継承したPhotosList classを定義

  const PhotosList({super.key, required this.photos});
  final List<Photo> photos;
  //②値渡しで使うやつ。Photo型のListを型に持つ変数photosを定義

  @override
  //③classを継承すると、継承元class(=StatelessWidget)内の関数が使える。
  // StatelessWidget内でbuildという関数が定義されている。
  // @overrideで、継承元から持ってきた関数をこのclass内だけ書き換えることができる。
  Widget build(BuildContext context) {
    //戻り値の型がWidget,関数の名前がbuild,引数がBuildContext型の変数context
		//という関数を定義。
    return GridView.builder(
      //④GridViewというWidget
      //.builderで、WidgetではないListを元に、Widgetを作りながら並べて行くイメージ。
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
			//GritViewの設定をする。
        crossAxisCount: 2,
			//横に何個並べるかを指定
      ),
      itemCount: photos.length,
      //GridViewの要素数を設定。
      // PhotoのList"photos"が持っているプロパティ"length"でListの要素数を取得できる。
      itemBuilder: (context, index) {
        //itemBuilderプロパティ
        return Image.network(photos[index].thumbnailUrl);
        //このreturnにWidgetを入れると、itemCountの数だけ同じWidgetを並べてくれる。
        //それぞれ、何番目のWidgetかに対応するindexという値を持っている。
				//「index番目のデータを持ったWidget」を並べることができる。
        //上記だと、Image.networkの引数に、
        //photoの要素のindex番目のPhotoが持つthumbnailUrlという文字列を指定している。
      },
    );
  }
}

1~3は流し読みでOK

extends の意味は「Dart 継承」などで調べると出てきます。ここでは詳しくは触れませんが、statelessWidget というFlutter内部で定義されたclassをベースにして、その内部を書き換えたり追加したりしたものに、PhotoListという名前をつけて定義するというイメージです。

値渡しです。忘れていたらこちらを見返しましょう!

@override も「継承」に関係します。ひとまずは、4の上のreturnの値を変えると画面に表示されるWidgetが変わる、ということが分かれば大丈夫です。

GridViewでは、以下のようにWidgetを並べることができます。

以下は、今回使われているGridViewの骨組みを切り出したものです。

GridView.builder(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
      ),
      itemCount: *並べる個数*,
      itemBuilder: (context, index) {
        return *並べたいWidget*;
      },
    );

お疲れ様でした!