Counts the Clouds

FlutterでダミーのAPI(JSONPlaceholder)から取得する
2021.08.31

Dart
Flutter
JSONPlaceholder

john-moeses-bauan-7XUbQhWjJX0-unsplash.jpg

Flutterでダミーのレスポンスをもとに画面を構成したいと思い、そのまんまな名前のDummyAPIというサービスを見つけた。

GoogleアカウントかGitHubアカウントがあればすぐに使える。

秘匿情報があると公開が面倒なので、JSONPlaceholderを使うことにした。

DartPadではhttpパッケージが未対応で、他に動作するサービスもなかったため、Gistに全容を置いておく。

httpパッケージをインストール

公式のクックブックをもとにしてダミーのレスポンスを読み込めるようにしていく。

httpパッケージを追加。

dependencies:
  http: ^0.13.3

インポートしておく。

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

モデル定義

JSONPlaceholderのusersエンドポイントを使う。

curl https://jsonplaceholder.typicode.com/users

以下のようなレスポンスが返ってくるので、受け止めるためのモデルを作成する。

{
  "data": [
    {
      "id": 1,
      "name": "Leanne Graham",
      "username": "Bret",
      "email": "Sincere@april.biz",
      "address": {
        // ...
      },
      "phone": "1-770-736-8031 x56442",
      "website": "hildegard.org",
      "company": {
        // ...
      }
    },
    // ...
  ]
}

RandomUserクラスを定義。今回は簡単のため浅いメンバーだけにした。

class RandomUser {
  final int id;
  final String name;
  final String username;
  final String email;
  final String phone;

  RandomUser({
    required this.id,
    required this.name,
    required this.username,
    required this.email,
    required this.phone,
  });

  factory RandomUser.fromJson(Map<String, dynamic> json) {
    return RandomUser(
      id: json['id'],
      name: json['name'],
      username: json['username'],
      email: json['email'],
      phone: json['phone'],
    );
  }
}

レスポンスを取得する関数を定義

非同期関数のためにdart:asyncを、jsonDecodeのためにdart:convertを追加。

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

最終的な実装は以下。

Future<List<RandomUser>> fetchRandomUser() async {
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
  if (response.statusCode == 200) {
    return jsonDecode(response.body)
        .map((dynamic value) => RandomUser.fromJson(value))
        .cast<RandomUser>()
        .toList();
  } else {
    throw Exception('Failed to load album');
  }
}

Unhandled Exception: type ‘XXX’ is not a subtype of type ‘XXX’

サンプルと違ってListを取得するため、Unhandled Exception: type 'XXX' is not a subtype of type 'XXX'が頻出して少々難儀した。

問題はレスポンスの受信に成功したあと、リストを返す部分。分解すると以下のようになる。

jsonDecode関数で、dynamic型のListを得る。

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

得られたListmapメソッドでRandomUserクラスに変換してRandomUser型のIterableを得る。

final Iterable<RandomUser> randomUserIterable =
    jsonDecoded.map((dynamic value) => RandomUser.fromJson(value));

IterableはdartではListMapを実装するための抽象とのこと。

Dartでは、Iterableクラスは抽象クラス(abstract class)です。抽象クラスはインスタンス化できませんので、Iterableクラスを直接インスタンス化することはできません。

Listとの違いは、Iterableでは、インデックスによる要素の読み込みが効率的であることが保証されないことです。Listとは対照的に、Iterableには[]演算子がありません。

そのまま使うものではなさそうなので、Listにして返す。

return randomUserIterable.toList();

これらを単純にメソッドチェーンにしてうまくいくかというとそう簡単でもなく。

return jsonDecode(response.body)
    .map((value) => RandomUser.fromJson(value))
    .toList();
[VERBOSE-2:ui_dart_state.cc(209)] Unhandled Exception: type 'List<dynamic>' is not a subtype of type 'FutureOr<List<RandomUser>>'
#0      fetchRandomUser (main.dart:56:10)

わからないが、上のように分解した時と違ってList<dynamic>のまま型変換(型推論?)されていないのかな?

強制的にRandomUser型にcastする。

return jsonDecode(response.body)
    .map((value) => RandomUser.fromJson(value))
    .cast<RandomUser>()
    .toList();

JavaScriptではなんとなくさっくり書けていた処理が、Dartのように型があると難解になる。

FutureBuilderでビューにデータを受け渡す

FutureBuilderで非同期データの取得に反応させる。

class _MyHomePageState extends State<MyHomePage> {
  late Future<List<RandomUser>> futureRandomUser;

  @override
  void initState() {
    super.initState();
    futureRandomUser = fetchRandomUser();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            FutureBuilder<List<RandomUser>>(
              future: futureRandomUser,
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  return Text(snapshot.data!.length.toString()); // FIXME: とりあえず件数を表示してみた
                } else if (snapshot.hasError) {
                  return Text('${snapshot.error}');
                }
                return const CircularProgressIndicator();
              },
            ),
          ],
        ),
      ),
    );
  }
}

FutureBuilderが動作していることがわかったので、実際にリストを表示する。

リスト表示部分を切り出し。

class _RandomUserList extends StatelessWidget {
  _RandomUserList({Key? key, required this.userList}) : super(key: key);

  List<RandomUser> userList = [];

  @override
  Widget build(BuildContext context) {
    return Text('${userList.length}');
  }
}

切り出したウィジェットを使うようにする。

-                 return Text(snapshot.data!.length.toString()); // FIXME: とりあえず件数を表示する
+                 return _RandomUserList(userList: snapshot.data!);

切り出したウィジェットにListView、ListTileを追加

class _RandomUserList extends StatelessWidget {
  _RandomUserList({Key? key, required this.userList}) : super(key: key);

  List<RandomUser> userList = [];

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: ListView.builder(
        itemCount: userList.length,
        itemBuilder: (context, index) {
          final user = userList[index];
          return ListTile(
            title: Text(user.username),
            subtitle: Text(user.name),
            leading: CircleAvatar(
              backgroundColor: Colors.blue,
              child: Text(initial.substring(0, 2)),
            ),
            onTap: () => {},
            onLongPress: () => {},
            trailing: const Icon(Icons.more_vert),
          );
        },
      ),
    );
  }
}

Vertical viewport was given unbounded height.

レイアウトに失敗。Columnの中にListViewがあるパターンなので、ListViewをExpandedで囲った。

JSONPlaceholderのレスポンスからListViewを表示できた。

1-start