Flutter: WebとDesktopマルチプラットフォームデプロイ

一度書けば、どこでも動く — WebとDesktopがあなたのコードをモバイルから世界へ届けます。

📋 前提条件: 以下を先にマスターしている必要があります

1. このレッスンで学ぶこと


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. マルチプラットフォームデプロイアーキテクチャ

100%
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より優れています。

📖 まとめ


📝 練習問題

  1. 基本 (⭐)flutter build webでWeb版をビルドし、ブラウザで実行して、パスURL戦略を設定してください。
  2. 中級 (⭐⭐):ResponsiveLayoutで3プラットフォーム適応を実装してください:モバイル2列グリッド、タブレット3列、デスクトップサイドバー + 4列。
  3. チャレンジ (⭐⭐⭐):完全なShopAppマルチプラットフォーム版を実装してください:WebレスポンシブECフロントエンド + Desktop管理ダッシュボード(window_manager + キーボードショートカット + メニューバー)、90%のコードを共有。

← 前のレッスン | 次のレッスン →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%