Closed5

FlutterのScaffoldをいつ使うべきか調査

johnnymanjohnnyman

Flutterの開発指南をしている時に「Widgetを作成する時はいつもScaffoldを使うべきなのか?」と質問されて回答に困ったので、調べたことを適当にまとめて行きます。

まず、flutter createで生成されたコードにも使用されるほどにページの根幹をなすのがScaffoldである。

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

johnnymanjohnnyman

Scaffoldの実装を見てみると、ただのStatefulWidgetを継承したWidgetであることがわかります。


class Scaffold extends StatefulWidget {
  /// Creates a visual scaffold for Material Design widgets.
  const Scaffold({
    super.key,
    this.appBar,
    this.body,
    this.floatingActionButton,
    this.floatingActionButtonLocation,
    this.floatingActionButtonAnimator,
    this.persistentFooterButtons,
    this.persistentFooterAlignment = AlignmentDirectional.centerEnd,
    this.drawer,
    this.onDrawerChanged,
    this.endDrawer,
    this.onEndDrawerChanged,
    this.bottomNavigationBar,
    this.bottomSheet,
    this.backgroundColor,
    this.resizeToAvoidBottomInset,
    this.primary = true,
    this.drawerDragStartBehavior = DragStartBehavior.start,
    this.extendBody = false,
    this.extendBodyBehindAppBar = false,
    this.drawerScrimColor,
    this.drawerEdgeDragWidth,
    this.drawerEnableOpenDragGesture = true,
    this.endDrawerEnableOpenDragGesture = true,
    this.restorationId,
  });

ScaffoldがコントロールしているWidgetは、

  • appBar
  • body
  • floatingActionButton
  • persistentFooterButtons
  • drawer
  • endDrawer
  • bottomNavigationBar
  • bottomSheet

で、それぞれnullであればビルドされないので、Scaffoldを使ったからと言ってレンダリングコストが高くなるわけではなさそう。

johnnymanjohnnyman

ScaffoldクラスのTroubleshootingの項の説明を参照。
https://api.flutter.dev/flutter/material/Scaffold-class.html

The Scaffold is designed to be a top level container for a MaterialApp. This means that adding a Scaffold to each route on a Material app will provide the app with Material's basic visual layout structure.

It is typically not necessary to nest Scaffolds. For example, in a tabbed UI, where the bottomNavigationBar is a TabBar and the body is a TabBarView, you might be tempted to make each tab bar view a scaffold with a differently titled AppBar. Rather, it would be better to add a listener to the TabController that updates the AppBar

ScaffoldはMaterialAppに対してトップレベルのコンテナであるという記述があり、さらに通常はネストする必要がないと書かれています。

Although there are some use cases, like a presentation app that shows embedded flutter content, where nested scaffolds are appropriate, it's best to avoid nesting scaffolds.

embedded flutter contentを表示するようなユースケース以外は、ネストは避けた方が良いということでした。

このembedded flutter contentとは何なのだろうか。また、なぜ避けるべきなのか。

johnnymanjohnnyman

再び、Scaffoldクラスの実装に立ち戻ってみる。
一部のWidgetのハンドリングにおいて以下のScaffold.of()メソッドが使用されており、これはexpensiveであるとのこと。
おそらく、Widgetツリー(Elementツリーの方が正しいはずだが)を辿ってScaffoldStateを持つWidgetを探しにいくのだろうか。


  static ScaffoldState of(BuildContext context) {
    final ScaffoldState? result =
        context.findAncestorStateOfType<ScaffoldState>();
    if (result != null) {
      return result;
    }
    throw FlutterError.fromParts(/* 省略 */);
  }

つまり、ネストしたScaffoldがいる場合は直近のScaffoldに対して各Widgetの表示・非表示を制御してしまうため、実装者の意図した挙動にならないということであろう。

SnackBarを表示するために使用するScaffoldMessengerもScaffoldStateを参照しているのだが、意図しないレイヤーにScaffoldが差し込まれていたら、変なところにSnackBarが表示されてしまうことになる。

以下の画像は、Scaffoldをネストさせて雑に色々なWidgetを詰め込んだものである。

johnnymanjohnnyman

結論としては、

  • Scaffoldをネストさせることが通常はBad Practiceである
  • Scaffoldをネストさせることで、実装者の意図しない挙動になってしまう
    • ただし、これは動作確認またはレビューで回避できるレベルのものなのでそこまで致命的ではない
    • MaterialAppのトップのみに制限しなくてもルーティングのトップ、ページのトップのみなどとルールを設定しておけば問題にはならなそう
このスクラップは2023/11/12にクローズされました