本記事はFlutter公式ドキュメントを学習したメモです。

ほとんどのアプリはアプリで完結するのではなく、ネットワークを通ってデータのやり取りが発生する。

ここでは公式ドキュメントで紹介されているhttp使用方法について確認していく。

はじめに

とにかくプロジェクトを作って基本的なレイアウトを作っておこう。

1
2
3
4
flutter create simple_http
cd simple_http
flutter pub add http
code .

これでプロジェクトを作って今回必要なhttpパッケージを追加した。

ここで使用中のdart、flutterバージョンを載せておく。

1
2
Dart SDK version: 3.2.3
Flutter 3.16.5

上でhttpパッケージを追加しているのでpubspec.yamlファイルで次のような内容が確認できると思う。

1
  http: ^1.2.0

main.dartに生成されたコードは削除しておこう。

次のように簡単なレイアウトを作っておく。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Colors.deepPurple[50],
        appBar: AppBar(
          elevation: 2,
          backgroundColor: Colors.deepPurple,
          foregroundColor: Colors.white,
          title: const Text(
            'HTTPテスト',
            style: TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ),
    );
  }
}

APIを使ってみる

今回、使うAPIはテスト用のJSONデータを提供しているサービスだ。

JSON PlaceHolder

次のURLで仮想の記事を取得する。

https://jsonplaceholder.typicode.com/posts

接続してみると次のようにJSONデータが取得されるのが確認できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 20240211141043
// https://jsonplaceholder.typicode.com/posts

[
  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "qui est esse",
    "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
  },
  {
    "userId": 1,
    "id": 3,
    "title": "ea molestias quasi exercitationem repellat qui ipsa sit aut",
    "body": "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"
  },

上は一部だけだが、100件の記事が取得できる。

APIを使うためのクラスを作成しよう。

/lib/servicesフォルダを作ってjson_place_holder.dartファイルを作ってJsonPlaceHolderクラスを作成しよう。

1
class JsonPlaceHolder {}

このクラスで必要なものは何かまずは上で紹介したURLだろう。

1
final String postsUrl = 'https://jsonplaceholder.typicode.com/posts';

記事リストを取得する関数を作る。

1
void getPosts() {}

この関数ではhttpパッケージの機能を使うので次のインポートを追加しておく。

1
import 'package:http/http.dart' as http;

今回使うのはhttpパッケージのget関数だ。インターネット経由で要請(リクエスト)してその応答(レスポンス)をもらうときに使う代表的なものだ。

Tooltip

上のイメージからわかるようにget関数の返却タイプはFuture<Response>だ。未来にResponseといつタイプを返却するとのこと。

Futureなので処理が終わるまで待ちたい。そのためにはawaitを使う必要があり、awaitを使うにはgetPosts関数を非同期関数にする必要がある。

1
void getPosts() async {}

また、上のイメージでget関数のパラメータはUriというものだ。UriクラスはURLを含むいろんな情報を持っているもので文字列からUriクラスを作るには次のようにparseメソッドを使う。

1
http.get(Uri.parse(postsUrl));

getの戻り値を変数に設定しよう。処理が終わるまで待ちたいのでawaitをつける。

1
final response = await http.get(Uri.parse(postsUrl));

レスポンスがもらえたら、すぐ使うのではなく正常なのか確認が必要だ。正常の確認はHTTPステータスコードで実施する。

HTTPステータスコードは、getの場合は200、postの場合は201が正常だ。

今回はgetなので200以外はエラーとして処理を終わらせる。

1
2
3
    if (response.statusCode != 200) {
      throw Exception('HTTP Error(${response.statusCode})');
    }

responseそのものにデータがあるのではなく、bodyにデータを持っている。bodyを出力してみよう。

次がJsonPlaceHolderクラスの全体コードだ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'package:http/http.dart' as http;

class JsonPlaceHolder {
  // 記事リストURL
  final String postsUrl = 'https://jsonplaceholder.typicode.com/posts';

  void getPosts() async {
    // レスポンス
    final response = await http.get(Uri.parse(postsUrl));
    // 正常以外は処理終了
    if (response.statusCode != 200) {
      throw Exception('HTTP Error(${response.statusCode})');
    }
    print(response.body);
  }
}

main関数に追加して何が出力されるか確認してみよう。

1
2
3
4
void main() {
  JsonPlaceHolder().getPosts();
  runApp(const MyApp());
}

DEBUG Console

デバッグコンソルに何かいっぱい出力されたので成功したぽい。

モデル

インターネット経由でデータを取得できたが、下のようにbodyはStringタイプだ。

Tooltip

中身はキー、バリューのJSONデータのリストであるが、bodyそのものはStringである。

これをコード内で使うためにはクラスとして変換しないといけない。

ここで使えるのがjsonDecode関数である。

Tooltip

ツールチップで見えるようにパラメータはString、戻りはdynamicである。なんでも入るdynamic。

デバッグコンソル確認したデータはJSONオブジェクトのリストであった。なのでjsonDecodeの戻りをリストに入れてみよう。

1
final List<dynamic> posts = jsonDecode(response.body);

ここでうまく入ったのかループで出力してみよう。

1
2
3
    for (var post in posts) {
      print(post);
    }

DEBUG Console

記事が1つずつ出力されていることが確認できた。

記事1つ1つをオブジェクトにするため、モデルクラスを作っていこう。

/lib/modelsフォルダを作ってpost_model.dartファイルを作成してPostModelクラスを作る。

1
class PostModel {}

下の内容をみるとPostModelに必要なメンバー変数がわかる。

1
2
3
4
5
6
  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  }

まず、userId、idはintタイプになるだろう。title、bodyはStringタイプだ。

メンバー変数を追加してコンストラクターで初期化するようにしよう。

1
2
3
4
5
6
7
8
9
10
11
12
13
class PostModel {
  final int userId;
  final int id;
  final String title;
  final String body;

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

先ほどAPIを使ったときにデータはJSON形式になっていた。通常のコンストラクターではなく、JSONで初期化するようにネームドコンストラクターを作ってみよう。

1
2
3
4
5
  PostModel.fromJson(Map<String, dynamic> json)
      : userId = json['userId'],
        id = json['id'],
        title = json['title'],
        body = json['body'];

このやり方はレスポンスのJSON形式データが、いつもすべての項目が必ずあるといのが前提だ。もし、設定されない項目がある場合は該当項目はnullを許容できるようにするか、チェックして何らかのデフォルト値を設定する必要がある。

PostModelクラスを使うようにgetPostsメソッドを変えてみよう。

1
2
3
4
5
6
7
8
9
10
11
12
13
  void getPosts() async {
    // レスポンス
    final response = await http.get(Uri.parse(postsUrl));
    // 正常以外は処理終了
    if (response.statusCode != 200) {
      throw Exception('HTTP Error(${response.statusCode})');
    }
    final List<dynamic> posts = jsonDecode(response.body);
    for (var post in posts) {
      var postModel = PostModel.fromJson(post);
      print(postModel);
    }
  }

再開してみるとちゃんと動作していることが確認できる。

DEBUG Console

必要なのは記事のリストなのでリストにPostModelオブジェクトを入れて返却するようにしよう。

また、未来に返却するものなのでFutureで返却する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  Future<List<PostModel>> getPosts() async {
    // レスポンス
    final response = await http.get(Uri.parse(postsUrl));
    // 正常以外は処理終了
    if (response.statusCode != 200) {
      throw Exception('HTTP Error(${response.statusCode})');
    }
    // リストに変換
    final List<dynamic> posts = jsonDecode(response.body);
    // 戻り用のリスト
    final List<PostModel> listOfpostModel = [];
    // 各要素ごとにモデルを作成してリストに入れる
    for (var post in posts) {
      var postModel = PostModel.fromJson(post);
      listOfpostModel.add(postModel);
    }
    return listOfpostModel;
  }

API機能をJsonPlaceeHodelオブジェクトを生成せずに使えるように全てにstaticにしておこう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import 'dart:convert';

import 'package:http/http.dart' as http;
import 'package:simple_http/models/post_model.dart';

class JsonPlaceHolder {
  // 記事リストURL
  static const String postsUrl = 'https://jsonplaceholder.typicode.com/posts';

  static Future<List<PostModel>> getPosts() async {
    // レスポンス
    final response = await http.get(Uri.parse(postsUrl));
    // 正常以外は処理終了
    if (response.statusCode != 200) {
      throw Exception('HTTP Error(${response.statusCode})');
    }
    // リストに変換
    final List<dynamic> posts = jsonDecode(response.body);
    // 戻り用のリスト
    final List<PostModel> listOfpostModel = [];
    // 各要素ごとにモデルを作成してリストに入れる
    for (var post in posts) {
      var postModel = PostModel.fromJson(post);
      listOfpostModel.add(postModel);
    }
    return listOfpostModel;
  }
}

リストを表示してみよう

APIからデータを取得できるようになったのでWidgetにメンバー変数を追加してデータを設定してみよう。

1
Future<List<PostModel>> posts = JsonPlaceHolder.getPosts();

未来に値をもらう変数。できるかな?

これをWidgetのメンバー変数に追加するとエラーが発生する。constになっているから。

constキーワードを削除しておく。

Futureで追加した変数をそのまま使えるのか?ここで役立つのがFutureBuilderである。

Tooltip

必須項目としてはWidgetを返却する関数とFutureタイプのデータだ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class MyApp extends StatelessWidget {
  MyApp({super.key});

  Future<List<PostModel>> posts = JsonPlaceHolder.getPosts();

  @override
  Widget build(BuildContext context) {
    print(posts);
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Colors.deepPurple[50],
        appBar: AppBar(
          elevation: 2,
          backgroundColor: Colors.deepPurple,
          foregroundColor: Colors.white,
          title: const Text(
            'HTTPテスト',
            style: TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
        body: FutureBuilder(
          future: posts,
          builder: (context, snapshot) {
            // snapshotでfutureの場外がわかる
            if (snapshot.hasData) {
              // Futureが完了されている(データが用意されている。)
              return const Text('データダウンロードが完了');
            } else {
              return const Text('ローディング中です。');
            }
          },
        ),
      ),
    );
  }
}

iOS Simulator

コードを修正して実行してみると最初の一瞬”ローディング中です。”の文字列が見える。

ローディングのテキストではなく、グルグルを見せるようにしておこう。

これを

1
return const Text('ローディング中です。');

これに置き換える。

1
2
3
return const Center(
    child: CircularProgressIndicator(),
);

何回かリロードしてみながら、グルグルが動作するか確認しておこう。

まとめ

FutureBuilderを使ってデータを表示できるようになった。StatelessWidgetのままステートを気にせずに。

過去記事に扱ったListView.builderを使って簡単にリストを表示しておいたのでここですべてのコードを載せておく。

main.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import 'package:flutter/material.dart';
import 'package:simple_http/models/post_model.dart';
import 'package:simple_http/services/json_place_holder.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp({super.key});

  final Future<List<PostModel>> posts = JsonPlaceHolder.getPosts();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Colors.deepPurple[50],
        appBar: AppBar(
          elevation: 2,
          backgroundColor: Colors.deepPurple,
          foregroundColor: Colors.white,
          title: const Text(
            'HTTPテスト',
            style: TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
        body: FutureBuilder(
          future: posts,
          builder: (context, snapshot) {
            // snapshotでfutureの場外がわかる
            if (snapshot.hasData) {
              // Futureが完了されている(データが用意されている。)
              return ListView.builder(
                // データがあるときに実行されるのでnullない
                itemCount: snapshot.data!.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    leading: const Icon(Icons.newspaper),
                    title: Text(snapshot.data![index].title),
                  );
                },
              );
            } else {
              return const Center(
                child: CircularProgressIndicator(),
              );
            }
          },
        ),
      ),
    );
  }
}

json_place_holder.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import 'dart:convert';

import 'package:http/http.dart' as http;
import 'package:simple_http/models/post_model.dart';

class JsonPlaceHolder {
  // 記事リストURL
  static const String postsUrl = 'https://jsonplaceholder.typicode.com/posts';

  static Future<List<PostModel>> getPosts() async {
    // レスポンス
    final response = await http.get(Uri.parse(postsUrl));
    // 正常以外は処理終了
    if (response.statusCode != 200) {
      throw Exception('HTTP Error(${response.statusCode})');
    }
    // リストに変換
    final List<dynamic> posts = jsonDecode(response.body);
    // 戻り用のリスト
    final List<PostModel> listOfpostModel = [];
    // 各要素ごとにモデルを作成してリストに入れる
    for (var post in posts) {
      var postModel = PostModel.fromJson(post);
      listOfpostModel.add(postModel);
    }
    return listOfpostModel;
  }
}

post_model.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PostModel {
  final int userId;
  final int id;
  final String title;
  final String body;

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

  PostModel.fromJson(Map<String, dynamic> json)
      : userId = json['userId'],
        id = json['id'],
        title = json['title'],
        body = json['body'];
}

iOS Simulator

最後に

今回の記事は思ったより、時間がかかった。公式ドキュメントをそのまままねるのではなく、自分で作る感覚でやってみたくてトライ・アンド・エラーを繰り返しながら作成した。

その分、自分のものになってきたと思う。

Flutter Basicシリーズに思ったより、時間がかかっているが、そろそろ最終に向かっている。

コメントする