初めに
FlutterのサーバーサイドはFirebaseばかり触っていたのですが、
普通にAPI連携もやっとかないと!と思い備忘録として記事を書きました。
今回は、Google Books APIを用いて簡易的な書籍リストと一覧表示をします。
また、APIのレスポンスの中にHTMLが含まれているため、Flutterで簡易的にhtmlタグを表示する内容について記述していきます。
使用パッケージ
- go_router: ^3.1.1
- url_launcher: ^6.1.2
- flutter_html: ^2.2.1
- http: ^0.13.4
最終ゴール


まずは全体のコード
サクッとコードを見ていきましょう。
主要部分の解説は、コードの下に記述していきます。
import 'package:flutter/material.dart';
import 'package:flutter_rest_api/detail_page.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key);
@override
// GoRouterの設定
Widget build(BuildContext context) => MaterialApp.router(
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
);
// ルーティングの設定
final GoRouter _router = GoRouter(
routes: <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const MyHomePage(title: 'Flutter REST API'),
),
GoRoute(
path: '/book/:id', // 本のIDを引き渡す
builder: (context, state) {
// パスパラメータの値を取得するには state.params を使用
final String id = state.params['id']!;
return BookDetailPage(id: id);
},
),
],
initialLocation: '/',
);
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// APIのレスポンスを格納する
List items = [];
// APIリクエスト
Future<void> getData() async {
var response = await http.get(Uri.https(
'www.googleapis.com', // ベースURL
'/books/v1/volumes', // パス
{'q': '{Flutter}', 'maxResults': '35', 'langRestrict': 'ja'})); // パラメーター
var jsonResponse = jsonDecode(response.body);
setState(() {
items = jsonResponse['items'];
});
}
@override
void initState() {
super.initState();
getData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter REST API'),
),
// データのリストを簡易的に表示
body: ListView.builder(
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
return Card(
child: Column(
children: <Widget>[
items[index] != null
? GestureDetector(
child: ListTile(
// サムネ(書籍画像)
leading: Image.network(
items[index]['volumeInfo']['imageLinks']
['thumbnail'] ??
'',
),
// 書籍タイトル
title:
Text(items[index]['volumeInfo']['title'] ?? ''),
// 発行年月日
subtitle: Text(items[index]['volumeInfo']
['publishedDate'] ??
''),
),
// IDを詳細画面に引き継ぐ
onTap: () {
// 詳細画面へ遷移
context.go('/book/${items[index]['id']}');
},
)
: const SizedBox(),
],
),
);
},
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_rest_api/debug_function.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:url_launcher/url_launcher.dart';
class BookDetailPage extends StatefulWidget {
const BookDetailPage({Key? key, required this.id}) : super(key: key);
final String id;
@override
State<BookDetailPage> createState() => _BookDetailPageState();
}
class _BookDetailPageState extends State<BookDetailPage> {
// APIのレスポンスを格納する
late Map<String, dynamic> bookInfo;
late Map<String, dynamic> saleInfo;
bool isFetching = true;
Future<void> getData() async {
var response = await http.get(Uri.https(
'www.googleapis.com',
'/books/v1/volumes/${widget.id}'));
var jsonResponse = jsonDecode(response.body);
setState(() {
bookInfo = Map<String,dynamic>.from(jsonResponse['volumeInfo']);
saleInfo = Map<String,dynamic>.from(jsonResponse['saleInfo']);
isFetching = false;
});
}
// 外部リンクへの遷移
void _launchURL(String linkText) async {
var url = Uri.parse('https://developer.android.com/?hl=ja');
// 正しいURLかチェックする
if (await canLaunchUrl(url)) {
// URLを開く
await launchUrl(url);
} else {
throw 'Could not launch $url';
}
}
String _authorsText(List authors){
String text = '';
int index = 1;
for(final author in authors){
text += author;
if(index != authors.length){
text += '/';
}
index ++;
}
return text;
}
@override
void initState() {
getData();
super.initState();
}
@override
Widget build(BuildContext context) {
return isFetching ? Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/'),
),
title: const Text('Loading...'),
),
body: const Center(child: CircularProgressIndicator()),
) : Scaffold(
appBar: AppBar(
title: const Text('書籍詳細'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/'),
),
),
body: ListView(
children: <Widget>[
const SizedBox(height: 15),
Center(
child: Text(bookInfo['title'], style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w400,
),maxLines: 3,),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: SizedBox(
height: 160,
child: GestureDetector(
// child: Container(color: Colors.red, width: 30, height: 30),
child: Image.network(bookInfo['imageLinks']['small']),
onTap: () {
_launchURL(saleInfo['buyLink']);
}
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('販売価格'),
const SizedBox(width: 20),
Text('¥ ${saleInfo['listPrice']['amount'].toString()}'),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('著者'),
const SizedBox(width: 20),
Text(_authorsText(bookInfo['authors'])),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('出版社'),
const SizedBox(width: 20),
Text(bookInfo['publisher']),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Text('書籍概要', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
),
Html(data: bookInfo['description']),
],
),
),
],
),
);
}
}
主要部分の解説
APIリクエスト
APIのリクエストをするには、httpパッケージを用いることで簡単にREST APIの処理が実装できます。作成段階ではまだ0系なものの、Likesの多さやPopularityを見たら問題はないかなと思います。
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> getData() async {
// GETリクエスト
var response = await http.get(Uri.https(
'www.googleapis.com', // ベースURL
'/books/v1/volumes', // パス
{'q': '{Flutter}', 'maxResults': '35', 'langRestrict': 'ja'})); // パラメータ
var jsonResponse = jsonDecode(response.body);
setState(() {
items = jsonResponse['items']; // 配列に格納
});
}
外部ページへ遷移する
外部のページをブラウザで開きたい(htmlのaタグ的なことがしたい)場合には、url_launcherパッケージがおすすめです。
import 'package:url_launcher/url_launcher.dart';
void _launchURL(String linkText) async {
var url = Uri.parse(linkText);
// 正しいURLかチェックする
if (await canLaunchUrl(url)) {
// URLを開く
await launchUrl(url);
} else {
throw 'Could not launch $url';
}
}
HTMLを描画する
Flutterの内部構造を深く知っているわけではないですが、HTMLの描画をするには一手間必要です。
今回はすごく簡易的で良かったため、flutter_htmlパッケージを利用してAPIから返ってくるHTMLを描画することができました。
import 'package:flutter_html/flutter_html.dart';
// Widget的な使い方です
Html(data: bookInfo['description']),
この書籍概要の部分は、Columnウィジェットなどは使っておらず、全てHTMLを変換したものです。
以上です。
flutter_htmlやhttpパッケージは、もっといろいろなことができます。
今回は簡易的な紹介でしたが、ぜひ興味を持った方は活用してみてください。