Flutter で作っている漫画管理アプリ「ほんだな」の開発記、第1回です。
第0回では、作り始めた理由と、最初に決めた「何を持たないか」を書きました。
今回は、その反対側の「何を作るか」と「どう作ったか」です。前半は要件(誰のどんな場面のためのアプリか、何を優先して何をやらないか)、後半は Flutter・Riverpod・Drift で組んだ全体の構成です。個々の実装は次回以降に書きます。
何を作るか
作り始めるときに出したのは、ざっくりとした要件でした。いま要件の文書として残っているのは「プロダクト定義」(PRODUCT.md)で、これは Claude に下書きを書かせて、自分で直したものです。開発を進めながら、フェーズが変わるたびに更新していました。
この節の内容は、この文書から引いています。
誰の、どんな場面のためのアプリか
想定したのは、数十〜数百タイトル、数百〜数千巻を持っている漫画のコレクターです。「あの巻、持ってたっけ?」「最新巻はどこまで出てる?」が日常的に起きる人。ライトユーザーも入口としては歓迎しつつ、中心の体験はコレクター向けに合わせる、としています。
使う場面は6つ挙げていました。
| 場面 | 頻度 |
|---|---|
| A. 書店で「この巻、持ってたっけ?」(最重要) | 毎週 |
| B. 買って帰った本をまとめて登録する | 毎週 |
| C. 今週末に何を買いに行くか決める | 毎週 |
| D. ジャンル別に本棚を整理する | 月1 |
| E. スマホの機種変更でデータを移す | まれ・ただし重要 |
| F. どのシリーズの新刊が出ているか確かめる | 週1 |
最重要としていたのは A です。書店の棚から本を取ってバーコードを読み、持っている巻数と最新巻をその場で確かめて、買うか戻すかを決める場面です。
何を優先するか
優先する価値には、順番を付けていました。
- 速さ・手軽さ —— スキャンから登録まで、最小のタップ数で終わらせる
- 正確さ —— シリーズ名と巻数を、複数の書誌サービス(ISBN からタイトル・著者・巻数などを引けるネット上のデータベース)で補い、ユーザーが直さなくて済む精度を目指す
- オフライン対応 —— 書店など電波の弱い場所でも使える
- 見た目の満足感 —— 表紙の色や本棚の表示で「自分のコレクション」らしさを出す
ISBN は、本ごとに振られた識別番号です。バーコードの中身もこれです。
3つ目は、文書では「全機能が使える」と書いていました。ただ実際には、ISBN から本の情報を引くところはネットワークが要ります。掲げた目標であって、全部が達成できていたわけではありません。
何をやらないか
やらないことも、文書に並べていました。
- 巻ごとの個別管理(飛び飛びに持っている、同じ巻を2冊持っている、など)—— 持っている巻数は「何巻まで持っているか」の整数1つで足りるとした
- 多言語対応 —— 日本語のユーザーに集中する
- ソーシャル機能(他の人との共有・ランキング)
- 電子書籍の管理 —— 紙の本に絞る
- 通販サイトとの深い連携(在庫確認・カート追加など)—— 購入リストから Amazon の検索を開くところまで
ソーシャル機能と電子書籍を外した理由は、第0回に書いたとおり「ユーザーのデータを持たない」ためです。巻ごとの管理を外した経緯は、データベースの回で書きます。
機能を足すかどうかの判断基準
新しい機能を足すか迷ったときに見る基準も、4つ決めていました。
- 想定ユーザーが週1回以上使うか
- スキャンから管理までの主な流れを単純に保てるか
- オフラインで動くか(ネットワークに頼る機能には、つながらないときの代わりを用意する)
- 既存ユーザーのデータを壊さないか(データベースの形を変えるときは、既存のデータを安全に移せること)
課題と解き方
文書には、課題ごとに「どう解くか」も書いていました。以下は文書の記述そのままです。
| 課題 | 解決方法 |
|---|---|
| 所持確認が面倒(記憶・メモに頼っている) | バーコードスキャン 1 回で volumes テーブルを照合し、同シリーズの別巻でも即座にヒット |
| 書誌情報の手入力が大変 | OpenBD + Google Books の 2 API 並列取得・巻数合意ロジックで自動補完 |
| どの巻が未購入か把握しにくい | 未購入フラグ・購入リスト自動生成・各行から Amazon 検索へ直接遷移 |
| 本棚の整理・分類ができていない | グループ機能・3 つの表示モード(リスト/グリッド/シェルフ) |
| 機種変更でデータが消える不安 | JSON バックアップ・リストア(isbn → volumes も同時復元) |
volumes や「2 API 並列取得」が何かは、次の節で書きます。なお1行目の「別巻でも即座にヒット」は、初めて読み取る巻では書誌サービスへの問い合わせを挟みます(これも次の節で書きます)。
どう作ったか —— Flutter・Riverpod・Drift
ここからは設計書(DESIGN.md)の内容です。設計書も Claude で書いたものです。
全体の構成
UI(画面)
│
状態管理(Riverpod)
│
Repository(データの出し入れの窓口)
│
データベース(Drift / SQLite・端末の中)
書誌サービスへの問い合わせ(BookApiService)
└ OpenBD と Google Books に同時に問い合わせて、結果をまとめる
画面(UI)は表示と操作の受け付けを受け持ちます。いま画面に出すべき内容は状態管理(Riverpod というライブラリ)が持ち、変わったら画面に知らせます。データベースとのやり取りは Repository という窓口にまとめ、書誌サービスへの問い合わせは BookApiService という窓口にまとめています。
決めていた原則は次のとおりです。
- 呼び出す向きは一方向(画面 → Repository → データベース)。画面はデータベースを直接触らない
- Repository は抽象クラス(中身を持たない「型」だけの窓口)を挟む。テストのときに、本物のデータベースの代わりに偽物を差し込めるようにする。実際に、画面のテストでは偽物の Repository に差し替えている
- 書誌サービスへの問い合わせは1つの窓口に集める。画面側は、どのサービスを呼んでいるかを知らなくてよい
本棚の記録はすべて端末の中の SQLite に置きます。記録を預かるサーバーはありません。本の情報を調べるときだけ、ISBN などを外部の書誌サービスに問い合わせます。
使った主なもの
Flutter 本体以外は、機能ごとに追加したライブラリ(部品)です。書誌情報の2つだけは、外部のサービスにネット経由で問い合わせています。
| 用途 | 使ったもの | 種類 |
|---|---|---|
| 状態管理 | Riverpod | ライブラリ |
| データベース | Drift(SQLite を型付きで扱う) | ライブラリ |
| バーコード読み取り | mobile_scanner | ライブラリ |
| 表紙からの色抽出 | palette_generator | ライブラリ |
| 表紙画像のキャッシュ | cached_network_image | ライブラリ |
| 書誌情報 | OpenBD・Google Books | 外部サービス |
データの持ち方
テーブルは4つです。
| テーブル | 持つもの |
|---|---|
series | シリーズの書誌情報(名前・著者・出版社・完結したか・最新巻・表紙) |
manga | 本棚に置いた1件。何巻まで持っているかと、どの棚に入っているか |
volumes | 巻ごとの ISBN と、書誌サービスから取った情報 |
bookshelf_groups | 本棚のグループ(名前・色・並び順・他人に見せない棚か) |
書誌情報(series)と、持っている状態(manga)を分けています。持っている状態は「何巻まで」の整数1つで、これが上の「巻ごとの個別管理はしない」の実体です。
ISBN は巻ごとに volumes に持たせています。一度読み取った巻は ISBN と本の情報が端末に残るので、同じ巻をもう一度読み取ったときは書誌サービスに問い合わせずに済みます。
初めて読み取る巻は、書誌サービスに問い合わせるので、ネットワークが要ります。調べた結果はタイトルの近さで登録済みのシリーズと照らし合わせ、候補が見つかれば確認や選択のダイアログを出します。どの書誌サービスからも情報が取れなかったときは追加画面に切り替わり、手入力で登録します。
画面
ホーム(一覧:リスト/グリッド/本棚の3表示)
├ カメラボタン → スキャン
│ ├ 登録済みの本 → 詳細(巻数の更新を提案)
│ └ 未登録の本 → 追加
├ 1件をタップ → 詳細
├ カートボタン → 購入リスト
└ メニュー(最新刊の確認・統計・バックアップ・本棚グループの管理など)
むすび
第0回が「何を持たないか」なら、今回は「何を作るか」と、それを「どう作ったか」でした。
ここに書いた姿は、いきなりできたものではありません。次回は、最初の版が動いたあと、次に何を作るかをどう決めたかを書きます。