Flutter: WebとDesktopマルチプラットフォームデプロイ
一度書けば、どこでも動く — WebとDesktopがあなたのコードをモバイルから世界へ届けます。
📋 前提条件: 以下を先にマスターしている必要があります
- レッスン17:プラットフォームチャンネルとネイティブ相互運用
1. このレッスンで学ぶこと
- Flutter Webビルド最適化:WASM / CanvasKit描画 / URL戦略 / SEO
- Webレスポンシブレイアウト:MediaQuery / LayoutBuilder / アダプティブブレークポイント設計
- Desktop適応:window_managerによるウィンドウ管理 / メニューバー / マウス&キーボードショートカット
- プラットフォーム差異処理:kIsWeb / Platform.isAndroid 条件付きインポートと適応
- ShopApp:Web版レスポンシブECフロントエンド + Desktop版管理ダッシュボード
2. モバイル専用から広がるストーリー
(1) 悩み:モバイルユーザー成長の頭打ち
BobのShopAppモバイルDAU成長率が月50%から5%に低下。市場データによると:EC注文の40%がデスクトップから(平均注文単価は2倍)、Web SEOは無料トラフィックをもたらします。しかしBobはFlutterコードベースが1つだけ — Web/Desktopをゼロから構築するには最低3ヶ月かかります。
(2) Flutterマルチプラットフォームソリューション
Flutterは同じコードベースをWebとDesktopにコンパイル — 必要なのはレイアウト適応とプラットフォーム固有機能の処理だけです。
BASH
# Webビルド
flutter build web --wasm
# Windowsビルド
flutter build windows
# macOSビルド
flutter build macos
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(3) 成果:追加開発ゼロ + 3日でWeb版ローンチ
Bobはレイアウト適応に3日間だけ費やし、Web版をローンチ。Desktop管理ダッシュボードはモバイルコードの80%を再利用し、2週間で完成しました。
3. マルチプラットフォームデプロイアーキテクチャ
graph TD
SC[Single Codebase] --> WEB[Flutter Web]
SC --> DSK[Desktop]
WEB --> WASM[WASM / CanvasKit]
WEB --> SEO2[URL Strategy / SEO]
DSK --> WIN[Windows]
DSK --> MAC[macOS]
DSK --> LNX[Linux]
DSK --> WM[window_manager]
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(1) プラットフォーム差異概要
| 機能 | モバイル | Web | Desktop |
|---|---|---|---|
| 入力 | タッチ | マウス + キーボード | マウス + キーボード |
| 画面 | 固定方向 | 可変サイズ | リサイズ可能ウィンドウ |
| ファイルシステム | サンドボックス | なし | フルアクセス |
| プッシュ通知 | FCM/APNs | Notification API | ネイティブサポートなし |
| 解像度 | 1x-4x | CSSピクセル | 高DPI |
4. Flutter Webビルドと最適化
(1) Webレンダリングエンジン比較
| エンジン | 初回読み込み | 描画パフォーマンス | 互換性 | 推奨ユースケース |
|---|---|---|---|---|
| CanvasKit | 大きい(2MB+) | 高い(GPU) | 良好 | 本番デフォルト |
| HTMLレンダラー | 小さい | 低い(DOM) | 普通 | 軽量アプリ |
| WASM (Skwasm) | 中程度 | 最高 | Flutter 3.22+ | 将来のデフォルト |
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Webビルド設定
BASH
# CanvasKitでビルド(デフォルト)
flutter build web
# WASMでビルド(Flutter 3.22+、推奨)
flutter build web --wasm
# HTMLレンダラーでビルド
flutter build web --renderer html
# サブディレクトリデプロイ用のカスタムbase-href
flutter build web --base-href "/shop/"
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(2) URL戦略
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:パスURL戦略
DART
import 'package:flutter/material.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
// ShopApp comes from project lib/app.dart
void main() {
// Use path URL strategy (clean URLs: /product/42)
usePathUrlStrategy();
// OR use hash URL strategy (compatibility: /#/product/42)
// useHashUrlStrategy();
runApp(const ShopApp());
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
| 戦略 | URL形式 | サーバー設定 | SEO |
|---|---|---|---|
| パス | /product/42 |
フォールバックルーティングが必要 | 良好 |
| ハッシュ | /#/product/42 |
設定不要 | 不良 |
(3) Web SEO最適化
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:メタタグとセマンティクス
HTML
<!-- web/index.html -->
`<head>`
<meta name="description" content="ShopApp - Million-level products, USD settlement">
<meta name="keywords" content="ecommerce, shop, deals">
<meta property="og:title" content="ShopApp">
<meta property="og:description" content="Cross-platform ecommerce app">
`<title>`ShopApp - Shop the World</title>
</head>
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
💡 ヒント: Flutter WebはSPA — 検索エンジンクローラーのSPAサポートは限定的です。コンテンツ重視のページではSSRソリューション(dart_frogなど)やプリレンダリングを検討してください。
5. レスポンシブレイアウト
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:LayoutBuilderアダプティブブレークポイント
DART
import 'package:flutter/material.dart';
// ResponsiveLayout: three-platform adaptive layout component
class ResponsiveLayout extends StatelessWidget {
final Widget mobile;
final Widget tablet;
final Widget desktop;
const ResponsiveLayout({
super.key,
required this.mobile,
required this.tablet,
required this.desktop,
});
static bool isMobile(BuildContext context) => MediaQuery.sizeOf(context).width < 600;
static bool isTablet(BuildContext context) =>
MediaQuery.sizeOf(context).width >= 600 && MediaQuery.sizeOf(context).width < 1024;
static bool isDesktop(BuildContext context) => MediaQuery.sizeOf(context).width >= 1024;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1024) return desktop;
if (constraints.maxWidth >= 600) return tablet;
return mobile;
},
);
}
}
// Usage
ResponsiveLayout(
mobile: const MobileProductGrid(crossAxisCount: 2),
tablet: const TabletProductGrid(crossAxisCount: 3),
desktop: const DesktopProductLayout(), // Sidebar + grid
)
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:ShopApp Webホームページレイアウト
DART
import 'package:flutter/material.dart';
// CategorySidebar / CartPanel / productCards come from the ShopApp project
class WebHomePage extends StatelessWidget {
const WebHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ShopApp'),
actions: [
// Desktop-only: search bar in AppBar
if (ResponsiveLayout.isDesktop(context))
const SizedBox(width: 300, child: SearchBar()),
IconButton(icon: const Icon(Icons.shopping_cart), onPressed: () {}),
IconButton(icon: const Icon(Icons.person), onPressed: () {}),
],
),
body: ResponsiveLayout(
mobile: _buildMobileLayout(),
tablet: _buildTabletLayout(),
desktop: _buildDesktopLayout(),
),
);
}
Widget _buildDesktopLayout() {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Sidebar categories
SizedBox(
width: 240,
child: CategorySidebar(),
),
// Main content
Expanded(
child: GridView.count(
crossAxisCount: 4,
children: productCards,
),
),
// Cart panel (desktop only)
SizedBox(
width: 300,
child: CartPanel(),
),
],
);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
6. Desktop適応
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:window_managerによるウィンドウ管理
DART
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
// Dependency: window_manager: ^0.3.0 (add to dependencies)
// ShopApp comes from project lib/app.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
WindowOptions windowOptions = const WindowOptions(
size: Size(1200, 800),
minimumSize: Size(800, 600),
center: true,
backgroundColor: Colors.transparent,
skipTaskbar: false,
titleBarStyle: TitleBarStyle.normal,
title: 'ShopApp',
);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
runApp(const ShopApp());
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:キーボードショートカット
DART
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// Dependency: go_router: ^13.0.0 (add to dependencies)
// showSearch / router come from the ShopApp project
class ShortcutWrapper extends StatelessWidget {
final Widget child;
const ShortcutWrapper({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyF): const Intent(#search),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyK): const Intent(#cart),
LogicalKeySet(LogicalKeyboardKey.escape): const Intent(#back),
},
child: Actions(
actions: {
Intent(#search): CallbackAction(onInvoke: (_) => showSearch(context)),
Intent(#cart): CallbackAction(onInvoke: (_) => context.go('/cart')),
Intent(#back): CallbackAction(onInvoke: (_) => Navigator.maybePop(context)),
},
child: child,
),
);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
7. プラットフォーム差異処理
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:条件付きインポートとプラットフォーム検出
DART
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io' show Platform;
// Note: dart:io is unavailable on Web — real projects need conditional imports
// NativePayButton / CreditCardOption / PayPalOption come from the ShopApp project
class PlatformHelper {
static bool get isWeb => kIsWeb;
static bool get isMobile => !kIsWeb && (Platform.isAndroid || Platform.isIOS);
static bool get isDesktop => !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
static bool get isApple => !kIsWeb && (Platform.isIOS || Platform.isMacOS);
static bool get supportsNativePay => isApple || (!kIsWeb && Platform.isAndroid);
}
// Usage in widget
Widget buildPaymentOptions() {
return Column(children: [
if (PlatformHelper.supportsNativePay)
NativePayButton(),
CreditCardOption(),
if (!PlatformHelper.isWeb)
PayPalOption(), // PayPal SDK not available on web
]);
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
8. 完全な例:ShopAppマルチプラットフォームエントリ
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:window_manager/window_manager.dart';
import 'dart:io' show Platform;
// Dependencies: flutter_riverpod: ^2.0.0, firebase_core: ^2.0.0, window_manager: ^0.3.0
// DefaultFirebaseOptions / initHive / PlatformHelper / ShortcutWrapper / router / ShopApp come from the ShopApp project
// themeModeNotifierProvider / shopAppLightTheme / shopAppDarkTheme come from the ShopApp theme module
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Platform-specific initialization
if (PlatformHelper.isDesktop) {
await _initDesktop();
}
// Common initialization
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await initHive();
runApp(ProviderScope(child: const ShopApp()));
}
Future<void> _initDesktop() async {
await windowManager.ensureInitialized();
const windowOptions = WindowOptions(
size: Size(1200, 800),
minimumSize: Size(800, 600),
title: 'ShopApp Admin',
);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
});
}
class ShopApp extends ConsumerWidget {
const ShopApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeNotifierProvider);
return MaterialApp.router(
title: PlatformHelper.isDesktop ? 'ShopApp Admin' : 'ShopApp',
theme: shopAppLightTheme,
darkTheme: shopAppDarkTheme,
themeMode: themeMode,
routerConfig: router,
builder: (context, child) {
// Desktop: add shortcuts
if (PlatformHelper.isDesktop) {
return ShortcutWrapper(child: child!);
}
return child!;
},
);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
❓ よくある質問
Q Flutter WebはSEOサイトに適していますか?
A 本質的には不向きです。Flutter WebはSPA — 検索エンジンクローラーのインデックスが困難です。コンテンツ重視のSEOサイトにはNuxt/Next.jsを使用してください。Flutter Webはツール系アプリケーションに適しています。
Q WASMとCanvasKitの違いは何ですか?
A CanvasKitはJS + WebGLで描画、WASMはC++コンパイル済みWebAssemblyでより高いパフォーマンス。Flutter 3.22+はWASMを推奨しています。
Q DesktopアプリはMac App Storeに公開できますか?
A はい。Apple Developerアカウントが必要で、Xcode ArchiveでDMG/PKGとしてパッケージングし、App Store Connect経由で審査に提出します。
Q WebとMobile間でコードを共有するには?
A 90%のコードは完全に共有可能です。プラットフォーム固有機能(決済/プッシュ/ファイルシステム)のみ条件分岐が必要です。PlatformHelperでプラットフォーム差異をカプセル化してください。
Q レスポンシブレイアウトの最適なブレークポイントは?
A モバイル < 600px、タブレット 600-1024px、デスクトップ > 1024px。Material Designのブレークポイント仕様を参照してください。
Q Desktopアプリのパフォーマンスはネイティブと比べてどうですか?
A Flutter Desktopのパフォーマンスはネイティブに近い(GPU描画)ですが、起動はネイティブより遅い(エンジンの読み込みが必要)。複雑なアニメーションシナリオではElectronより優れています。
📖 まとめ
- Flutter WebはCanvasKitとWASMレンダリングエンジンをサポート、WASMが最高パフォーマンス
- パスURL戦略はSEOに有利だが、サーバーのフォールバックルーティング設定が必要
- LayoutBuilder + ブレークポイントでMobile/Tablet/Desktopの3プラットフォームアダプティブレイアウトを実現
- window_managerがDesktopのウィンドウサイズ、タイトルバー、最小寸法を管理
- kIsWebとPlatform.isXxxでプラットフォームを検出し、プラットフォーム差異を条件付きで処理
📝 練習問題
- 基本 (⭐):
flutter build webでWeb版をビルドし、ブラウザで実行して、パスURL戦略を設定してください。 - 中級 (⭐⭐):ResponsiveLayoutで3プラットフォーム適応を実装してください:モバイル2列グリッド、タブレット3列、デスクトップサイドバー + 4列。
- チャレンジ (⭐⭐⭐):完全なShopAppマルチプラットフォーム版を実装してください:WebレスポンシブECフロントエンド + Desktop管理ダッシュボード(window_manager + キーボードショートカット + メニューバー)、90%のコードを共有。