📱

main.dart から理解する「Flutterの基礎」|mainとrunAppの違い

に公開

初めに

皆さん、お疲れさマッチョです💪
エンジニアをしています "のぞみん" です。

今回は、flutter create 直後に作成される lib/main.dart を基に、Flutter開発の基礎と「なぜそうなってんの...?」までを幾つかのパートに分解して説明していきたいと思います!


本記事では、main・runAppの違いやFlutterにおけるツリー構造についてまとめています。

これからFlutterを使用する方や、実際にFlutterで開発してるけどいまいち理解しきれてない...といった方は、是非最後まで読んでいただければと思います☺️

前提:典型的な main.dart はこうなっている

Flutter 3 系で新規プロジェクトを作ると、おおよそ次のようなコードが生成されます(細部はバージョンによって多少異なります)。

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text('$_counter'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

この1ファイルを読み解くことで、FLutterの基礎概念の大体を理解することができます...!

1. なぜ material.dart を読むのか?

main.dart の1行目に import文( import 'package:flutter/material.dart'; )があるかと思います。
このimport文について簡単にまとめると以下の通りとなります。

  • package:flutter/material.dart は、Material Design ベースの UI を作るためのウィジェット群をまとめたライブラリ
  • MaterialApp, Scaffold, AppBar, FloatingActionButton など、サンプルアプリで登場するコンポーネントがすべてこのライブラリから提供される

では、なぜこの material.dart が初期生成時点で読み込まれているのでしょうか?
それは、以下の通りとなります。

  • Dart のライブラリ仕様上、「そこに定義されているクラスや関数を使いたいなら import しないといけない」
  • Flutter の Material デザイン用コンポーネント(MaterialApp, Scaffold, AppBar, FloatingActionButton, ThemeDataなど)が 全部この materialライブラリの中に入っている
  • material.dart 自体が「Materialデザイン一式をまとめてエクスポートしている窓口ファイル」になっている

2. mainrunApp ー アプリ起動の「2段階」

Dartのエントリポイント main()

Dartで書かれたプログラムは、必ずトップレベルの main()から実行されるようになっています。
これは、Dart VMが「ここから実行を始めるよ〜」と決めている言わば"入り口"です。

つまり...
Flutterアプリであっても「ただのDartプリグラム」なので、 main() がないとそもそも起動しないんですねぇ。

runApp() は何をしているのか?

main() の中では、必ずと言って良いほど runApp() を呼ぶようになっています。

void main() {
  runApp(const MyApp());
}

公式ドキュメントでは、この runApp() について次のような説明になっています。

  • 渡されたウィジェット(上記ではMyApp)を「inflate(展開)」
  • 展開したウィジェットを「ビューにアタッチ」する
  • ウィジェットツリーのルートとして扱う

ここだけ見ると「???????」だと思います...笑

もうちょっと平たく言うと...
「このウィジェットをアプリ全体の根っこにして、画面全体を描き始めてね!」
といった命令をフレームワークに投げている、というイメージです。

なぜ main()runApp() が分かれるのか?

結論から言うと、
main はあくまでDartアプリの入口で、UIフレームワークには依存しない」
runApp はFlutterのウィジェットシステムを起動する関数」
とそれぞれ用途が違うからです。

mainrunApp を分けておくと、例えば...

  • runApp 前に環境変数を読む
  • 設定ファイルやリモートコンフィグを読み込む
  • DIコンテナのセットアップを行う
  • Firebase初期化などの非同期処理を待つ

といった
『「アプリ共通の初期化処理」をmain側で自由に行った上で、準備が整ったらUIを立ち上げる』
という構成がとれます。

3. Flutterの根本思想:全てはウィジェット、UIはツリー

「全てはウィジェット」の意味

Flutterでは、次のようなものは全部ウィジェットです。

  • テキスト表示(Text
  • ボタン(ElevatedButton など)
  • レイアウト(Column, Row, Center など)
  • 画面全体(Scaffold
  • なんならアプリ全体(MaterialApp / MyApp

HTMLで言うと、"全てがタグ" みたいなイメージです☺️

ウィジェットツリーとは?

ウィジェットは親子関係を持って入れ子になり、「ツリー構造」を作ります。これをウィジェットツリーと言います。

カウンターアプリの一部をツリーとして眺めると...

MyApp
 └─ MaterialApp
     └─ MyHomePage
         └─ Scaffold
             ├─ AppBar
             │   └─ Text(Widget.title)
             ├─ body: Center
             │   └─ Column
             │       ├─ Text('You have pushed...')
             │       └─ Text('$_counter')
             └─ floatingActionButton
                 └─ FloatingActionButton

なぜツリー構造なのか?

ツリー構造にしておくことで、Flutterは効率よくUIを再描画することができます。

  1. setStateなどで状態が変わると、そのウィジェットのサブツリーが「dirty(再描画必要)」としてマークされる
  2. 次のフレームでdirtyな部分のみを再ビルド
  3. 新しいウィジェットツリーと以前のツリーを比較、差分のみを下層のレンダーツリーに適用

この『差分更新』ができるおかげで、宣言的なUIと高いパフォーマンスを維持しています!

差分更新って具体的に何をしているの?

例えばカウンターアプリで、ボタンを押して_counterが1→2になったとします。

  1. setState で「このStateの下のツリーを作り直して」とマーク
  2. _MyHomePageState.build が再実行される
  3. 新しいWidgetツリーができる(Text('1')Text('2')に変更)
  4. Flutterが「前のWidgetツリー」と「新しいWidgetツリー」を比較
  5. 中身が変わった箇所(Text('$_counter')のノード)に対応
  6. Element/RenderObjectのみ更新して、再レイアウト・再描画

ポイントとして...

  • コード上は「全部書き直しているように見える」
  • 実際内部では、「前と同じ構造の箇所はそのまま再利用」「変わった部分だけ差し替え」

という『差分更新』が行われている、ということです。

まとめ

  • material.dartは、MaterialDesignベースのUIを作るための、ウィジェット群をまとめたライブラリであり、MaterialApp, Scaffold, AppBar, FloatingActionButtonなどのコンポーネントがこのライブラリから提供される
  • main()はDartのエントリポイントであり、Dart VMが「ここから実行を始めるよ」と決めている言わば"入り口"である
  • runApp()は、「このウィジェットをアプリ全体の根っこにして、画面全体を描き始めてください」といった命令をフレームワーク自体に投げている
  • main()runApp()が分割している理由は以下の通りである
    • main はあくまでDartアプリの入口で、UIフレームワークには依存しない
    • runApp はFlutterのウィジェットシステムを起動する関数
  • Flutterのウィジェットは親子関係を持って入れ子になり、「ツリー構造」を構築する。これを ウィジェットツリー と呼ぶ
  • ツリー構造とすることで、『差分更新』により宣言的UIと高いパフォーマンスの維持が可能

Discussion