---
title: "Flutter WidgetsAppとは？MaterialAppとの違いと必須引数・最小構成【Flutter 3.47】"
url: "https://www.issoh.co.jp/tech/details/7297/"
published: 2025-06-18
updated: 2026-11-06
categories: ["Flutter"]
publisher: "株式会社一創"
---

# Flutter WidgetsAppとは？MaterialAppとの違いと必須引数・最小構成【Flutter 3.47】

WidgetsApp は、Flutter アプリの最上位に置くウィジェットのうち、デザイン言語を持たない土台にあたるクラスです。`package:flutter/widgets.dart` に入っていて、画面遷移を管理する Navigator、ロケール、キーボードショートカット、アプリ名の登録といった「どのアプリにも要る部品」をまとめて組み立てます。[公式 API リファレンス](https://api.flutter.dev/flutter/widgets/WidgetsApp-class.html)のとおり、MaterialApp と CupertinoApp もこの WidgetsApp を土台に使っています。

この記事では、WidgetsApp が組み立てるもの、起動に必要な引数、ルーティング、MaterialApp との違い、Material のウィジェットを混ぜたときのエラー、MaterialApp を外すと自作になる部品を順に説明します。ソースの引用は Flutter 3.47.9（2026年10月29日（UTC）公開の安定版・Dart 3.13.7）のタグ、コード例とエラーメッセージは 2026年11月6日に同じ版の SDK（macOS x64）で `flutter analyze` と `flutter test` を通した結果です。

## まとめ：WidgetsAppの役割とMaterialAppとの使い分け

- **役割**：Navigator・Localizations・Shortcuts／Actions・Title・デバッグ用バナーを組み立てる、デザイン非依存のアプリの土台です。StatefulWidget で、MaterialApp と CupertinoApp も内部でこれを使っています。
- **必須の引数**：`color` は必ず渡します。`home` か `routes` を使うなら `pageRouteBuilder` も要ります。
- **MaterialAppとの差**：テーマ、MaterialLocalizations、ScaffoldMessenger、MaterialPageRoute、Hero の制御が入っていません。
- **よく出るエラー**：WidgetsApp の下で AppBar・TextField・`showDialog` を使うと `No MaterialLocalizations found.` で止まります。Scaffold 単体は動きます。
- **自作になるもの**：ボタンの押下表示と読み上げ、画面単位の Semantics、戻る UI、Hero の制御です。
- **使い分け**：Material か Cupertino の部品を1つでも使うなら MaterialApp／CupertinoApp を選びます。WidgetsApp が向くのは、UI をすべて自前のウィジェットで組むアプリです。

## WidgetsAppが組み立てるウィジェットの層

WidgetsApp の `build` メソッドは、外側から順に次のウィジェットを重ねて返します（[3.47.9 の widgets/app.dart](https://github.com/flutter/flutter/blob/3.47.9/packages/flutter/lib/src/widgets/app.dart)）。プロパティの多くは、どれか1つの層の設定値です。

| 層（外側から）              | 担う機能                 | 関係するプロパティ                                      |
| -------------------- | -------------------- | ---------------------------------------------- |
| RootRestorationScope | 状態の復元                | restorationScopeId                             |
| SharedAppData        | アプリ全体の共有値            | なし                                             |
| Shortcuts／Actions    | キー操作と処理の対応           | shortcuts、actions                              |
| FocusTraversalGroup  | Tab キーでのフォーカス移動順     | なし                                             |
| Localizations        | ロケールと文字の向き           | locale、supportedLocales、localizationsDelegates |
| Title                | OS に見せるアプリ名と色        | title、onGenerateTitle、color                    |
| CheckedModeBanner    | 右上の DEBUG 帯（デバッグ時のみ） | debugShowCheckedModeBanner                     |
| DefaultTextStyle     | Text の既定スタイル（指定時のみ）  | textStyle                                      |
| Navigator            | 画面の積み重ねと遷移           | home、routes、onGenerateRoute ほか                 |

MediaQuery は WidgetsApp ではなく外側の View が用意します。`useInheritedMediaQuery` は非推奨で、値は無視されます。

`home` に置いた Builder から祖先の Element をたどると、今回のウィジェットテストでは、テスト用のルート要素も含めて WidgetsApp は 97 段、MaterialApp は 133 段、CupertinoApp は 117 段でした。要素数は画面構成や実行条件で変わります。差の 36 段は MaterialApp が足すテーマ・ScaffoldMessenger などの層です。

WidgetsApp を StatelessWidget と書いた解説もありますが、正しくは StatefulWidget で、ロケールの解決結果や Navigator の GlobalKey を状態に持ちます。2つの違いは[StatelessWidgetとStatefulWidgetの違いと使い分け](/tech/details/4442/)で実測しています。

## 起動に必要な引数：colorとpageRouteBuilderの条件

`color` は WidgetsApp で唯一の `required` 引数です。Title ウィジェットへ不透明にした色で渡され、OS がアプリを一覧表示するときの色に使われます。Android のタスク切り替え画面がこれにあたります。

ほかの引数の組み合わせは、コンストラクタの assert などで検査されます。デバッグ実行で違反すると、次のメッセージで起動時に止まります。

| 違反する組み合わせ                                        | 3.47.9 で出るメッセージ（冒頭）                                |
| ------------------------------------------------ | -------------------------------------------------- |
| builder・onGenerateRoute・pageRouteBuilder がすべて未指定 | `the pageRouteBuilder must be specified`           |
| home と routes の `'/'` を両方指定                      | `the routes table cannot include an entry for "/"` |
| 画面の生成元が1つも無い（※）                                  | `Either the home property must be specified`       |
| ルート指定なしで navigatorKey や initialRoute を渡す         | `If no route is provided using home, routes...`    |
| home と onGenerateInitialRoutes を両方指定             | `the home argument will be redundant`              |

※生成元とは、`home`・`builder`・`onGenerateRoute`・`onUnknownRoute`・`routes` の `'/'` のいずれかです。`routes` に `'/'` が無く、ほかの生成元もすべて未指定の場合に、この assert に当たります。

MaterialApp からの書き換えで当たるのは1行目です。MaterialApp は `pageRouteBuilder` に MaterialPageRoute を作る関数を自動で渡しています。`builder` か `onGenerateRoute` があれば1行目の assert は通りますが、`home` か `routes` から画面を作る時点で別の assert が `pageRouteBuilder` を要求します。不要なのは、`home` と `routes` を使わない構成だけです。

## 最小構成のWidgetsApp：Hello Worldのコード

`package:flutter/widgets.dart` だけで動く最小のアプリです。`pageRouteBuilder` は遷移アニメーションの無い PageRouteBuilder を返しています。

```
import 'package:flutter/widgets.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return WidgetsApp(
      title: 'WidgetsApp Demo',
      color: const Color(0xFF2196F3),
      textStyle: const TextStyle(fontSize: 16, color: Color(0xFF212121)),
      pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
        return PageRouteBuilder<T>(
          settings: settings,
          pageBuilder: (context, animation, secondaryAnimation) => builder(context),
        );
      },
      home: const ColoredBox(
        color: Color(0xFFFFFFFF),
        child: Center(child: Text('Hello, WidgetsApp')),
      ),
    );
  }
}
```

このコードの3つの値は、省くと次の問題が出ます。

- **textStyle**：渡したときだけ DefaultTextStyle が挿入されます。省くと文字の大きさと色は Flutter の初期値のままです。
- **ColoredBox**：WidgetsApp も PageRouteBuilder も背景を塗りません。画面の地の色は自分で用意します。
- **title**：Web では省くと Title ウィジェット自体が挿入されません（`kIsWeb` の分岐）。ブラウザのタブ名を出すなら指定します。

## WidgetsAppのルーティング：routes・onGenerateRoute・onUnknownRoute

### 名前付きルートが解決される順番

WidgetsApp の Navigator は、名前でルートを探すとき次の順に生成元を当たります。

1. `'/'` なら `home`
2. `routes` の表に名前があればその WidgetBuilder
3. `onGenerateRoute`（null を返すと次へ）
4. 最後に `onUnknownRoute`

次の例は、固定のルートを `routes` に、`/items/42` のように値を含むパスを `onGenerateRoute` に、それ以外を `onUnknownRoute` に振り分けます。

```
import 'package:flutter/widgets.dart';

final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

class RoutingApp extends StatelessWidget {
  const RoutingApp({super.key});

  @override
  Widget build(BuildContext context) {
    return WidgetsApp(
      color: const Color(0xFF2196F3),
      navigatorKey: navigatorKey,
      initialRoute: '/',
      pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
        return PageRouteBuilder<T>(
          settings: settings,
          pageBuilder: (context, animation, secondaryAnimation) => builder(context),
          transitionsBuilder: (context, animation, secondaryAnimation, child) =>
              FadeTransition(opacity: animation, child: child),
        );
      },
      routes: <String, WidgetBuilder>{
        '/': (context) => const Page(label: 'home'),
        '/settings': (context) => const Page(label: 'settings'),
      },
      onGenerateRoute: (settings) {
        final uri = Uri.parse(settings.name ?? '');
        if (uri.pathSegments.length == 2 && uri.pathSegments.first == 'items') {
          return PageRouteBuilder<void>(
            settings: settings,
            pageBuilder: (context, a1, a2) => Page(label: 'item ${uri.pathSegments[1]}'),
          );
        }
        return null;
      },
      onUnknownRoute: (settings) => PageRouteBuilder<void>(
        settings: settings,
        pageBuilder: (context, a1, a2) => Page(label: 'not found: ${settings.name}'),
      ),
    );
  }
}

class Page extends StatelessWidget {
  const Page({super.key, required this.label});
  final String label;

  @override
  Widget build(BuildContext context) {
    return ColoredBox(
      color: const Color(0xFFFFFFFF),
      child: Center(child: Text(label, textDirection: TextDirection.ltr)),
    );
  }
}
```

テストで順に push すると「settings」「item 42」「not found: /nope」が表示されました。`routes` の WidgetBuilder は `pageRouteBuilder` で包まれますが、`onGenerateRoute` と `onUnknownRoute` は Route を自分で返すので、遷移の見た目も自分で決めます。

### onUnknownRouteを省いたときの例外

`onUnknownRoute` を渡さずに存在しない名前を push すると、次の例外で止まります。

```
Could not find a generator for route RouteSettings("/nope", null) in the _WidgetsAppState.
...
Unfortunately, onUnknownRoute was not set.
```

起動時の初期ルート（`initialRoute` や Web で直接開いた URL）はこの処理を通りません。解決できない名前だと「Could not navigate to initial route.」を報告したうえで `'/'` の画面で起動します。初期 URL も独自のエラー画面に振り分けるなら、`onGenerateInitialRoutes` か Router 側で処理します。

### builderのcontextとNavigatorの配置関係

`builder` は Navigator の外側を包むための引数です。実測では、`builder` の context から `Navigator.maybeOf` と `Overlay.maybeOf` を呼ぶとどちらも null でした。`builder` の中のボタンから画面遷移するなら、`navigatorKey` を渡して `navigatorKey.currentState!.pushNamed(...)` で呼びます。

`builder` だけで作ると Navigator と Overlay が存在せず、ダイアログも出せません。1画面だけのツールやテスト用の外枠に限った構成です。

### go\_routerとWidgetsApp.routerの接続

Flutter 公式の[Navigation and routing](https://docs.flutter.dev/ui/navigation)は、名前付きルートを「We don’t recommend using named routes for most applications」として多くのアプリには推奨していません。理由はディープリンクの挙動を変えられないことと、ブラウザの「進む」ボタンに対応しないことで、代わりに go\_router などを挙げています。

Router API を使うには別のコンストラクタ `WidgetsApp.router` に `routerConfig` を渡します。[go\_router](https://pub.dev/packages/go%5Frouter) 18.1.3（Flutter 3.44 以上が必要）の GoRouter もそのまま渡せて、`home`・`routes`・`pageRouteBuilder` は引数に存在しません。

注意するのは画面遷移です。`WidgetsApp.router` の下で `GoRoute` の `builder` を使うとページは NoTransitionPage になり、実測では `go('/b')` の次のフレームで切り替わりました。遷移を付けるには次のように `pageBuilder` で CustomTransitionPage を返します。存在しないパスでは go\_router の Page Not Found 画面が出るので、`errorBuilder` で差し替えます。

```
GoRoute(
  path: '/b',
  pageBuilder: (context, state) => CustomTransitionPage<void>(
    key: state.pageKey,
    child: const DetailPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) =>
        FadeTransition(opacity: animation, child: child),
  ),
),
```

## MaterialApp・CupertinoAppとの違い

MaterialApp と CupertinoApp は、自分の `build` の中で WidgetsApp に決まった値を渡しています。渡している値と追加している層を比べると次のとおりです。

| 項目                | WidgetsApp              | MaterialApp                | CupertinoApp            |
| ----------------- | ----------------------- | -------------------------- | ----------------------- |
| 画面遷移              | pageRouteBuilder を自分で指定 | MaterialPageRoute          | CupertinoPageRoute      |
| Text の既定スタイル      | textStyle 指定時のみ         | 警告用の赤字スタイル                 | CupertinoTheme の文字      |
| color の既定         | 必須（既定なし）                | theme.primaryColor → 青     | テーマの primaryColor       |
| ローカライズ            | Widgets のみ              | Material＋Cupertino＋Widgets | Cupertino＋Widgets       |
| テーマ               | なし                      | Theme（切替時アニメーション）          | CupertinoTheme          |
| ScaffoldMessenger | なし                      | あり                         | なし                      |
| Hero の制御          | なし                      | あり                         | あり                      |
| スクロール挙動           | ScrollBehavior          | MaterialScrollBehavior     | CupertinoScrollBehavior |
| 祖先 Element 数（実測）  | 97                      | 133                        | 117                     |

「警告用の赤字スタイル」は、Scaffold や Material の外に Text を置くと出る黄色い二重下線の正体です。WidgetsApp を直接使うアプリでは起きません。

### material.dartの非推奨化とWidgetsAppへの影響

Material と Cupertino の部品は [Flutter 3.47 から別パッケージでも配布](https://docs.flutter.dev/release/breaking-changes/material-ui-and-cupertino-ui)されています。2026年11月6日時点の最新は [material\_ui](https://pub.dev/packages/material%5Fui) 1.10.0 と [cupertino\_ui](https://pub.dev/packages/cupertino%5Fui) 1.1.6 で、どちらも Flutter 3.47.0 以上が必要です。

安定版の 3.47.9 では、SDK 同梱の `package:flutter/material.dart` は非推奨になっていません。次の版のベータである [3.50.0-0.4.pre の material.dart](https://github.com/flutter/flutter/blob/3.50.0-0.4.pre/packages/flutter/lib/material.dart) には `@Deprecated` が付き、`package:material_ui/material_ui.dart` への移行を促しています。`cupertino.dart` にも同様に付いていて、`widgets.dart` には付いていません。同じベータで本記事のコードを確かめると、テストはすべて通り、`flutter analyze` は `material.dart` を import したファイルにだけ非推奨の info を出しました。担当者は[追跡 Issue](https://github.com/flutter/flutter/issues/172942) で、コードの削除は翌年（2027年）に行う見込みとコメントしていますが、削除の版は確認できません。

WidgetsApp だけで組むアプリに、この移行作業はありません。import するのは `widgets.dart` で、material\_ui 1.10.0 の MaterialApp も内部で SDK の WidgetsApp を返しています。Material を使うアプリの移行判断は[Flutter 3.47のmaterial\_ui移行](/tech/details/16894/)にまとめています。

## WidgetsAppの下でMaterialのウィジェットを使うときのエラー

### AppBar・TextField・showDialogで出るNo MaterialLocalizations found

WidgetsApp の Localizations は、既定で Widgets 用のデリゲートしか持っていません。ボタンのツールチップや読み上げ用ラベルを MaterialLocalizations から取る部品は、配置した時点で次の例外を出します。

```
No MaterialLocalizations found.
AppBar widgets require MaterialLocalizations to be provided by a Localizations widget ancestor.
```

3.47.9 で試した結果は次のとおりです。material\_ui 1.10.0 の AppBar でも同じ例外が出ました。

- **例外になる**：AppBar、TextField（Material で包んでも同じ）、`showDialog`（呼び出した時点で同期的に例外）
- **動く**：Scaffold を body だけで置いた場合、widgets 層の `showGeneralDialog`

### Theme未配置時のTheme.ofのフォールバック

例外が出ないせいで気づきにくいのが Theme です。WidgetsApp の下で `Theme.of(context)` を呼ぶと、祖先に Theme が無くても既定の ThemeData が返り、`colorScheme.primary` は #6750A4（Material 3 の基準色の紫）でした。ブランド色のはずの部品が紫で描かれたら、Theme が届いていないことを疑います。

### Materialの部品を混ぜるときの最小の追加

WidgetsApp のまま Material の部品を使う場合は、デリゲート・画面遷移・テーマの3点を足します。次のコードでは例外が出ず、AppBar が描画されました。import を `package:material_ui/material_ui.dart` に替えても同じコードが動きます。

```
import 'package:flutter/material.dart';

class HybridApp extends StatelessWidget {
  const HybridApp({super.key});

  @override
  Widget build(BuildContext context) {
    return WidgetsApp(
      color: const Color(0xFF2196F3),
      localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
        DefaultMaterialLocalizations.delegate,
        DefaultWidgetsLocalizations.delegate,
      ],
      pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) =>
          MaterialPageRoute<T>(settings: settings, builder: builder),
      builder: (context, child) => Theme(
        data: ThemeData(colorSchemeSeed: const Color(0xFF2196F3)),
        child: child!,
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text('Material on WidgetsApp')),
        body: const Center(child: TextField()),
      ),
    );
  }
}
```

これでも SnackBar を出す ScaffoldMessenger や Hero の制御は入っていません。ここまで足すなら MaterialApp に切り替えた方が早く、WidgetsApp を選んだ意味もなくなります。

## MaterialAppを外すと自作になる部品と実装

Material の部品を使わないなら、MaterialApp が肩代わりしていた処理を自分で書きます。先に手を付けるのは、操作に直結するボタンと画面の読み上げです。

### GestureDetectorだけのボタンに足りない読み上げとキーボード操作

GestureDetector に Text を入れただけの部品は、Semantics のタップ操作とラベルは持つものの、ボタンの印（`isButton`）が false でした。押下の表示もキーボードのフォーカスもありません。次の PressButton は、この3つをまとめて補います。

```
import 'package:flutter/widgets.dart';

class PressButton extends StatefulWidget {
  const PressButton({super.key, required this.label, required this.onPressed});
  final String label;
  final VoidCallback onPressed;

  @override
  State<PressButton> createState() => _PressButtonState();
}

class _PressButtonState extends State<PressButton> {
  bool _pressed = false;
  bool _focused = false;

  void _invoke(Intent intent) => widget.onPressed();

  @override
  Widget build(BuildContext context) {
    return Semantics(
      container: true,
      button: true,
      enabled: true,
      child: FocusableActionDetector(
        onShowFocusHighlight: (value) => setState(() => _focused = value),
        actions: <Type, Action<Intent>>{
          ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _invoke),
          ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(onInvoke: _invoke),
        },
        child: GestureDetector(
          behavior: HitTestBehavior.opaque,
          onTapDown: (_) => setState(() => _pressed = true),
          onTapUp: (_) => setState(() => _pressed = false),
          onTapCancel: () => setState(() => _pressed = false),
          onTap: widget.onPressed,
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 100),
            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
            decoration: BoxDecoration(
              color: _pressed ? const Color(0xFF0D47A1) : const Color(0xFF1565C0),
              border: Border.all(
                color: _focused ? const Color(0xFFFFB300) : const Color(0x00000000),
                width: 2,
              ),
            ),
            child: Text(widget.label),
          ),
        ),
      ),
    );
  }
}
```

実測では `isButton` が true になり、Tab でフォーカスしたあと Enter と Space のどちらでも `onPressed` が呼ばれました。WidgetsApp の `defaultShortcuts` がキーを Intent に結び付けているので、受け取る Action だけ書けば足ります。Web の Enter は ActivateIntent でなく ButtonActivateIntent に結び付くので、Action は2つ登録します。1つだけだと、Web と同じ割り当てのテストで Enter が効きませんでした。

- **onTapCancel**：指を押したまま外へ滑らせると `onTapUp` は呼ばれず、`onTapCancel` だけが呼ばれました。省くと押下色のまま残ります。
- **HitTestBehavior.opaque**：既定の `deferToChild` は、子を持たない SizedBox に置くとタップを1回も受けませんでした。余白まで押せる部品では指定します。

### PageRouteBuilderに足すSemanticsのscopesRoute

[MaterialPageRoute の buildPage](https://github.com/flutter/flutter/blob/3.47.9/packages/flutter/lib/src/material/page.dart) は、画面全体を `Semantics(scopesRoute: true, explicitChildNodes: true)` で包んでいます。[scopesRoute](https://github.com/flutter/flutter/blob/3.47.9/packages/flutter/lib/src/semantics/semantics.dart) は、画面名を読み上げる単位の根を示す印です。PageRouteBuilder にはこれがありません。

Semantics ツリーで scopesRoute の付いたノードを数えると、ただの PageRouteBuilder は 0、MaterialPageRoute と次の appRoute は 1 でした。画面ごとに書くと漏れるので、`pageRouteBuilder: appRoute` と渡して1か所に集めます。

```
import 'package:flutter/widgets.dart';

PageRoute<T> appRoute<T>(RouteSettings settings, WidgetBuilder builder) {
  return PageRouteBuilder<T>(
    settings: settings,
    pageBuilder: (context, animation, secondaryAnimation) => Semantics(
      scopesRoute: true,
      explicitChildNodes: true,
      child: builder(context),
    ),
    transitionsBuilder: (context, animation, secondaryAnimation, child) =>
        FadeTransition(opacity: animation, child: child),
  );
}
```

### システムの戻る操作と画面内の戻るUI

Android の戻る操作は WidgetsApp だけで効きます。`handlePopRoute` を呼ぶと Navigator が1画面戻り、`PopScope(canPop: false)` の画面では戻らずに `onPopInvokedWithResult` が `didPop=false` で呼ばれました。

足りないのは画面に見える戻るボタンで、AppBar が無いので自作します。PressButton の `onPressed` に `Navigator.maybePop` をつなぐのが最短です。Esc キーでは画面は戻らなかったので、デスクトップ向けなら Shortcuts と Action を自分で足します。

### HeroアニメーションのHeroController追加

Hero ウィジェットは WidgetsApp でも置けますが、そのままでは飛びません。遷移の途中で「hero」の文字を探すと、制御なしでは遷移元と遷移先の2か所に同時に出ました。HeroController を足すと、2点の間を移動する1つだけになりました。

```
class _AppState extends State<App> {
  final HeroController _heroController = HeroController();

  @override
  void dispose() {
    _heroController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return WidgetsApp(
      color: const Color(0xFF2196F3),
      navigatorObservers: <NavigatorObserver>[_heroController],
      pageRouteBuilder: appRoute,
      routes: routes,
    );
  }
}
```

HeroController は State のフィールドに1回だけ作り、`build` のたびに作り直さないようにします。

### Androidのオーバースクロールとデスクトップのスクロールバー

WidgetsApp のスクロール挙動は素の ScrollBehavior で、Android の ListView には色が #FFFFFF 固定の GlowingOverscrollIndicator が付きました（MaterialApp は StretchingOverscrollIndicator）。macOS では RawScrollbar が付きます。`scrollBehavior` 引数は無いので、ScrollBehavior を継承して `buildOverscrollIndicator` で `child` を返すクラスを `builder` の ScrollConfiguration で差し込むと、グローは消えました。

### 文字入力のEditableText

TextField の代わりになるのは widgets 層の EditableText です。必須の5引数を渡すと、WidgetsApp の下でも例外なく文字列を入力できました。枠線・ラベル・エラー表示は持たないので、フォームが多いアプリでは自作の量が増えます。

## WidgetsAppでのテーマとダークモードの書き方

WidgetsApp には `theme`・`darkTheme`・`themeMode` が無く、見た目に関わる引数は `textStyle` と `color` だけです。端末のダークモードに合わせるには、`builder` の中で `MediaQuery.platformBrightnessOf` を読み、背景と既定の文字色を切り替えます。

```
import 'package:flutter/widgets.dart';

class BrightnessApp extends StatelessWidget {
  const BrightnessApp({super.key, required this.home});
  final Widget home;

  @override
  Widget build(BuildContext context) {
    return WidgetsApp(
      color: const Color(0xFF2196F3),
      pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) =>
          PageRouteBuilder<T>(
            settings: settings,
            pageBuilder: (context, a1, a2) => builder(context),
          ),
      builder: (context, child) {
        final dark = MediaQuery.platformBrightnessOf(context) == Brightness.dark;
        return ColoredBox(
          color: dark ? const Color(0xFF121212) : const Color(0xFFFFFFFF),
          child: DefaultTextStyle(
            style: TextStyle(
              fontSize: 16,
              color: dark ? const Color(0xFFE0E0E0) : const Color(0xFF212121),
            ),
            child: child!,
          ),
        );
      },
      home: home,
    );
  }
}
```

テストで端末をダークに切り替えると、Text の既定色が #212121 から #E0E0E0 に変わりました。

## WidgetsAppで日本語ロケールを設定する方法

`supportedLocales` の既定値は `[Locale('en', 'US')]` で、何も指定しないと `Localizations.localeOf(context)` は en\_US を返します。次の例ではロケールを ja\_JP に固定します。端末の言語設定に追従させる場合は `locale` を省略し、`supportedLocales` に対応言語を並べます。

```
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

class JaApp extends StatelessWidget {
  const JaApp({super.key, required this.home});
  final Widget home;

  @override
  Widget build(BuildContext context) {
    return WidgetsApp(
      color: const Color(0xFF2196F3),
      locale: const Locale('ja', 'JP'),
      supportedLocales: const <Locale>[Locale('ja', 'JP'), Locale('en', 'US')],
      localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
        GlobalWidgetsLocalizations.delegate,
      ],
      pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) =>
          PageRouteBuilder<T>(
            settings: settings,
            pageBuilder: (context, a1, a2) => builder(context),
          ),
      home: home,
    );
  }
}
```

`GlobalWidgetsLocalizations` は `flutter_localizations` パッケージ（SDK 同梱）のデリゲートです。実行前に `flutter pub add flutter_localizations:"{sdk: flutter}"` で依存関係に追加します。実行すると `Localizations.localeOf` は ja\_JP でした。RichText は locale を指定されないとこの値を段落の locale に使うので、漢字が中国語の字形で出る現象への対策にもなります。WidgetsApp だけで組むなら `GlobalMaterialLocalizations` は不要です。

## WidgetsAppを選ぶ場面と選ばない場面

判断の軸は1つで、Material か Cupertino の部品を使うかどうかです。

- **WidgetsApp を選ぶ**：ボタン・入力欄・ダイアログまで自社のデザインシステムで作るアプリ（ゲーム風の UI、店頭端末のキオスク画面、ブランド表現が最優先のアプリ）。パッケージのサンプルやウィジェットテストで、外枠を最小にしたい場合。
- **MaterialApp を選ぶ**：TextField・AppBar・SnackBar・`showDialog` のどれか1つでも使う場合。テーマ切り替えやダークモードの自動追従が欲しい場合。Cupertino の部品だけで組むなら CupertinoApp です。

避けたいのは、「軽そうだから」という理由で WidgetsApp から始め、あとで Material の部品を足すたびにデリゲート・テーマ・ScaffoldMessenger を手で積み直すパターンです。祖先 Element の差は 36 段ありますが、処理時間は測っていないので、軽さは読み上げや操作部品を自作する根拠になりません。部品を自作する計画が無いなら、最初から MaterialApp で始めてください。

WidgetsApp は特定のデザイン言語を提供しないため、自作UIをOSの新しい見た目に合わせるかどうかはアプリ側で判断します。iOS の新しい見た目に合わせたいアプリは、[FlutterのLiquid Glass対応状況](/tech/details/16743/)を先に確かめます。部品の自作量は見積もりを大きく左右するので、外部に開発を頼むなら[Flutter / React Native開発](/service/apps/cross%5Fplatform/)のような窓口に、部品の一覧を添えて相談します。層の切り方は[Flutterアーキテクチャのベストプラクティス](/tech/details/7232/)で整理しています。

## WidgetsAppについてよくある質問

### WidgetsAppとMaterialAppはどちらを使えばよいですか？

Material の部品を1つでも使うなら MaterialApp です。部品をすべて自作するアプリだけが WidgetsApp を選ぶ理由を持ちます。今回の祖先 Element 数の差は36段ですが、処理時間やメモリ使用量は測定していません。性能を理由に選ぶ場合は、対象画面で別途比較が必要です。

### WidgetsAppの中でScaffoldは使えますか？

Scaffold を body だけで置く分には動きます。AppBar や TextField を入れると `No MaterialLocalizations found.` で止まるので、`localizationsDelegates` に `DefaultMaterialLocalizations.delegate`（日本語なら `GlobalMaterialLocalizations.delegate`）と `DefaultWidgetsLocalizations.delegate` を渡します。日本語で表示する場合は、`supportedLocales` にも日本語を登録します。

### WidgetsAppのcolorは画面のどこに使われますか？

アプリ内の画面には使われません。Title ウィジェットに渡され、OS がアプリを一覧表示するとき（Android のタスク切り替え画面など）の色になります。画面の背景色は ColoredBox などで自分で塗ります。

### WidgetsAppでnavigatorKeyを使って画面遷移できますか？

できます。`GlobalKey<NavigatorState>` を `navigatorKey` に渡せば、`builder` の中や context を持たないクラスから `navigatorKey.currentState!.pushNamed('/settings')` で遷移できます。`builder` の context で `Navigator.of` を呼んでも Navigator は見つかりません。

### WidgetsAppでThemeExtensionやthemeModeは使えますか？

WidgetsApp 自体には `theme` も `themeMode` もありません。ThemeExtension は ThemeData の機能なので、`builder` で Theme ウィジェットを挿入して ThemeData の `extensions` に渡せば `Theme.of(context).extension<T>()` で読めます。ダークモードの切り替えは `MediaQuery.platformBrightnessOf` を見て自分で行います。

## 関連記事

- [StatelessWidgetとStatefulWidgetの違いと使い分け｜Flutter 3.47で実測した判定基準](/tech/details/4442/)
- [Flutter 3.47とは？material\_ui移行・iOS 15と3.47.7までの修正【2026年10月時点】](/tech/details/16894/)
- [FlutterのLiquid Glass対応状況｜公式実装は作業中、今使えるパッケージと選び方](/tech/details/16743/)
- [Material Design 3（Material 3）とは？使い方・仕様の数値とExpressiveの実装別対応](/tech/details/6886/)
- [Flutterアーキテクチャのベストプラクティス｜公式ガイドのレイヤー設計・ディレクトリ構成・設計パターン【Flutter 3.47対応】](/tech/details/7232/)

---

出典: [Flutter WidgetsAppとは？MaterialAppとの違いと必須引数・最小構成【Flutter 3.47】](<https://www.issoh.co.jp/tech/details/7297/>)（株式会社一創）
