DartPadがパッケージに対応したのでパッケージ「http」を利用してAPIを呼んでみました。
UIでAPIのリクエストパラメータを入力 → APIにリクエスト → UIに結果を表示
までやってみました。DartPadでここまで出来ると、Visual Studio Codeなどでアプリ開発をする前に結構活用できそうです!対応パッケージが増えるといいですね。
DartPadで利用できるパッケージ
DartPadの画面右下にあるiアイコンをクリックすると利用できるパッケージが表示されます。
2021/12/12時点では「Directly importable packages」「Packages available transitively」の両方の合計で67種類に対応していました。
DartPadでパッケージ「http」を利用してAPIにリクエストする
Flutterで開発を計画しているアプリでAPIを呼びたいのでhttpパッケージについて学びたいと考えていました。ネットで情報を探していたところ、以下のサイトで紹介されているソースコードを活用させていただいて、DartPadでGoogle Book APIを呼んでみることにしました。
[Flutter]API経由でJSONを取得、パースする方法
https://zenn.dev/r0227n/articles/8ef8ad78265d6a
Google Book APIのデータを表示するDartPadはこちらになります!
「Run」をクリックするとFlutterのUIが表示されます。
テキストボックスに検索したいワードを入力して、ボタンをクリックするとGoogle Book APIのレスポンス結果の一部がテーブル形式で掲載されます。
ソースの解説
ソースコードを直接ブログにも記載しました。
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(ApiCall());
}
class ApiCall extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ApiCallState();
}
}
class _ApiCallState extends State<ApiCall> {
//テーブルに表示するデータを格納するリスト
List<Map> dataList = [];
//検索ワードを入力するテキストボックスのコントローラ
TextEditingController myController = TextEditingController(text: '越尾圭');
@override
Widget build(BuildContext context) {
return MaterialApp(
//ダークモード
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
MyWidget(),
const Text('検索したいワードを入力してください'),
//検索ワード入力欄
TextField(
enabled: true,
//入力数
maxLength: 50,
obscureText: false,
maxLines: 1,
//コントローラの設定
controller: myController, //(A)
),
//検索を実行するボタン
ElevatedButton(
//fetchBookメソッドを呼んでGoogle Book APIにリクエスト
onPressed: () => fetchBook(myController.text) //(B)
//レスポンスを_apiInfoメソッドで処理
.then((value) => _apiInfo(value)), //(I)
child: const Text(
'本を検索する',
)),
//テーブルを記載する
FittedBox(
child: DataTable(
//ヘッダー設定。5列のテーブル
columns: const <DataColumn>[
DataColumn(
label: Text('title'),
),
DataColumn(
label: Text('authors'),
),
DataColumn(
label: Text('publisher'),
),
DataColumn(
label: Text('publishedDate'),
),
DataColumn(
label: Text('pageCount'),
),
],
//データはcreateRowListメソッドで設定
rows: createRowList(),
))
]))));
}
//Google Book APIにリクエストする
Future<Book> fetchBook(String query) async {
//(C)
String url = 'https://www.googleapis.com/books/v1/volumes?q=' + query;
url = Uri.encodeFull(url); //(D)
final response = await http.get(Uri.parse(url)); //(E)
if (response.statusCode == 200) { //(F)
return Book.fromJson(jsonDecode(response.body));
} else { //(G)
throw Exception('Failed to load album');
}
}
//Google Book APIのレスポンスを処理する
void _apiInfo(Book data) {
/// "items"の要素を全て表示
dataList = [];
Map dm;
for (var i = 0; i < data.items.length; i++) { //(J)
dm = {};
//print(data.items[i]);
dm['title'] = data.items[i]['volumeInfo']['title'] ?? '';
dm['authors'] = data.items[i]['volumeInfo']['authors'].join(',');
dm['publisher'] = data.items[i]['volumeInfo']['publisher'] ?? '';
dm['publishedDate'] = data.items[i]['volumeInfo']['publishedDate'] ?? '';
dm['pageCount'] =
data.items[i]['volumeInfo']['pageCount'].toString() == 'null'
? ''
: data.items[i]['volumeInfo']['pageCount'].toString();
dataList.add(dm);
}
/// "items"の要素0のidを表示
//print(data.items[0]['id']);
//画面を更新する
setState(() { //(K)
rebuildAllChildren(context);
});
}
//テーブルにデータを設定する
List<DataRow> createRowList() {
List<DataRow> dataRowList = [];
List<DataCell> dataCellList;
if (dataList.isNotEmpty) {
for (Map m in dataList) {
dataCellList = [];
m.forEach((key, value) {
dataCellList.add(DataCell(Text(value)));
});
dataRowList.add(DataRow(cells: [...dataCellList]));
}
}
return dataRowList;
}
//画面を更新する
void rebuildAllChildren(BuildContext context) {
void rebuild(Element el) {
el.markNeedsBuild();
el.visitChildren(rebuild);
}
(context as Element).visitChildren(rebuild);
}
}
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(
'本の検索',
style: Theme.of(context).textTheme.headline4,
);
}
}
//Google Book APIの結果を格納するクラス
class Book {
final int totalItems;
final List items;
Book({
required this.totalItems,
required this.items,
});
factory Book.fromJson(Map<String, dynamic> json) {
return Book(
totalItems: json['totalItems'],
items: json['items'],
);
}
}
このソースは以下の4種類を実施しています。
- UIで検索ワードを入力してAPIのリクエストパラメータに設定
- APIにリクエスト
- APIのレスポンスを加工
- UIを再描画
UIで検索ワードを入力してAPIのリクエストパラメータに設定
UIで検索ワードを入力するのにTextFieldを利用します。
//検索ワード入力欄
TextField(
enabled: true,
//入力数
maxLength: 50,
obscureText: false,
maxLines: 1,
//コントローラの設定
controller: myController, //(A)
),
Aの箇所でコントローラ(このソースではmyController)を設定しています。コントローラによってテキストボックスで入力された文字列を取得することができます。myController.textで取得できるので、「本を検索する」ボタンをクリック時に文字列を取得してAPIのレスポンスに利用します。(Bの箇所)
//検索を実行するボタン
ElevatedButton(
//fetchBookメソッドを呼んでGoogle Book APIにリクエスト
onPressed: () => fetchBook(myController.text) //(B)
//レスポンスを_apiInfoメソッドで処理
.then((value) => _apiInfo(value)), //(I)
child: const Text(
'本を検索する',
)),
APIにリクエスト
APIにリクエストするメソッドfetchBookを用意しています。
//Google Book APIにリクエストする
Future<Book> fetchBook(String query) async {
//(C)
String url = 'https://www.googleapis.com/books/v1/volumes?q=' + query;
url = Uri.encodeFull(url); //(D)
final response = await http.get(Uri.parse(url)); //(E)
if (response.statusCode == 200) { //(F)
return Book.fromJson(jsonDecode(response.body));
} else { //(G)
throw Exception('Failed to load album');
}
}
テキストボックスで入力された文字列をqueryとして受け取っていて、Google Books APIへのリクエストURLに設定しています。(Cの箇所)
また日本語が含まれているのでUriクラスのencodeFullメソッドでURLエンコードしています。(Dの箇所)
以上でAPIリクエストの準備が済んだのでEでAPIにリクエストします。awaitによってレスポンスが返ってくるまで待ちます。またJSON形式でデータを取得します。
レスポンスを取得後、レスポンスの種類によって処理を変えています。
レスポンスが正常な場合(Eの箇所)、Bookクラスに用意したfromJsonメソッドでレスポンスを加工します。レスポンスが異常な場合(Fの箇所)、例外を投げて処理を終了しています。
APIのレスポンスを加工
APIのレスポンスを格納するためにBookクラスを用意しています。
//Google Book APIの結果を格納するクラス
class Book {
final int totalItems;
final List items;
Book({
required this.totalItems,
required this.items,
});
factory Book.fromJson(Map<String, dynamic> json) { //(H)
return Book(
totalItems: json['totalItems'],
items: json['items'],
);
}
}
APIのレスポンスはJSON形式ですが、HのfromJSONメソッドで加工しつつ、Bookオブジェクトを返します。
FlutterではJSON形式のデータを簡単にMapオブジェクトに変換することができます。以下のQiitaの記事が分かりやすいです。
【Flutter】JSONをデコードする
UIを再描画
このソースではAPIのレスポンス結果をテーブル形式で掲載しています。
初期表示ではテーブルのヘッダーのみ表示されている状態ですが、APIのレスポンスを無事取得できたらテーブルのセルにデータを設定してUIを再描画する必要があります。
「本を検索する」クリック
→ Google Books APIにリクエスト:(B) fetchBook(myController.text)
→ レスポンスを加工:(H) Book.fromJson( )
→ UIを再描画
最後のUIを再描画は_apiInfoメソッドで実施しています。_apiInfoメソッドの呼び出し元は以下のIの箇所です。B、Hの処理が終わった後に、加工したデータを利用して呼ばれます。
//検索を実行するボタン
ElevatedButton(
//fetchBookメソッドを呼んでGoogle Book APIにリクエスト
onPressed: () => fetchBook(myController.text) //(B)
//レスポンスを_apiInfoメソッドで処理
.then((value) => _apiInfo(value)), //(I)
child: const Text(
'本を検索する',
)),
_apiInfoメソッドは以下のようになっています。
Jでテーブルのセルのデータを用意して、Kの処理でUIを再描画しています。
setStateメソッドはFlutterでUI画面を作成するのに利用するStatefulWidgetクラスのメソッドです。StatefulWidgetクラスはステータスを持っていて、setStateメソッドを呼ぶことでステータスを更新することができます。さらにrebuildAllChildrenメソッドを呼ぶことでUIを再描画できます。
//Google Book APIのレスポンスを処理する
void _apiInfo(Book data) {
/// "items"の要素を全て表示
dataList = [];
Map dm;
for (var i = 0; i < data.items.length; i++) { //(J)
dm = {};
//print(data.items[i]);
dm['title'] = data.items[i]['volumeInfo']['title'] ?? '';
dm['authors'] = data.items[i]['volumeInfo']['authors'].join(',');
dm['publisher'] = data.items[i]['volumeInfo']['publisher'] ?? '';
dm['publishedDate'] = data.items[i]['volumeInfo']['publishedDate'] ?? '';
dm['pageCount'] =
data.items[i]['volumeInfo']['pageCount'].toString() == 'null'
? ''
: data.items[i]['volumeInfo']['pageCount'].toString();
dataList.add(dm);
}
/// "items"の要素0のidを表示
//print(data.items[0]['id']);
//画面を更新する
setState(() { //(K)
rebuildAllChildren(context);
});
}