Flutter

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

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

WidgetsApp は、Flutter アプリの最上位に置くウィジェットのうち、デザイン言語を持たない土台にあたるクラスです。package:flutter/widgets.dart に入っていて、画面遷移を管理する Navigator、ロケール、キーボードショートカット、アプリ名の登録といった「どのアプリにも要る部品」をまとめて組み立てます。公式 API リファレンスのとおり、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)。プロパティの多くは、どれか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の違いと使い分けで実測しています。

起動に必要な引数: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は、名前付きルートを「We don’t recommend using named routes for most applications」として多くのアプリには推奨していません。理由はディープリンクの挙動を変えられないことと、ブラウザの「進む」ボタンに対応しないことで、代わりに go_router などを挙げています。

Router API を使うには別のコンストラクタ WidgetsApp.router に routerConfig を渡します。go_router 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 から別パッケージでも配布されています。2026年11月6日時点の最新は material_ui 1.10.0 と cupertino_ui 1.1.6 で、どちらも Flutter 3.47.0 以上が必要です。

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

WidgetsApp だけで組むアプリに、この移行作業はありません。import するのは widgets.dart で、material_ui 1.10.0 の MaterialApp も内部で SDK の WidgetsApp を返しています。Material を使うアプリの移行判断はFlutter 3.47のmaterial_ui移行にまとめています。

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 は、画面全体を Semantics(scopesRoute: true, explicitChildNodes: true) で包んでいます。scopesRoute は、画面名を読み上げる単位の根を示す印です。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対応状況を先に確かめます。部品の自作量は見積もりを大きく左右するので、外部に開発を頼むならFlutter / React Native開発のような窓口に、部品の一覧を添えて相談します。層の切り方はFlutterアーキテクチャのベストプラクティスで整理しています。

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 を見て自分で行います。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

RELATED POSTS 関連記事

目次