1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Proプラン1本でテスト1,118件のFlutter Webアプリを作った開発プロセス──Opus設計×Sonnet実装と手触りの受け入れテスト

1
Posted at

この記事は Zenn に投稿した記事を、Qiita 向けに再構成したものです。
元の記事: https://zenn.dev/aburachudoku/articles/nebulark-hands-on

3行で

  • Claude の Pro プラン1本で、思考整理アプリを約4週間で作りました。上位プランに課金する予算はありません
  • 予算が無いので、Opus には考える仕事だけ、コードを書くのは Sonnet、という3フェーズ分業で回しました
  • AI が「動くけど重い」コードを堂々と出してくるので、手触りまでテストで数えて守りました(テスト 1,118 件)

はじめに:予算は Pro プラン、欲は大盛り

作りたかったのは、こんな道具です。

  • マインドマップと長文のメモを、1画面で行き来したい
  • 登録不要で、URL を開いた瞬間に書き始めたい
  • データは自分のブラウザの中だけに置きたい
  • ついでに Word も Excel も取り込みたいし、AI でマップも作りたいし、スマホでも使いたい

「ついでに」の後ろが長い。完全に注文の多い料理店です。

一方、手元にあるのは Claude の Pro プラン1本。
Opus は賢いのですが、何でも頼んでいると、乗ってきた所で利用上限のお知らせが来ます。続きはリセットまでお預けです。

この「予算は並、注文は特盛」のギャップをどう埋めたか、というのがこの記事の話です。

できあがったのが Nebulark(ネビュラーク) です。

👉 https://nebulark.app/ (無料・登録不要・PC とスマホのブラウザで動きます)

Nebulark のデモ:目次でメモを移動し、矢印キーでノードを選び、Tab で子を足し、表を書き換え、Ctrl+K でコマンドを探す

項目 数字
契約 Claude Pro プラン1本
期間 約4週間(最初のコミットが 2026-09-23)
コミット 100 件超
アプリのコード 約 3.5 万行
テストのコード 約 3.4 万行(ほぼ同じ量。どっちが本体か分からない)
テスト 1,118 件(うち受け入れシナリオ 163 件。すべて flutter test で流れる)

技術スタックは Flutter(Web)・Riverpod 3・freezed・flutter_quill です。

作ったもの(ざっくり)

3ペインが連動する:ツリー × マインドマップ × Markdown メモ

マインドマップで全体を見て、ノートで細部を書く。
これを別々のツールでやると、だいたい「このメモ、どの枝の話だっけ?」で迷子になります。

なので画面を左から ノード一覧(ツリー)・キャンバス(マインドマップ)・メモ の3つに分け、どこで選んでもほかの2つが同じノードを指すようにしました。

メモは見た目どおりに編集でき(WYSIWYG)、保存は Markdown です。

  • 見出し(H1〜H3)から目次を自動で作り、押すとその見出しへ滑らかに移る
  • Excel のように編集できる表: 「セル選択モード」と「編集モード」を分け、Tab・矢印キーでセルを移り、打つと中身を置き換える。Excel からの貼り付けもそのまま表になる
  • コードブロック・引用・チェックリスト・区切り線・文字色・ノードへの内部リンク
  • 画像は貼るだけで、自動で縮小・圧縮して埋め込む(1 枚 200KB 以下に)
  • 行頭で # ・- ・> と打つと見出し・箇条書き・引用に、--- と打って改行すると区切り線になる

通信ゼロでファイルを取り込む

Word(.docx)・Excel(.xlsx)・PowerPoint(.pptx)・Markdown・CSV / TSV・HTML・テキストを、サーバーに送らず、ブラウザの中だけで解析してツリーにします。

Office のファイルは、実は中身が ZIP です。開けると XML がぎっしり詰まっています。福袋みたいなものです。それを端末の中で読んでいます。

  • Word は見出しのスタイル・箇条書き・段落番号・表を読み、見出しの階層をそのまま木にする
  • Excel は XML の木(DOM)を作らずに頭から順に読む。5,000 行 × 20 列のブックで、取り込みが 1.3〜1.6 秒から 0.3 秒になった
  • ファイルを画面にドロップするだけで、複数まとめて取り込める

BYOK(自分の API キー)で使う AI

AI は利用者自身のキーで動き、ブラウザから直接呼びます(Google Gemini API か、OpenAI 互換のエンドポイント)。既定のモデルは無料枠で使いやすい gemini-flash-lite-latest です。

操作 ショートカット 内容
マップ化 Ctrl+Enter ノードのタイトル・メモ・画像から、子ノードと各ノードのメモを作る
提案 S 次のアイデアを、異なる切り口で3つ出す
清書 Ctrl+Shift+Enter 事実を削らずに、見出し・箇条書き・表で整える(画像の中の文字も読む)
統合 Ctrl+Alt+Enter 広げた子孫を1つの Markdown 文書にまとめ直す(マップ化の逆)

どれも Ctrl+Z の1回で元に戻せます。
無料枠でよく出る 429(回数の上限)・503(混雑)には、待ち行列と自動の再試行(2 → 4 → 8 → 16 秒…)で対応しています。利用枠に泣かされた者として、使う人まで泣かせたくなかったので。

PC はキーボードだけで、スマホは2画面で

  • Ctrl+K のコマンドパレット: すべての機能と、すべてのキャンバスのノードを名前で探して実行する
  • 空間の矢印ナビゲーション: 親子関係ではなく、画面上の配置で「押した方向にある一番近いノード」へ移る
  • Alt+矢印キーで兄弟の入れ替え・階層の上げ下げ、Tab で子を足す、F2 で名前を変える、C で色を変える

スマホ・タブレットでは、3つのうち2つを上下(横持ちなら左右)に並べる専用の画面になります。

実践1:Pro プランで回す「Opus × Sonnet」3フェーズ分業

Opus にコードを書かせるのは、もったいない

コードを書く作業は、とにかく量が多い。量が多いと、そのぶん利用枠が減ります。
でも、本当に賢さが要るのは「何を作るか」「抜けはないか」を考える所です。

Opus にひたすらコードを書かせるのは、名医に受付の書類整理をお願いするようなもの。できるけど、そこじゃない。

そこで作業を3つに分けて、担当のモデルを変えました。

フェーズとコマンド

フェーズは Claude Code のカスタムスラッシュコマンド(.claude/commands/)で始めます。コマンドのフロントマターに model: を書いておくと、担当のモデルに自動で切り替わります。人間が切り替えを覚えておく必要がありません。忘れっぽい人間にやさしい。

フェーズ コマンド 担当 やること たとえるなら
1. 要件定義・設計 /design Opus 5.5 コードを書かずに読んで、エッジケースを洗い出し PLAN.md に書く 設計士
2. 実装・単体テスト /build Sonnet 5.5 PLAN.md のとおりに実装し、テストを足す 腕の立つ大工
3. 受け入れ・監査 /check Opus 5.5 git diff と PLAN.md を照らし合わせる。直さずに差し戻す 厳しめの検査官

コマンドの中身はこんな形です(抜粋)。

.claude/commands/design.md
---
description: Phase 1 要件定義・設計(Opus)。PLAN.md を作成して停止する
argument-hint: <作りたい機能・直したい不具合の説明>
model: opus
---

## 手順
2. **コードは一切変更しない。** 関連するファイル・Provider・ウィジェット・既存のテストと Robot を読んで調べるだけにする
3. エッジケースまで洗い出す: 要件の曖昧な点、フォーカス・IME・Web とテスト環境の違い・描き直しの抑制・
   保存とデバウンス・元に戻す / やり直す・キャンバスの切り替え・ライト / ダーク・狭い画面のあふれ、既存機能への影響
5. リポジトリ直下の `PLAN.md` に次の見出しで書き出す
   - 目的・要件 / 影響範囲 / 実装手順 / 単体テストの項目 / 受け入れテストの項目 / 懸念・未決事項
6. PLAN.md を書いたら停止し、要点を日本語で短く報告する
.claude/commands/build.md
---
description: Phase 2 実装・単体テスト(Sonnet)。PLAN.md に従って実装する
model: claude-sonnet-5-5
---

分けて良かったこと

1. 「考える」と「書く」を混ぜない

振り返ると、不具合のほとんどは「書く前に気づけたはずの条件」でした。フォーカスの取り合い、日本語入力の変換中、Web とテスト環境の違い、などです。

だから設計のフェーズでは、Opus にコードを一切触らせません。
触らせると、気持ちよく書き始めてしまうので。

2. Sonnet が迷わない粒度で計画を書く

PLAN.md の手順が細かいと、Sonnet は驚くほど速く、まっすぐ書きます。
ふわっとした指示を出すと、ふわっとしたものが返ってきます。それはそう。

PLAN.md に無い設計判断が必要になったら、勝手に広げず PLAN.md に追記して報告する、というルールです。

3. 書いた本人に採点させない

自分の答案を自分で採点すると、だいたい満点になります。人間も AI も同じです。

/check は別のモデルが、git diff を PLAN.md と CLAUDE.md(設計上の注意・開発ルール)に照らして見ます。修正はせずに差し戻すだけ。
これで「監査のついでに直したら、別の所が壊れた」という悲劇が起きなくなりました。

利用枠を守る4つの掟

CLAUDE.md には、仕様だけでなく「利用枠を守るための掟」も書いています。
どれも一度痛い目を見てから足したものです。

掟1: 同じエラーで2回続けて失敗したら、止まって報告する

AI は、同じ穴をとても真面目に掘り続けます。掘るたびに枠が減ります。
2回掘ったら手を止めて、人間に返してもらいます。

掟2: 環境のせいのエラーを深掘りしない

テストランナーの接続が切れる、のような環境の問題がたまに起きます。
そして賢いモデルほど、ここで名探偵になります。ログを読み、冗長ログで再実行し、推理を始める。謎は解けるかもしれませんが、枠は消えます。

なので、こう書きました。

【トークン節約ルール: 既知・環境起因エラーの深掘り禁止】flutter test と flutter analyze が通っていて、テストランナー自体のソケットの切断・時間切れ・「did not complete」などのインフラ・環境起因の既知の事象が起きたときは、原因の調査や冗長ログ(-v)での再実行を自分から始めない。

掟3: テストのログは必ずファイルに残す

-r expanded で1件ずつ結果を残し、Tee-Object でファイルに書きながら流します。落ちたらファイルを読むだけ。流し直しは要りません。

ちなみに PowerShell で | Select-Object -First N のようにパイプを途中で打ち切ると、flutter ツールだけが止まり、約 600MB の flutter_tester が親の無いまま残り続けます。
気づいたら PC の中にゾンビがいました。これも CLAUDE.md に書いています。

掟4: ハマった理由を CLAUDE.md に残す

「何をしたか」ではなく「なぜそうしたか」を書いておくと、次の設計はそれを前提に始まります。
同じ穴に二度落ちないこと。結局これが、いちばんの節約でした。

実践2:AI に「手触り」まで保証させる受け入れテスト

テストは全部緑、でも触ると重い

AI はコードを速く書きます。それはありがたい。
でもたまに、動くけど重いものを、とても自信ありげに出してきます。

  • キーを押し続けると、画面が追いつかない
  • 1文字打つたびに、画面全体が作り直される

こういう劣化は、単体テストではまず見つかりません。テストは全部緑なのに、触ると重い。
健康診断はオールAなのに、階段で息が切れる、みたいな状態です。

そこで CLAUDE.md の開発ルールに、次の一文を【必須ルール】として書きました。

新しい機能の追加や既存UIの改修を行った際は、必ず integration_test/ 内のシナリオ(Robotパターン)を拡張・追加し、手触りやパフォーマンスが劣化していないことをテストで証明してからタスクを完了すること。

その結果、テストは 1,118 件(単体・ウィジェット 900 件超+受け入れシナリオ 163 件)になりました。

Robot パターン:ロボットに人間のふりをさせる

受け入れテストでは、AppRobot・MindMapRobot・EditorRobot・CommandPaletteRobot などの Robot が、人と同じ手順で操作します(integration_test/robots/ に 16 ファイル)。

キーは押したまま・離すまでをちゃんと送り、文字は1文字ずつ入力欄へ届けます。
人間よりも几帳面に、人間のふりをしてくれます。

RebuildCounter:作り直しの回数を数える

debugOnRebuildDirtyWidget にフックをかけ、数え始めた時点にあった部品の作り直しだけを、型ごと・キーごとに数えます。

integration_test/scenarios.dart(抜粋)
final counter = RebuildCounter()..start();
await palette.arrow(LogicalKeyboardKey.arrowRight);
await palette.arrow(LogicalKeyboardKey.arrowRight);
counter.stop();

expect(counter.total, greaterThan(0)); // 数え漏れで素通りしないように
expect(counter.countOf<CanvasArea>(), 0, reason: counter.report());
expect(counter.countOf<MindMapNodeView>(), 0, reason: counter.report());
expect(counter.countOf<NodeSidebar>(), 0, reason: counter.report());

地味に大事なのが counter.total > 0 のチェックです。

フックが外れていると、何も数えられずに「0回!合格!」になってしまいます。体重計の電源が入っていないのに「0kg!痩せた!」と喜ぶようなものです。
AI は素通りしたテストでも元気に「完了しました」と言うので、ここは疑ってかかります。

数えてみたら、事件だった

計測で見つかった「動くけど重い」の数々です。どれもテスト環境での値です。

場面 直す前 直した後
ペインの境界線を 30 フレーム ドラッグ メモの行を 2,460 回、ツールバーのボタンを 1,020 回作り直していた 幅から決まる値が変わったときだけ作り直す。キャンバスの線の描き直しは 31 回 → 0 回
500 行 × 12 列の表を編集できる状態にする 17.7 秒(入力欄 6,000 個) 入力欄は今のセルだけ作って 1.8 秒。受け入れテスト全体も 67 秒 → 8 秒
その表があるメモに1文字打つ 表を毎回整え直して 12ms 表ごとに結果を覚えて 0.7ms
約2万字のメモを開く(Markdown → 文書の変換) 約 40ms 変換結果を覚えておいて約 3ms
約2万字・表3つのメモのノードへ戻る エディタを作り直して約 0.5 秒 長いメモのエディタは捨てずに隠しておき、戻ったら出すだけで約 30ms
約2万字のメモに1文字打つ 打つたびに全体を Markdown にして約 10ms 手が止まって 300ms 後に1回だけ変換
矢印キーを連打 キーのたびにメモを作り直し、押し続けると打鍵に追いつかない 連打のあいだはメモを読まず、手が止まって 50ms 後に読む

境界線をちょっとドラッグしただけで、メモの行を 2,460 回作り直していました。
目で見ると「少し重いかな?」くらいなんです。数えて初めて、事件の大きさが分かりました。

長いメモの切り替えは、2段構えで直しました。

  • 変換結果のキャッシュ: Markdown の文字列をキーに、変換した文書(Delta)を最近の 40 個まで覚えておく
  • エディタのキープアライブ: 3,000 字以上のメモのエディタを最大 3 個まで捨てずに Offstage で隠しておき、戻ったら表示するだけにする

本棚の本を毎回燃やして書き直していたのを、しおりを挟んで置いておくようにした、くらいの違いです。

ハマりどころ図鑑

ここからは、4週間で出会った罠のうち、人に話したくなるものです。
どれも CLAUDE.md に「なぜそうしたか」として残っている実話です。

ショートカット争奪戦

キーボードで全部できるアプリを作ろうとすると、空いているキーを探す旅が始まります。
そして、行く先々に先客がいます。

やりたかったこと 最初の案 先客
表を挿入 Ctrl+T ブラウザが「新しいタブ」として持っていく。ページにキーが届かないので止めようがない
表を挿入(再挑戦) Alt+T Mac の Option+T は文字の「†」として届く。キーの位置(PhysicalKeyboardKey)で判定して解決
ペインの移動 Alt+1 Mac の Option+1 は「¡」になる。これもキーの位置で判定
AI の提案 Ctrl+Space Mac の入力ソースの切り替え・日本語入力のオンオフとぶつかる。S キーに
メモの文字色 Ctrl+Shift+C Chrome の開発者ツールが開く。Alt+C に
コマンドパレット Ctrl+K メモのエディタ(flutter_quill)が「リンクを付ける」として先に持っていく。エディタの customShortcuts の先頭にも同じキーを登録して奪い返した

表を入れたかっただけなのに、短剣(†)が刺さりました。

教訓: Mac の Option キーの組み合わせは文字として届くので、文字(LogicalKeyboardKey)ではなくキーの位置で判定すると確実です。

AI は JSON の前に挨拶してくる

「JSON の配列だけを出力して」とお願いしても、AI の返事はなかなか自由です。

  • 「はい、承知しました!」と元気に挨拶してから JSON が始まる
  • 頼んでいないのに、コードブロックで包んでくる
  • メモに Windows のパス(C:\Users)を書いて、バックスラッシュで JSON を壊す
  • 長い文書だと、出力の上限で途中で力尽きる

最後のが一番つらかった。15章立ての技術文書で、長い返事を頑張って書いてくれたのに、途中で JSON がぶつ切りになって「JSON として読めませんでした」。全部パーです。

そこで、崩れた JSON でもなんとか読む LenientJson を作りました。次の順で試します。

  1. そのまま読む
  2. コードブロック・前後の挨拶を除き、最初の [ / { から対応する閉じ括弧までを読む
  3. 文字列の中の生の改行・制御文字と不正なエスケープを直し、閉じ括弧の前の余分なカンマを取る
  4. 途中で切れていたら、最後のきりのいい所で切って、開いたままの括弧を閉じてあげる

あわせて、JSON 出力のモードと出力の上限(maxOutputTokens)を指定し、長い入力のときは「メモは要点だけにして、JSON は最後まで閉じて」とお願いを足しています。

今は AI が途中で力尽きても、作れた所まではちゃんとノードになります。
AI が書いた JSON を、AI が書いたコードが介護している。そんな時代です。

区切り線が「テキスト- - -」になって消える

メモで区切り線の上下の余白を押してから文字を打つと、文字が線と同じ行に入り、保存すると テキスト- - - という謎の文字列になって線が消えていました。

初めて見たとき、しばらく画面の前で固まりました。何が起きたのか、まったく分からない。

対策は3段構えです。

  • カーソルを線の行に止めない
  • 線の前後で必ず行を分ける
  • 以前の版で保存された テキスト- - - も、読み込むときに線へ戻す

アニメーションには擬音で名前を付ける

キャンバスの動きは、設計メモ(CLAUDE.md)の中で擬音で呼んでいます。大真面目に。

名前 動き 長さ・カーブ
ポン(出現) 0 から大きくなり、行き過ぎて跳ねてから落ち着く 450ms、elasticOut
ギューン(削除) 1.3 倍までふくらんでタメを作り、加速しながら縮んで消える 400ms、TweenSequence
スッ(選択枠) 前のノードの枠から次のノードの枠へ滑る 280ms、easeOutCubic

削除は最初 easeInBack を使いましたが、ふくらみが1割ほどで、ギューンが全然見えませんでした。
名前を付けておくと、「削除のギューンが弱い」の一言で AI に伝わります。仕様書に「ギューン」と書く日が来るとは思いませんでした。

ちなみに、OS の「視差効果を減らす」設定(MediaQuery.disableAnimations)に従って動きを止めるようにしていたら、開発機の Windows の「アニメーション効果」がオフだったせいで、テストの中でだけ動きが止まるという事件もありました。アプリが真面目すぎる。テストでは disableAnimations: false を明示しています。

iPhone のブラウザは別の惑星

いちばん苦労したのは iPhone のブラウザです。
テスト環境では起きない問題ばかりなので、実機の条件をテストで再現することにこだわりました。

キーボードが出ても、画面の大きさが変わらない
iOS Safari は、キーボードが出ても Flutter に画面の大きさの変化を伝えてくれません。結果、打っている行が下のツールバーの裏に隠れて、自分が何を打っているのか見えない。
visualViewport からキーボードの高さを出し、カーソルの行の上に 24px・下に 120px を残してスクロールします。テストでは viewInsets を変えずに「ブラウザのキーボードの高さだけ」を差し替えて確かめます。

テストは全部緑なのに、ボタンが効かない
Flutter の入力欄は、外を押されるとフォーカスを外します。ただし、指のタッチでそうするのは Web のときだけ。テスト環境(Android 扱い)の指では外しません。
つまり「iPhone では貼り付けボタンが効かないのに、テストは全部緑」。これは気づけない。
テストではボタンをマウスで押す(どの環境でもフォーカスを外す)ことにして、Web の指と同じ条件にしました。

アイコンが描かれない
「選択」「全選択」のアイコンが、iPhone の実機でだけ空白になりました。PC では出ているのに。仕方がないので文字のボタンにしました。

頼んでいないのに拡大する
入力欄の文字が 16px 未満だと、iOS は気を利かせて画面を拡大します。Flutter の隠れた入力欄の 13.3px を 16px にしました。

メモを書きたいのに、カード番号を聞かれる
iOS がメモの入力欄をパスワード・住所の欄だと思い込み、キーボードの上に鍵とクレジットカードの帯を出していました。入力欄に autocomplete="off" を付け直しています。

落ちたときの「現場検証」を自動で残す

テストが失敗したときは、直近 60 個の操作・フォーカスの経路・開いているダイアログ・AI の処理の状態などを書き出す診断(DIAGNOSTIC DUMP)を出します。

AI がログを1回読むだけで、どこで・なぜ落ちたか見当が付きます。流し直さなくていいので、これも利用枠の節約になりました。

まとめ

やったこと 効いたこと
Opus は設計と監査、Sonnet は実装 賢さが要る所にだけ高いモデルを使い、Pro プランの枠で回る
書いた本人に採点させない 監査は直さずに差し戻すので、ついでの修正で壊れない
手触りを数えて守る 作り直しの回数と1打鍵の時間をテストにし、「緑なのに重い」を防ぐ
理由を CLAUDE.md に残す 同じ穴に二度落ちない。品質にも枠にも効く

上位プランが無くても、役割を分けて、掟を書いて、手触りまでテストで数えれば、ここまで作れました。
予算は並でも、仕組み次第で特盛は作れます。

おわりに

Nebulark は、登録不要・無料で今すぐブラウザから使えます。

👉 https://nebulark.app/

  • データはブラウザの中だけに保存されます(サイトデータを消すと消えるので、メニューの「データをエクスポート」で JSON のバックアップを取ってください)
  • AI を使うときだけ、設定した AI(Gemini やローカル LLM など)へ端末から直接送られます

「こういう操作がほしい」「ここで引っかかった」など、コメントや X で気軽に教えてください。
いいね・ストックをもらっても利用枠は増えませんが、作者のやる気は増えます。

1
1
3

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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?