Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Flutter デスクトップアプリで遊んでみたら意外となんでもできた
Search
Tasuku Maeda
May 16, 2026
59
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
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.8k
Featured
See All Featured
Testing 201, or: Great Expectations
jmmastey
46
8.3k
Winning Ecommerce Organic Search in an AI Era - #searchnstuff2025
aleyda
1
2.1k
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
508
140k
Leadership Guide Workshop - DevTernity 2021
reverentgeek
1
370
Organizational Design Perspectives: An Ontology of Organizational Design Elements
kimpetersen
PRO
1
820
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
1
6.2k
The Illustrated Children's Guide to Kubernetes
chrisshort
51
53k
How to Grow Your eCommerce with AI & Automation
katarinadahlin
PRO
1
270
Darren the Foodie - Storyboard
khoart
PRO
3
3.9k
Building Applications with DynamoDB
mza
96
7.2k
Faster Mobile Websites
deanohume
310
32k
Applied NLP in the Age of Generative AI
inesmontani
PRO
4
2.4k
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 に公開予定です マッハグループ メンバー募集中です! ご興味を持たれた方はぜひご連絡ください