FlutterでダミーのAPI(JSONPlaceholder)から取得する 2021.08.31
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);
得られたList
をmap
メソッドでRandomUser
クラスに変換してRandomUser
型のIterable
を得る。
final Iterable<RandomUser> randomUserIterable =
jsonDecoded.map((dynamic value) => RandomUser.fromJson(value));
Iterable
はdartではList
やMap
を実装するための抽象とのこと。
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を表示できた。