1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ふしぎなるAdvent Calendar 2024

Day 20

[Flutter] 楽天APIでDVD/BD情報を取得する

Last updated at Posted at 2024-12-20

はじめに

Flutter の勉強として、DVD/BD管理アプリ風のものを作っています。

前回は、ローカルDBへの永続化を実施しました。
今回は、楽天APIを使ったDVD/BD情報を取得してみたいと思います。

楽天APIの選定理由

映画情報の取得であれば、TMDbのAPIが使えそうでしたが、DVD/BD管理アプリというコンセプトから、商品検索が可能なAPIを選びました。
Amazon API も後々試してみたいと思いますが、ひとまず今回は楽天APIを使ってみます。

楽天アプリケーションIDの取得

アプリケーションの登録

の「+ New App」からアプリケーションを登録します。

すると、Application IDが発行されます。これをリクエスト時に利用するので控えておきましょう。

image.png

試し撃ち

「楽天ブックス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,
              ),
            ),
          ],
        ),
      );
    });
  }
}

完成!

できました! 以下は完成イメージです。

test4.gif

詳細な変更箇所は PR を参照してください。

終わりに

商品検索ページを作ってみました。
まだ、検索するだけで登録も何もできないので、ブラッシュアップしていきたいと思います。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?