Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Flutter デスクトップアプリで遊んでみたら意外となんでもできた
Search
Tasuku Maeda
May 16, 2026
0
0
Share
Flutter デスクトップアプリで遊んでみたら意外となんでもできた
2026/05/11開催の
「AI時代のFlutter開発スペシャル」
で発表した内容です。
Tasuku Maeda
May 16, 2026
More Decks by Tasuku Maeda
See All by Tasuku Maeda
これからはじめるSwiftUI 〜iOSアプリ開発の新スタイル・新スタンダード〜
tasukumaedacm
0
1.7k
Featured
See All Featured
How Fast Is Fast Enough? [PerfNow 2025]
tammyeverts
3
560
4 Signs Your Business is Dying
shpigford
187
22k
GraphQLとの向き合い方2022年版
quramy
50
15k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
49
9.9k
Leveraging LLMs for student feedback in introductory data science courses - posit::conf(2025)
minecr
1
250
Mozcon NYC 2025: Stop Losing SEO Traffic
samtorres
0
220
The Anti-SEO Checklist Checklist. Pubcon Cyber Week
ryanjones
0
130
Building Experiences: Design Systems, User Experience, and Full Site Editing
marktimemedia
0
500
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
540
Music & Morning Musume
bryan
47
7.2k
YesSQL, Process and Tooling at Scale
rocio
174
15k
Breaking role norms: Why Content Design is so much more than writing copy - Taylor Woolridge
uxyall
0
270
Transcript
Flutter デスクトップアプリで 遊んでみたら 意外となんでもできた AI時代のFlutter開発スペシャル 前田タスク(クラスメソッド リテールアプリ共創部 マッハグループ) 2026年5月11日
自己紹介 前田タスク(リテールアプリ共創部 マッハグループ) iOS/Android モバイルアプリ開発 iOSエンジニアから、Flutterエンジニアへ(1年くらい) 前はWebやってました(7年ぐらい前まで) 2
チーム紹介 クラスメソッド リーテルアプリ共創部 マッハグループ LINE・モバイルアプリ 立ち上げ専門部署 https://mach.classmethod.info/team/ 絶賛メンバー募集中です! 3
今日話すこと パート 時間 内容 デモ1 8分 Flutter Desktop でためした機能を紹介 デモ2
3分 機能を組み合わせた画像ビューア アプリ デモ3 6分 VSCode拡張 × Claude × VOICEVOX × Flame でずんだもん解説を生成 ※ ここだけFlutter Webです ※ デモ1はソースコード公開予定です 4
ご注意 すべてmac版のみとなります。Windowsは書き出し含め試してません デモはすべてClaude Codeで作成したものです 概要は理解していますが、実装の細かい部分まではすぐに回答できない可能性が あります、 、 5
みなさんFlutter Desktop使ってますか? ふだんモバイルアプリ開発しかしてなかった私ですが、 今回Flutter Desktopを試してみました! 6
初期設定(mac用) # macOS 用プロジェクトを作成 fvm flutter create --platforms=macos myapp cd
myapp Flutter SDK管理にふだんfvmを使っているのでfvmで書いてますが、もちろんfvmな しでも大丈夫です! iOS / Android 開発と同じ感覚で始められる 7
Debug & Release # Debug 起動(ホットリロード可) fvm flutter run -d
macos # Release ビルド fvm flutter build macos --release # → build/macos/Build/Products/Release/myapp.app Debug は ホットリロード可 で開発中はこっち Release ビルドは .app バンドル ができ、Finder でダブルクリック可能 Debug と Release では挙動が違う場面がある(デモ1 のハマりポイントで詳し く…) 8
デモ1 Flutter Desktop でためした機能を紹介 9
デモ1: 紹介する機能(7つ) 1. Window — ウィンドウの操作・制御 2. System Tray —
メニューバーに常駐 3. Drag & Drop — ファイルを D&D で受け取る 4. File Explorer — ローカルファイルを閲覧 5. Local Server — HTTP サーバーを起動 6. curl — HTTP リクエストを実行 7. Claude — Claude CLI を呼び出す 10
デモ動画 ① UI・ファイル系 Window / System Tray / Drag &
Drop / File Explorer https://youtu.be/0UcUY6jr2Lc 11
1. Window ウィンドウのサイズ・位置・常時最 前面・透明度を制御 「自プロセスをもう1つ起動」 で別ウ ィンドウを開く(別アプリ扱い) 12
1. Window — 実装 window_manager ライブラリを使用 // ウィンドウ操作 await windowManager.setSize(const
Size(800, 600)); await windowManager.setAlwaysOnTop(true); await windowManager.setOpacity(0.8); // 「新しいウィンドウ」ではなく、自プロセスをもう1つ起動 // これはwindow_managerではなくdart.ioの機能 await Process.start(Platform.resolvedExecutable, []); 13
1. Window — ハマりどころ main() で await windowManager.ensureInitialized() を必ず先に呼ぶ runAppの前
window_managerだと複数ウインドウの管理はできない アプリをもうひとつ起動する形 Debugだと新しいウインドウは作れなかった 別ウィンドウで状態共有したいなら desktop_multi_window 等の本格パッケージ へ 14
2. System Tray メニューバーにアイコンを常駐させ る サブメニュー付きメニューも作れる 各 MenuItem のクリックを受け取れ る
15
2. System Tray — 実装 tray_manager ライブラリを使用 class _State extends
State<MyPage> with TrayListener { await trayManager.setIcon('assets/tray_icon.png'); // Flutter assets パスを渡す await trayManager.setContextMenu(Menu(items: [ MenuItem(label: 'あいさつ', key: 'greet'), MenuItem(label: 'モード', submenu: Menu(items: [...])), ])); @override void onTrayIconMouseDown() => trayManager.popUpContextMenu(); } 16
2. System Tray — ハマりどころ(Directory.current 罠) ${Directory.current.path}/... で絶対パス組立 → Debug
は通るが Release (Finder起動)でうまくいかない( Directory.current が起動方法で変わる) tray_manager の macOS 実装は内部で rootBundle.load(iconPath) を呼ぶ → アセット相対パス( assets/xxx.png )を渡すのが正解 pubspec.yaml の flutter.assets に PNG を登録する必要あり 17
3. Drag & Drop ファイル / ディレクトリを D&D で受 け取る
複数ファイル同時、ドラッグ中の hover フィードバック対応 18
3. Drag & Drop — 実装 desktop_drop DropTarget( onDragEntered: (_)
=> setState(() => _isDragging = true), onDragExited: (_) => setState(() => _isDragging = false), onDragDone: (DropDoneDetails details) { for (final xFile in details.files) { final file = File(xFile.path); // 絶対パスが取れる // ... } }, child: /* ドロップエリア */, ) 19
3. Drag & Drop — ハマりどころ ディレクトリも XFile で渡る →
stat.type == FileSystemEntityType.directory で判定 macOS Sandboxing でファイルアクセス権限の Entitlement が必要 20
4. File Explorer ローカルファイルの一覧・閲覧 テキストファイルは シンタックスハ イライト付きで表示 21
4. File Explorer — 実装 dart:io (一覧・読込) highlight (パース)+ flutter_highlight
(Monokai テーマだけ拝借) // ディレクトリ一覧(sortでフォルダ優先で名前順ソートなどできる) final entries = Directory(path).listSync() ..sort((a, b) { /* ... */ }); // ファイル読込 final content = await File(path).readAsString(); // シンタックスハイライト(選択コピーも効かせるため自前で TextSpan に) final result = highlight.parse(content, language: lang); return SelectableText.rich(_toSpans(result.nodes, theme)); 22
4. File Explorer — ハマりどころ flutter_highlight の HighlightView だと表示は簡単。でも選択できない。 (
SelectionArea で囲んでも 選択コピー不可) → 自前で SelectableText.rich を組む( highlight パッケージで AST 取得 → TextSpan に変換) 23
デモ動画 ② 通信・外部連携系 Local Server / curl / Claude https://youtu.be/uy3PfaNYOOg
24
5. Local Server アプリ内で HTTP サーバーが立つ 通常のブラウザや curl からもアクセ ス可能
リクエストログをアプリ画面に流せ る 25
5. Local Server — 実装 dart:io の HttpServer (追加パッケージ不要!) final
server = await HttpServer.bind( InternetAddress.loopbackIPv4, 8080, ); server.listen((HttpRequest request) { request.response ..headers.contentType = ContentType.json ..write(jsonEncode({'message': 'Hello from Flutter'})) ..close(); }); 実はiOS/AndroidでもHTTPサーバーは立てられる。でも外部からアクセスしづらい デスクトップアプリならブラウザからもアクセス可能 26
5. Local Server — ハマりどころ App Store 配布で app-sandbox: true
にする場合のみ Release.entitlements に com.apple.security.network.server の追加が必要 開発時は何もしなくて OK(Flutter テンプレートのデフォルトで app-sandbox: false のため Entitlement の制限を受けない) dispose() で必ず server.close() 。やらないとポートを掴みっぱなし 27
6. curl 外部コマンド curl を実行して、結 果を画面に表示 HTTP クライアント不要 で外の世界 と通信できる。
28
6. curl — 実装 dart:io の Process.run final result =
await Process.run('curl', ['-s', url]); final body = result.stdout as String; 29
6. curl — ハマりどころ curl は /usr/bin/curl にあるので、コマンドが動作しないといった PATH 問題
は起きない(次の Claude では起きる…) 開発時は何もしなくて OK( app-sandbox: false のため) 。App Store 配布時のみ network.client の Entitlement が必要 30
7. Claude CLI連携 claude -p を呼び出し Claude のレスポンスを Markdown で
レンダリング Flutter から AI を呼べる! 31
7. Claude CLI連携 — 実装 dart:io の Process.run + flutter_markdown_plus
ライブラリ(表示) // Finder ダブルクリック起動でも PATH を解決するため zsh -ilc 経由 final result = await Process.run( '/bin/zsh', ['-ilc', 'claude -p "\$1"', 'flutter-app', prompt], ); // Claude の応答(Markdown)をレンダリング return Markdown( data: result.stdout as String, selectable: true, // 選択コピー対応 styleSheet: MarkdownStyleSheet(/* h1/code 等のスタイル指定 */), ); 32
7. Claude CLI連携 — ハマりどころ Release を Finder からダブルクリック起動 すると
claude が PATH に無く、アプ リごと落ちる(なぜかcatch も素通り) /bin/zsh -ilc でログイン + インタラクティブシェル経由で呼ぶ。 -i を付けな いと ~/.zshrc が読まれず PATH が補強されない シェルエスケープ回避のため、プロンプトは "$1" 展開で渡す 33
デモ2 画像ビューアアプリ 34
デモ動画 画像ビューアアプリ https://youtu.be/Q7jnEAr-kF4 35
デモ2: 画像ビューア 入力: D&D / macOSのメニューから NSOpenPanel 対応形式: PNG /
JPEG / GIF / WebP / ZIP 並び順: 名前順 / 更新日時 表示: 全体 / 画面いっぱい / 実寸 shared_preferences で設定と最終 ソースを永続化(次回起動時に自動 復元) 36
デモ2: 実装ポイント 実用アプリにするにあたって、こだわった所: 1. ファイル選択 — MethodChannel で NSOpenPanel を直接叩く(ファイル
/ ディレ クトリを 1 ダイアログで両対応) 2. マウスホイール対応 — スクロール / ピンチ操作 3. キーボードショートカット対応 — ページ送り / 表示モード切替などをキー1つで 4. 画像 ZIP 対応 — archive パッケージで ZIP 内画像を直接 ImageProvider 化 → それぞれ別ページで実装の中身を見ていきます 37
実装① ファイル選択(NSOpenPanel) // Swift (macos/Runner/MainFlutterWindow.swift) let panel = NSOpenPanel() panel.canChooseFiles
= true panel.canChooseDirectories = true // ← ファイル「も」ディレクトリ「も」OK panel.allowsMultipleSelection = true panel.allowedFileTypes = ["png","jpg","jpeg","gif","webp","zip"] panel.beginSheetModal(for: self) { _ in result(panel.urls.map { $0.path }) } // Dart (data/services/file_dialog_service.dart) final paths = await MethodChannel('image_viewer/file_dialog') .invokeListMethod<String>('openPaths'); file_picker ライブラリだとファイル/ディレクトリで別ダイアログになる NSOpenPanel を直接叩くと 1 つのダイアログで両対応にできる 38
実装② マウスホイール対応 Flutter 標準の Listener + PointerScrollEvent // viewer_page.dart Listener(
onPointerSignal: (event) { if (event is! PointerScrollEvent) return; if (event.scrollDelta.dy > 0) { notifier.showNext(); // ↓ スクロール = 次の画像 } else if (event.scrollDelta.dy < 0) { notifier.showPrevious(); // ↑ スクロール = 前の画像 } }, child: ..., ) 39
実装③ キーボードショートカット 2系統 で実装: // ① menus/app_menu_bar.dart — macOS メニューと連動(メニューに
⌘O が表示される) PlatformMenuBar(menus: [ PlatformMenu(label: 'ファイル', menus: [ PlatformMenuItem( label: '開く...', shortcut: const SingleActivator(LogicalKeyboardKey.keyO, meta: true), onSelected: notifier.openFromMenu, ), ]), ]); // ② widgets/viewer_page.dart — メニューに出さないキーは Focus で直接処理 Focus(onKeyEvent: (node, event) { if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { notifier.showPrevious(); return KeyEventResult.handled; } if (event.logicalKey == LogicalKeyboardKey.arrowRight) { notifier.showNext(); return KeyEventResult.handled; } // = / - / 0 でズーム操作 return KeyEventResult.ignored; }, child: ...); 40
実装④ 画像 ZIP 対応 archive パッケージ + 自前の ImageProvider //
ZIP を開く(中身の解凍は遅延、find したものだけ展開される) final input = InputFileStream(zipPath); final archive = ZipDecoder().decodeStream(input); Uint8List readBytes(String entryName) => archive.find(entryName)!.content; // ImageProvider を継承して、ZIP 内のエントリを画像として返す class ArchiveImageProvider extends ImageProvider<ArchiveImageProvider> { Future<Codec> _loadAsync(...) async { final bytes = source.readBytes(entryName); // ZIP から取り出す final buffer = await ImmutableBuffer.fromUint8List(bytes); // メモリへ return decode(buffer); // → Flutter の Image へ } } 41
デモ3 VSCode拡張 × ずんだもん解説 42
デモ動画 VSCode拡張 × ずんだもん解説 https://youtu.be/3qwrJnNTdg8 操作フロー: 1. 適当なコードを範囲選択(左下にチップが自動表示) 2. 質問を入力
→ 「解説して!」 ボタン 3. ずんだもんが進捗を実況: 「Claude さんに依頼中なのだ ...」 「合成中 1/8 ...」 4. 「準備できたのだ〜!画面をクリックで再生開始するのだ!▼」 で告知 5. ステージクリック → 字幕 + 口パク + 音声同期で会話劇スタート 43
デモ3: 何をしているの? VSCode の 下パネル に解説タブを追 加 コードを範囲選択 → 質問を書いて
「解説して!」 ボタン ずんだもん × 四国めたんが 掛け合い で解説(音声合成 + 字幕 + 口パクを リアルタイムで合成) プロジェクトの CLAUDE.md も Claude に読まれる → コードベースに 即した解説 44
VSCode 拡張機能はどう作る? 本体(拡張ホスト): Node.js / TypeScript で動く VSCode の API(エディタ操作、コマンド登録、選択範囲取得など)を呼べる
GUI: WebView で HTML / JS / CSS を表示するのが標準 拡張ホスト WebView は postMessage で双方向通信 → 今回は WebView の中身を Flutter Web で作った flutter build web の成果物を WebView に読ませる 普段の Flutter 開発と同じ感覚で UI を組める! 45
VOICEVOX とは 無料で使える テキスト音声合成エンジン ローカル PC で動く(オフライン・無料) ずんだもん / 四国めたん
など 30+ 種類のキャラ音声 ローカル HTTP API( http://127.0.0.1:50021 )で操作 # テキストからクエリ生成 POST /audio_query?text=こんにちは&speaker=3 # クエリから音声合成(WAV 取得) POST /synthesis?speaker=3 公式: https://voicevox.hiroshiba.jp/ 46
Flame ライブラリとは Flutter 用の 2D ゲームエンジン スプライトアニメーション / 衝突判定 /
シーン管理 Component ベース の設計( FlameGame に Component を追加していく) update(dt) で 各フレームごとの処理 を書ける ゲームに限らず タイミング制御が必要なアニメ に向く 公式: https://flame-engine.org/ → デモ3 では 口パクアニメーション と 字幕の文字送り に使用 47
デモ3: 全体構成 48
デモ3: 仕組み(パイプライン) 1. VSCode 拡張でエディタ選択 + プロンプトを取得 2. claude -p
--output-format json で シナリオ JSON を生成 [{speaker: 'zundamon', text: '...'}, {speaker: 'metan', text: '...'}, ...] cwd を workspace folder に設定 → プロジェクトの CLAUDE.md を引き継ぎ 3. VOICEVOX のローカル API で各セリフを WAV 化 → /connect_waves で 1 本に連 結 4. Flutter Web (Flame) が WebView で再生 AudioPlayer 1 個 + seek(offsetMs) でセリフ切替 字幕送り( durationMs / textLength )+ 話者で口パク切替 49
デモ3: ハマり① ブラウザの autoplay policy 最初は「セリフごとに AudioPlayer を作って play() を順次呼ぶ」方式だったが…
2 セリフ目以降が無音(字幕は進むのに音だけ止まる) 原因: ブラウザは「ユーザー操作と紐付いた <audio> 要素」しか自動再生を許可しな い。 audioplayers の Web 実装は AudioPlayer ごとに <audio> を作るので、2 個目 以降は別要素扱いでブロック。 解決: VOICEVOX の /connect_waves で全セリフを 1 本の WAV に連結 → AudioPlayer 1 個固定 + seek で位置切替。最初のクリックで取った再生権限を全セ リフ通して使い回せる。 50
デモ3: ハマり② WebView の音声 URL と CSP 合成した WAV を
WebView から再生するには 2つの壁 がある: 1. localResourceRoots に書き出し先を追加 webview.options = { localResourceRoots: [flutterRoot, audioRoot], // ← audioRoot を追加 }; const url = webview.asWebviewUri(vscode.Uri.file(wavPath)).toString(); 2. CSP に media-src を追加(忘れると <audio> が黙殺) media-src ${webview.cspSource} blob: 書き出し先は context.storageUri (workspace storage)を使う。 51
デモ3: Flame での同期タイムライン 52
デモ3: Flame で口パク・字幕送り flame (ゲームエンジン本体) audioplayers (音声再生、独立パッケージ。 flame_audio は今回使わず) //
セットアップは 1 回だけ final player = AudioPlayer(); await player.setSource(UrlSource(audioUrl)); // 各セリフ: seek → resume → durationMs 待つ → pause await player.seek(Duration(milliseconds: entry.offsetMs)); await player.resume(); await Future.delayed(Duration(milliseconds: entry.durationMs)); await player.pause(); 口パク: SpriteAnimationComponent.playing = true/false で話者切替 字幕送り: update(dt) で durationMs / textLength の速度で文字を進める 53
まとめ Flutter Desktop は意外となんでもできた! iOS/Android 開発者でも入りやすい 各種ライブラリが充実している ターミナルと連動できるのでいろいろ作りやすい 自分の思い通りのアプリを気軽に作れる 54
ありがとうございました! デモ1 のコードは GitHub に公開予定です マッハグループ メンバー募集中です! ご興味を持たれた方はぜひご連絡ください