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. main と runApp ー アプリ起動の「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のウィジェットシステムを起動する関数」
とそれぞれ用途が違うからです。
main と runApp を分けておくと、例えば...
- 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を再描画することができます。
- setStateなどで状態が変わると、そのウィジェットのサブツリーが「dirty(再描画必要)」としてマークされる
- 次のフレームでdirtyな部分のみを再ビルド
- 新しいウィジェットツリーと以前のツリーを比較、差分のみを下層のレンダーツリーに適用
この『差分更新』ができるおかげで、宣言的なUIと高いパフォーマンスを維持しています!
差分更新って具体的に何をしているの?
例えばカウンターアプリで、ボタンを押して_counterが1→2になったとします。
-
setStateで「このStateの下のツリーを作り直して」とマーク -
_MyHomePageState.buildが再実行される - 新しいWidgetツリーができる(
Text('1')→Text('2')に変更) - Flutterが「前のWidgetツリー」と「新しいWidgetツリー」を比較
- 中身が変わった箇所(
Text('$_counter')のノード)に対応 - 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