[Flutter]2022年に作ってきた開発物の紹介！

# はじめに
こんにちは！ヨシカワです。この記事では私が2022年12月現在、モバイルアプリ開発フレームワークのFlutterで開発中のアプリを紹介します。
また、この記事は「[Qiita x エンジニアカフェ共催！今年がんばった技術（と好きなラーメンと麺のかたさ） Advent Calendar 2022](https://qiita.com/advent-calendar/2022/qiita-engineercafe)」に投稿しています。この記事以外にもAdvent Calendarには様々な記事が投稿されていますので是非チェックしてみてください！

## 目次
[Flutterとは](#flutterとは)
[シンプルなメモアプリ「Memogement」](#シンプルなメモアプリmemogement)
[卓球向けのスコアボードアプリ「スコアボードfor卓球」](#卓球向けのスコアボードアプリスコアボードfor卓球)
[蔵書管理アプリ（開発中）](#蔵書管理アプリ開発中)

# Flutterとは？
Flutter公式サイトでは以下のように記述されています。
>Flutter is an open source framework by Google for building beautiful, natively compiled, multi-platform applications from a single codebase.

えー....deeplさんに翻訳してもらいます...。

>Flutterは、単一のコードベースから美しいネイティブコンパイルされたマルチプラットフォームアプリケーションを構築するためのGoogleによるオープンソースフレームワークです。

つまり、FlutterはGoogleによって開発された、マルチプラットフォームのアプリケーションを構築するためのオープンソースフレームワークのことです。マルチプラットフォームというのは別名クロスプラットフォームとも呼ばれていて、あるプログラムを異なる仕様の機種やOSで同じように動作させることを指します。

# シンプルなメモアプリ「Memogement」
2022年に作ったアプリの第１作目です！
最初に作るアプリはシンプルなメモアプリを作ろうと思いました。その名もMemo(メモ)+Manegement(マネジメント)でMemogement(メモジメント)です。
![メモアプリ　フィーチャーグラフィック　完成版._page-0001 (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1877808/e6e0c975-0c97-50dc-0eb0-70e2e868dfbb.jpeg)

メモをリスト状に表示するアプリです。
何か自分でオリジナルのメモ帳を持ってみたい！という私自身の願望がありこのアプリを開発するに至りました。
まだ、リリースできていませんが、いずれはリリースしてアップデートもしていこうと思っています。


# 卓球向けのスコアボードアプリ「スコアボードfor卓球」
２作目は卓球向けのスコアボードアプリ「スコアボードfor卓球」です。
![卓球スコアボード　フィーチャーグラフィック_page-0001 (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1877808/062c0b45-7b88-aa98-06ca-af79f92561c0.jpeg)

このアプリのこだわりポイントとしてボタンをシンプルにし、直感的に操作できるようにしましたね。本格的な競技用ではなく仲間とセット数気にせずたくさん遊びたい方をターゲットに開発しました。
これもMemogement同様にリリース予定です。


# 蔵書管理アプリ（開発中）
今回のメインですね。
まず、開発の背景として、私が所属しているサークルの本棚には約100冊以上の技術書があります。その本の中から借りて読みたい本があった場合、いつその本を借りて、いつ返却したのかを記録する貸出表が紙媒体で設置してあります。「これをアプリにして蔵書を確認出来たり、貸出と返却の手続きが出来たら便利だろうな〜」と思って開発し始めました。

実装したい機能は以下の5つです。

- 書籍の一覧表示
- 貸出機能
- 返却機能
- 登録機能
- 検索機能

今年はこれらの中から「登録機能」と「書籍の一覧表示」を実装することができました。
しかし、今回は時間とこの記事の長さの都合上、登録機能のみの紹介とさせていただきます。

## 登録機能
### 使用したデータベース・API・パッケージとプラグイン
<dl>
  <dt>データベース</dt>
  <dd>Cloud FireStore</dd>
  <dt>API</dt>
  <dd>OpenBD</dd>
  <dt>パッケージとプラグイン</dt>
  <dd>http ver.0.13.5</dd>
  <dd>cloud_firestore ver.4.1.0</dd>
  <dd>flutter_barcode_scanner ver.2.0.0</dd>
  <dd>dart:convert</dd>
  <dd>dart:core</dd>
</dl>
※使用したデータベース・API・パッケージとプラグインについての説明は省きます。

### 概要
1\.まず、データベースに登録したい書籍の情報は｢タイトル｣、｢著者｣、｢出版社｣をString型で、さらに｢貸出状況｣をboolean型で登録します。
2\.これをいちいちデータベースに手打ちで登録していくのは面倒なので、書籍のISBNをカメラでスキャンしてそれをOpenBDに送ると、書籍の情報がJSON形式で返ってきます。
3\.これをDartオブジェクトにデコードして登録をするか確認を行います。登録する場合、Cloud FireStoreにデコードした書籍の情報を追加します。登録しない場合は何も行いません。

##### 1について
データベースからデータを取得するときは基本的にモデルを準備するようです。
なので登録する書籍のモデルをDartコードで表すと以下のようになります。
```dart
class Book {
  Book(
    this.title,
    this.author,
    this.publisher,
    this.isLending,
  );

  String title;
  String author;
  String publisher;
  bool isLending;
}
```
書籍のタイトル(title)・著者(author)・出版社(publisher)をString型、貸出状況を表す変数isLendingをboolean型で宣言しています。

##### 2について
今回ISBNを読み込むパッケージとしてflutter_barcode_scannerを使用しました。
以下のコードはそのパッケージを使ってISBNをスキャンするコードです。
```dart
String _isbn = '';

Future scanISBN() async {
  try {
    //ISBNをスキャンする
    String isbn = await FlutterBarcodeScanner.scanBarcode(
        "#ff6666", "Cancel", true, ScanMode.DEFAULT);
      
    setState(() {
      _isbn = isbn;
    });
  } catch (e) {
    return errorText = 'バーコードのスキャンに失敗しました';
  }
}
```
`FlutterBarcodeScanner.scanBarcode`メソッドでスキャンを開始します。第１引数にはカメラを起動した時に表示する読み取り線(？)の色を16進数で指定します。第２引数にはスキャン画面上のキャンセルボタンのテキストを指定できます。第３引数はフラッシュアイコンの表示/非表示をbool値で指定できます。今回、私は念の為trueにしています。第４引数はQR・BARCODE・DEFAULTのいずれかを入れてscanModeを指定できます。

ISBNをスキャンすることができました。今度はスキャンしたISBNに応じた書籍の情報をOpenBDからhttpリクエストで取得するコードを書きます。以下のようなコードになります。
```dart
//ISBNに応じた書籍の情報をopenBDからhttpリクエストで取得する
Future<http.Response> getBookInfo() async {
  //scanISBNを実行
  await scanISBN();

  //取得したISBNから書籍の情報が入ったJSONをhttpリクエストで取得する
  var response = await http
      .get(Uri.parse("https://api.openbd.jp/v1/get?isbn=$_isbn&pretty"));

  //取得したJSONを返す
  return response;
}
```
このコードはまず前述した`scanISBN`メソッドが終了するのを待ちます。終了したら、`http.get`メソッドを使用してJSONをhttpリクエストで取得します。この時、引数はURIでなくてはなりません。なので、`Uri.parse`メソッドを使って引数の文字列をURIに変換します。`Uri.parse`メソッドの引数にある文字列のうち、`$_isbn`はString型の変数で取得したISBNを埋め込んでいます。
JSONをhttpリクエストで取得できたらURIをresponseという変数に代入し、返します。

##### 3について
`getBookInfo`メソッドが終了したら返ってきたJSONを受け取ってDartオブジェクトにデコードします。
```dart
String _bookTitle = '';
String _bookAuthor = '';
String _bookPublisher = '';
List<String> _bookInfo = [];
String errorText = '';

//getBookInfoで取得したJSONをDartオブジェクトにデコードする
Future<dynamic> _decodeResponse(http.Response response) async {
  if (response.statusCode == 200) {
    var bookInfo = await json.decode(response.body)[0];
    _bookTitle = bookInfo["onix"]["DescriptiveDetail"]["TitleDetail"]
        ["TitleElement"]["TitleText"]["content"];
    _bookAuthor = bookInfo["onix"]["DescriptiveDetail"]["Contributor"][0]
        ["PersonName"]["content"];
    _bookPublisher =
        bookInfo["onix"]["PublishingDetail"]["Imprint"]["ImprintName"];
    ["ResourceLink"];
    if (bookInfo != null) {
      setState(() {
        _bookInfo = [
          _bookTitle,
          _bookAuthor,
          _bookPublisher,
        ];
      });
      return _bookInfo;
    } else {
      setState(() {
        errorText = '書籍の情報を正しく受け取れませんでした';
      });
      return null;
    }
  } else {
    setState(() {
      errorText = '書籍情報の取得に失敗しました';
    });
    return null;
  }
}
```
JSONをString型のDartオブジェクトにデコードしたあと、`setState`メソッドでString型のリスト`_bookInfo`に書籍のタイトル(`_booktitle`)、著者(`_bookAuthor`)、出版社(`_bookPublisher`)を代入して、返します。

String型のDartオブジェクトが帰ってきたら`showModalBottomSheet`メソッドを使って書籍の情報を表示します。この表示は書籍の本当に登録するのかどうか確認する画面に使います。
```dart  
//書籍の情報を取得できた時にモーダルシートを出す
_showModalSheet(List<String> bookInfoList) {
  Size size = MediaQuery.of(context).size;
  return showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent,
      builder: (BuildContext context) {
        return Container(
          decoration: const BoxDecoration(
            //モーダル自体の色
            color: Colors.white,
            //角丸にする
            borderRadius: BorderRadius.only(
              topLeft: Radius.circular(20),
              topRight: Radius.circular(20),
            ),
          ),
          child: Padding(
            padding: EdgeInsets.symmetric(horizontal: 20),
            child: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        "[タイトル]:${bookInfoList[0]}",
                        style: TextStyle(fontSize: 15),
                      ),
                      Text(
                        "[著者]:${bookInfoList[1]}",
                        style: TextStyle(fontSize: 15),
                      ),
                      Text(
                        "[出版社]:${bookInfoList[2]}",
                        style: TextStyle(fontSize: 15),
                      ),
                    ],
                  ),
                  const Text(
                    'この本を登録しますか？',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 20,
                    ),
                  ),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      ElevatedButton(
                          onPressed: () {
                            _addToDataBase(bookInfoList);
                          },
                          child: const Text('はい')),
                      const SizedBox(
                        width: 50,
                      ),
                      ElevatedButton(
                          onPressed: () {
                            Navigator.of(context).pop();
                          },
                          style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.red),
                          child: const Text('いいえ'))
                    ],
                  )
                ],
              ),
            ),
          ),
        );
      });
}
```

さて、最後に確認画面で[はい]が押されたら、`_decodeResponse`メソッドから返ってきたString型のリストを受け取って、デコードした書籍の情報を事前に用意したデータベースに追加してあげます。
以下は事前に用意したデータベースの写真です。
![スクリーンショット 2022-12-16 16.34.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1877808/0261c586-ea65-45e9-996c-35e920bdd613.png)
`books`というコレクションのみがあり、ドキュメントが何も入ってない状態です。

以下のコードはデータベースに書籍の情報を追加するコードです。

```dart
FirebaseFirestore firestore = FirebaseFirestore.instance;

_addToDataBase(List<String> bookInfoList) async {
  try {
    await firestore.collection("books").add({
      "title": bookInfoList[0],
      "author": bookInfoList[1],
      "publisher": bookInfoList[2],
      "isLending": false
    }).then((documentSnapshot) {
      Navigator.of(context).pop();
    });
  } catch (e) {
    setState(() {
      errorText = "データベースに書籍情報を追加できませんでした。";
      Navigator.of(context).pop();
    });
  }
}
```
`collection`メソッドで先ほどのbooksコレクションを指定してあげます。さらに`add`メソッドでbooksコレクションのフィールド値に書籍のタイトル、著者、出版社を追加します。この時、ドキュメントIDは自動生成されます。そして、追加が終了したらモーダルシートを閉じるために`Navigator.of(context).pop()`をします。

### デモンストレーション
今回は「リーダブルコード」という本を登録してみましょう。

[書籍を登録する]というボタンを押すと....
![img1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1877808/df8d5cfb-10f0-fd15-5d99-f56aa66e8efc.jpeg)

ISBNをスキャンする画面が開きます。
ISBNを画面に映して、スキャンしてみましょう。
![img2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1877808/95b0d17c-7d28-892d-15a9-62c479b7bc86.jpeg)

スキャンに成功すると確認画面が開きます。
もし登録したい書籍と同じであれば[はい]を押しましょう。
![img3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1877808/19b4eded-c542-abd4-0671-614509fee080.jpeg)

書籍を一覧表示する画面に移動してみると、登録した書籍を確認出来ます。
![img4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1877808/c1f23945-6dd0-e837-548c-f903d17a35b9.jpeg)

データベースを確認してみても、フィールドが追加されていることを確認出来ます。
![スクリーンショット 2022-12-16 18.42.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1877808/56ac7f90-9915-df29-35f4-32e649588e5f.png)

# おわりに
ここまで読んでいただきありがとうございました！
2022年は２つの成果物を出来たので、かなり開発を進めることができた年だったと思います。また、自分自身の技術力の向上をさせることができました。本当に実りある1年でした！
2023年も楽しくアプリの開発を進めていこうと考えています！

# 好きなラーメン！
普段はあまりラーメンは食べないのですが、一風堂　大名本店さんの「博多とんこつらぁめん」が美味しかったです！特にスープが美味しかったです。
麺の硬さはバリカタでいただきました。一風堂さんで初めてバリカタを食べたのですが、噛みごたえがよく美味しかったです！

# 参考
https://firebase.flutter.dev/docs/firestore/usage/#collections--documents