🍣

【Flutter】楽天ブックスAPIで書籍検索アプリを作る

2023/06/18に公開

今回は楽天ブックスAPIを使って書籍の検索アプリを作ってみようと思います。

目標

以下の要件を実現することを目標として進めます。
・楽天ブックス系APIを叩いて書籍の情報を取得する
→書影、タイトル、著者、出版社、発売日を一覧形式で画面に表示
・画面上部に検索キーワードの入力フォームを作る
・検索ボタンを押すと、入力された文字列を使って検索結果の一覧を表示させる

APIを使うための準備

アプリを作り始める前に楽天APIを使えるようにする準備をしましょう。
リンク:https://webservice.rakuten.co.jp/
私はこちらの方などのサイトを参考にしました。Application IDに関しては十分に気をつけて管理する必要があります。

パッケージをインストール

今回使用するパッケージは以下の通りです。

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.4  #httpパッケージのバージョンを指定

ソースコード

アプリのエントリーポイントやroot widgetを定義し、書籍検索画面を含むFlutterアプリケーションの基本的な構造となる部分のコードを作ります。home プロパティにBookListScreen クラスのインスタンスを指定していますが、これは画面として書籍リスト画面を表示させるためのものです。

main.dart

import 'package:flutter/material.dart';
import 'book_list_screen.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Book Search',
      theme: ThemeData(
        primarySwatch: Colors.red,
      ),
      home: BookListScreen(),
    );
  }
}

book_list_screen.dart

それでは次に、main.dartで記述したBookListScreen クラスを別ファイルに作っていきます。今回はこのファイルをmain.dartと同じディレクトリに置くことにします。
表示させる本の数ですが、楽天ブックスのページを見ていただくとわかるように1ページあたり最大で30冊までなので、このアプリでも同様に最大で30冊まで表示させる仕様になります。

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'bookinfo.dart';

class BookListScreen extends StatefulWidget {
  @override
  _BookListScreenState createState() => _BookListScreenState();
}

class _BookListScreenState extends State<BookListScreen> {
  final TextEditingController _searchController = TextEditingController();//キーワード入力用に_searchController を作成
  List<Book> books = [];//書籍情報を保持するためのリスト

  @override
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

  Future<void> fetchBooks(String keyword) async {//書籍を検索するためのメソッド
    final appId = 'hoge';//ここにApplication IDを書く
    final apiUrl = 'https://app.rakuten.co.jp/services/api/BooksBook/Search/20170404';

    final url = Uri.parse('$apiUrl?applicationId=$appId&title=${Uri.encodeQueryComponent(keyword)}');

    final response = await http.get(url);

    if (response.statusCode == 200) {
      final data = json.decode(response.body);
      List<Book> fetchedBooks = [];

      for (var bookData in data['Items']) {
        String imageUrl = bookData['Item']['largeImageUrl'];
        String title = bookData['Item']['title'];
        String author = bookData['Item']['author'];
        String publisher = bookData['Item']['publisherName'];
        String pubdate = bookData['Item']['salesDate'];

        fetchedBooks.add(Book(
          imageUrl: imageUrl,
          title: title,
          author: author,
          publisher: publisher,
          pubdate: pubdate,
        ));
      }

      setState(() {
        books = fetchedBooks;
      });
    }
  }

  void handleSearch() {//検索ボタンが押されたときの処理
    String keyword = _searchController.text.trim();
    if (keyword.isNotEmpty) {
      fetchBooks(keyword);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('書籍検索'),
      ),
      body: Column(
        children: [
          Container(
            decoration: BoxDecoration(
              border: Border.all(color: Colors.black),
              borderRadius: BorderRadius.circular(8.0),
            ),
            margin: EdgeInsets.all(16.0),
            padding: EdgeInsets.symmetric(horizontal: 16.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _searchController,
                    decoration: InputDecoration(
                      labelText: 'キーワード検索',
                      border: InputBorder.none,
                    ),
                  ),
                ),
                SizedBox(width: 16.0),
                ElevatedButton(
                  onPressed: handleSearch,
                  child: Text('検索'),
                ),
              ],
            ),
          ),
          Expanded(
            child: ListView.separated(
              itemCount: books.length,
              separatorBuilder: (BuildContext context, int index) {
                return Divider(
                  thickness: 1.0,
                  color: Colors.grey,
                );
              },
              itemBuilder: (BuildContext context, int index) {
                return ListTile(
                  leading: Image.network(books[index].imageUrl),
                  title: Text(books[index].title),
                  subtitle: Text(
                    '著者: ${books[index].author}\n出版社: ${books[index].publisher}\n発売日: ${books[index].pubdate}',
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

bookinfo.dart

最後に、書籍情報を表すBookクラスを定義していきます。

class Book {
  final String imageUrl;//書影
  final String title;//タイトル
  final String author;//著者
  final String publisher;//出版社
  final String pubdate;//発売日

  Book({
    required this.imageUrl,
    required this.title,
    required this.author,
    required this.publisher,
    required this.pubdate,
  });
}

完成イメージ(スクショ)

線型代数と入力して検索した場合を見てみましょう。
画面を上下にスクロールすると30冊まで表示されていることが確認できるかと思います。

最後までご覧いただきありがとうございました。

Discussion