はじめに
Flutter の勉強として、DVD/BD管理アプリ風のものを作っています。
前回は、ローカルDBへの永続化を実施しました。
今回は、楽天APIを使ったDVD/BD情報を取得してみたいと思います。
楽天APIの選定理由
映画情報の取得であれば、TMDbのAPIが使えそうでしたが、DVD/BD管理アプリというコンセプトから、商品検索が可能なAPIを選びました。
Amazon API も後々試してみたいと思いますが、ひとまず今回は楽天APIを使ってみます。
楽天API には 1秒に1回までのリクエスト制限があるため、超えないように注意しましょう
https://webservice.faq.rakuten.net/hc/ja/articles/900001974383-%E5%90%84API%E3%81%AE%E5%88%A9%E7%94%A8%E5%88%B6%E9%99%90%E3%82%92%E6%95%99%E3%81%88%E3%81%A6%E3%81%8F%E3%81%A0%E3%81%95%E3%81%84
Amazon API の方も同様
https://affiliate.amazon.co.jp/help/node/topic/GLL6HEVVWUKMQDDQ
楽天アプリケーションIDの取得
アプリケーションの登録
の「+ New App」からアプリケーションを登録します。
すると、Application IDが発行されます。これをリクエスト時に利用するので控えておきましょう。
試し撃ち
「楽天ブックスDVD/Blu-ray検索API」を使ってみます。
以下のコマンドを実行します。 ([API Key]のところは取得したApplication IDに置き換えてください)
https://app.rakuten.co.jp/services/api/BooksDVD/Search/20170404?applicationId=[API Key]&title=%E3%83%9D%E3%83%83%E3%82%BF%E3%83%BC&sort=%2BitemPrice
実行結果
{"GenreInformation":[],"Items":[{"Item":{"affiliateUrl":"","artistName":"クリス・コロンバス/ダニエル・ラドクリフ/ルパート・グリント/エマ・ワトソン","artistNameKana":"コロンバス クリス/ラドクリフ ダニエル/グリント ルパート/ワトソン エマ","availability":"4","booksGenreId":"003201003","discountPrice":0,"discountRate":0,"itemCaption":"","itemPrice":983,"itemUrl":"https://books.rakuten.co.jp/rb/4044773/?rafcid=wsc_b_ds_1073712914928508395","jan":"4988135577450","label":"ワーナーエンターテイメントジャパン(株)","largeImageUrl":"https://thumbnail.image.rakuten.co.jp/@0_mall/book/cabinet/7450/4988135577450.jpg?_ex=200x200","limitedFlag":0,"listPrice":0,"makerCode":"NFPA-74953","mediumImageUrl":"https://thumbnail.image.rakuten.co.jp/@0_mall/book/cabinet/7450/4988135577450.jpg?_ex=120x120","postageFlag":2,"reviewAverage":"5.0","reviewCount":1,"salesDate":"2006年07月21日","smallImageUrl":"https://thumbnail.image.rakuten.co.jp/@0_mall/book/cabinet/7450/4988135577450.jpg?_ex=64x64","title":"ハリー・ポッターと秘密の部屋【UMD】","titleKana":"ハリーポッタートヒミツノヘヤ"}}...
無事に取れているようです。
Flutter アプリで利用
依存関係の追加
以下を実行します。
flutter pub add http
HTTPリクエストの許可 (macOS)
macOS でネットワークリクエストを許可するための設定をします。
上記ドキュメントに従い、
./macos/Runner/DebugProfile.entitlements
に以下を追加。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
+ <key>com.apple.security.network.client</key>
+ <true/>
</dict>
</plist>
楽天APIサービスの追加
楽天APIにリクエストするためのサービスを作成します。
apiKey
には最初に取得したアプリケーションIDを入力してください。
本対応は暫定的なもので、本来はキー情報をソースに入れるべきではありません
// rakuten_api.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
class RakutenAPI {
static const String apiKey = '[API Key]'; // 取得したAPIキーをここに入れる
static const String endpoint =
'https://app.rakuten.co.jp/services/api/BooksDVD/Search/20170404';
static Future<List<dynamic>> searchItems(String query) async {
// URLエンコード
final url = Uri.parse(
'$endpoint?format=json&formatVersion=2&title=$query&applicationId=$apiKey&sort=-releaseDate');
final response = await http.get(url);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return data['Items'] ?? [];
} else {
throw Exception('Failed to load data from Rakuten API');
}
}
}
タイトルで検索し、リリース日降順で取得するようにしています。
パラメータの詳細、その他指定可能なパラメータは以下を参照してください。
検索用のページを作成
検索ボックスからタイトルで検索して、結果リストを表示する単純なページを作成します。
// ./main.dart
// ...
import 'package:media_manager_app/services/rakuten_api.dart';
// ...
class SearchPage extends StatefulWidget {
@override
State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
// 検索入力フィールドのコントローラー
final _searchController = TextEditingController();
List<dynamic> _searchResults = [];
bool _isLoading = false;
/// 楽天APIを使って検索を実行する関数.
/// 検索中は _isLoading に true を設定する
/// 検索結果を _searchResults に格納する
void _performSearch() async {
// 検索開始時にローディング状態をtrueに設定
setState(() {
_isLoading = true;
});
try {
// 楽天APIを呼び出して検索結果を取得
final results = await RakutenAPI.searchItems(_searchController.text);
// 検索結果を状態に反映
setState(() {
_searchResults = results;
});
} catch (e) {
// エラーが発生した場合にエラーメッセージを出力
print('Error: $e');
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Search DVDs/Blu-rays'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// 検索入力フィールド
TextField(
controller: _searchController,
decoration: InputDecoration(
labelText: 'Search',
suffixIcon: IconButton(
icon: Icon(Icons.search),
onPressed: _performSearch,
),
),
),
// ローディング中はプログレスインジケーターを表示
if (_isLoading)
Center(child: CircularProgressIndicator())
else
// ローディング中以外は検索結果を表示
Expanded(
child: ListView.builder(
itemCount: _searchResults.length,
itemBuilder: (context, index) {
final item = _searchResults[index];
return ListTile(
title: Text(item['title']), //< 作品タイトル
subtitle: Text(item['salesDate']), //< 販売日
leading: Image.network(item['mediumImageUrl']), //< 商品画像
);
},
),
),
],
),
),
);
}
}
検索ページの組み込み
で作ったアプリに新規ページを追加してみます。
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = MyMediaPage();
break;
+ case 2:
+ page = SearchPage();
+ break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return LayoutBuilder(builder: (context, constraints) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: constraints.maxWidth >= 600,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('My Media'),
),
+ NavigationRailDestination(
+ icon: Icon(Icons.search),
+ label: Text('Search'),
+ ),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page,
),
),
],
),
);
});
}
}
完成!
できました! 以下は完成イメージです。
詳細な変更箇所は PR を参照してください。
終わりに
商品検索ページを作ってみました。
まだ、検索するだけで登録も何もできないので、ブラッシュアップしていきたいと思います。