flutter create
で新しく Flutter プロジェクトを作ると自動生成されるのが「カウンターアプリ」であることは、Flutter を触ったことがある方であればピンとくるのではないかと思います。
Flutter アプリ開発者にとっての初めの一歩とも言うべきカウンターアプリですが、その main.dart
に書かれているコメントを読んだことがあるでしょうか。私は毎回全置換で消しています。
ただ改めて考えてみると、このコメントも Flutter フレームワークの開発チームが Flutter アプリ開発者のためにわざわざ書いてくれたもの だと思いますので、この記事ではそのコメント全てを翻訳しながらしっかりと内容を確認してみたいと思います。
また、初学者向けの簡単な解説も交えつつ書いていければと思いますので、何かの役に立てれば嬉しいです。
ではどうぞ。
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
まずは MyApp
クラスの宣言の下、8 行目に記載された内容です。
(意訳) この Widget はアプリケーションの根っこです。
Flutter では、 Widget をツリー構造で下へ下へとつなげていくことで UI を構築します。カウンターアプリにおける MyApp
という Widget はそのツリー構造のまさに開始地点(根っこ)であるということがここに記載されています。
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
MyApp
の build()
メソッドの中、 14 行目からのコメントには以下のようなことが書かれています。
(意訳) これはアプリケーションのテーマです。アプリケーションを "flutter run" コマンドで実行してみてください。青いツールバーが表示されるはずです。では、アプリは起動したまま、下に書かれた primarySwatch の値を Colors.green に変更して "ホットリロード" を実行してみてください(コンソールから "flutter run" で実行している場合は "r" キーを押します。 Flutter に対応した IDE で実行している場合は単純にファイルを保存すればホットリロードが実行されます)。カウンターがゼロに戻ることなく、つまりアプリがリスタートすることはありません。
ここでは ThemeData
がアプリ全体のテーマ(色など)を保持するオブジェクトであることと "ホットリロード" の説明がされています。
ホットリロードはご存知の通り、 Flutter の目玉とも言える機能です。ここで指示された通りに primarySwatch
の値を Colors.blue
から Colors.green
に変えてファイルを保存するだけで、アプリを再起動することなく UI が変化することが体験できるようになっています。
Flutter フレームワークの開発チームとしても、まず初めにこのホットリロードを体験してほしい、ということなのだろうと推測できます。
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
次はさらに下に下がったところ、 33 行目から始まるコメントです。
(訳) この Widget はアプリケーションのホームページです。これは stateful、つまり State という(この下に定義された)オブジェクトを持っており、State には見た目を変化させるためのフィールドが定義されています。
この MyHomePage という Widget クラスは、その State の設定値を保持するクラスです。このクラスは親(つまり MyApp) から受け取った値(ここでは title)を保持していて、この title は State の build() メソッドで使われています。 Widget のサブクラスのフィールドは常に "final" でなければなりません。
ここは Flutter で動的に変化する UI を構築するためのひとつの手段である StatefulWidget
の説明がされています。
StatefulWidget
は Widget が設定値を保持し、 State
の build()
でそれを使うことで、例えばここでは親である MyHomePage
から受け取ったタイトル文字列が AppBar
に表示されるようになっています。
Widget に定義するフィールドは常に final
でなければならないという点も重要です。 Widget は「不変な」(immutable)クラスであるため、保持するフィールドの値を変えることはできません。 Widget は「不変な値(設定値)を保持するオブジェクト」としての役割に徹する ことで Flutter フレームワークは Widget オブジェクトの(オブジェクト生成後の動的な)変化を気にする必要がなくなり、それをスムーズな UI 描画のための最適化に利用しています。
一方で、ユーザーの操作などによってフィールドの値を動的に変化させたい場合はその値を State
に保持 させます。State
は Widget
とは違い、 一度生成したオブジェクトをなるべく使い回す ために設計されたクラスです。アプリの操作に合わせて UI を変化させるためのデータもこの State
オブジェクトが保持するのが Flutter における基本的なやり方です。1
さて、先に進みます。
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
これは先ほど出てきた State
クラスのコードです。 StatefulWidget
には必ずペアになる State
が存在し、 MyHomePage
という Widget のペアになるのがこの _MyHomePageState
である、というわけです。
さて、 53 行目から始まるコメントを確認します。
(意訳) この setState を呼び出すことで、 Flutter フレームワークに対して State が保持する何らかの値が変化したことを伝えます。これにより、下に定義した build() メソッドが再度呼び出され、変更後の値が画面上の表示内容に反映されます。もし _counter の値を setState() を呼び出すことなく変化させた場合、 build() メソッドが呼び出されずに UI も何も変化しません。
State
が保持する値を変更する場合、必ず setState()
をセットで呼び出さなければならない(正確には、 値を変化させる関数を setState()
メソッドの引数に渡す)ことが記載されています。
Flutter フレームワークは State
のフィールドの値が変化したこと自体を検知することはできません。 setState()
が呼ばれることによって初めて「値が変わって UI の更新が必要になった」と判断し、 build()
をもう一度呼び出すことで画面を更新します。
さて、次はその build()
メソッドの中のコメント、 64 行目です。
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
(意訳) このメソッドは setState が呼び出されるたびに実行され直します。例えば、上に定義された _incrementCounter メソッドの実行が完了された後などです。
フラッターフレームワークはこの build メソッドが高速に呼び出されるように最適化されています。つまり、Flutter アプリの開発者は UI の更新のために個別の Widget に対して何かの更新操作を行うのではなく、必要なタイミングでこの build メソッドを呼び出して全体をリビルドできるようになっています。
これは Kotlin や Swift といったいわゆる「ネイティブの」アプリ開発を経験した方にとっては見慣れない考え方なのではないかと思います。
Flutter では、一度生成した「テキスト」や「画像」などを表すオブジェクトに対して「表示内容を変更する」ようなメソッドを呼び出すことはしません。
替わりに、 build()
メソッドを呼び出し直して Widget オブジェクトそのものを全て取り替える ことで画面を更新する作りになっています。 その際、 Widget の構築に利用しているフィールドの値が変わっていれば build()
メソッドのロジックの実装に従って生成される Widget オブジェクトも変化し、結果として UI が変化する 、という考え方です。
ここで勘違いしてはいけないのは、 Widget オブジェクト自身はレイアウト計算や UI の描画を担当するオブジェクトではないということです。先ほどのコメントで書いてあった通り、Widget は「設定値」を保持するだけのオブジェクトですので、画面全体の Widget オブジェクトを再生成したからといって画面全体が再描画されるわけではありません。ここが Kotlin や Swift の View
というものとは違う点です。
再生成された Widget と古い Widget を比較し、 UI の変更が必要な最低限の部分だけを再描画する 、という最適化を Flutter フレームワークは行っています。つまり、アプリ開発者はとにかく新しい値を保持した Widget オブジェクトを生成すれば、あとは必要なものだけを Flutter フレームワークが判断して使ってくれる、という考え方で build()
メソッドを実装します。
次は 72 行目です。
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title)
(意訳) ここで、 MyApp の build() メソッドの中で生成され、 MyHomePage オブジェクトを経由して受け取った文字列を使って AppBar のタイトルをセットしています。
Widget が保持する値を State で利用する場合は、この例のように widget.xxx
でアクセス可能です。ここでは、 MyApp
から受け取った "Flutter Demo Home Page" という文字列が widget.title
に格納されていて、それが AppBar
の中に表示されるタイトルとして利用されるわけです。
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
(意訳) Center はレイアウトのための Widget です。子となる Widget をひとつだけ受け取り、親に対して中央寄せになるように配置します。
Flutter では「テキスト」や「画像」といった表示物だけでなく、それを配置するためのレイアウトの設定を保持するのも Widget の役割です。 Center
はそのようなレイアウト用の Widget のひとつであることが説明されています。
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
(意訳) Column もレイアウトのための Widget です。Column は子のリストを受け取り、それを縦に並べます。デフォルトでは、子の幅がちょうど収まるだけの幅と、親と同じだけの高さを確保します。
"debug painting" を実行することで(コンソールでは "p" キーを押します。Android Studio の場合は Flutter インスペクタの "Toggle Debug Paint" アクションを選択してください。Visual Studio の場合は"Toggle Debug Paint" コマンドを選択します) それぞれの子Widget のワイヤーフレームを確認できます。
Column にはどのように自身のサイズを決定し、どのように子を配置するかを設定する多くのプロパティが用意されています。ここでは、 mainAxisAlignment を指定することで子を縦方向で中心寄せで配置しています。なお、 "main axis" とは、 Column においては縦方向の軸を意味します。(また、 "cross axis" は横方向です)
先ほどの Center
は子を1つだけ受け取る Widget だったのに対し、 Column
は複数の子 Widget を受け取る Widget で、それを縦に並べる役割を持っています。 main axis
と cross axis
の用語は UI を構築する上で頻繁に出てくるので覚えておくとよいでしょう。
children
を縦に並べる Column
においては、 main
が縦、 cross
が横方向となりますが、 children
を横に並べる Row
においては、 main
は横、 cross
が縦方向となります。
ここでは、Flutter の "Debug Painting" 機能についても説明されています。実際にコメントの指示通りに実行してみると、以下のように子要素の範囲や Column
自身の幅、高さが画面上に可視化され、これを元にレイアウトを調整できるようになっているというわけです。
さて、次で最後のコメントです。
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods
(意訳) この末尾のカンマは build メソッドの自動フォーマットを良い感じにするためのものです。
Flutter は Widget をツリー構造で構築する仕組みになっている都合上、 build()
メソッドは Widget の入れ子になることが多いです。つまり、ソースコード的にネストがどんどん深くなっていきます。
そのネストをきれいな階段上にフォーマットするための目印として、標準で用意されているコードフォーマッタはカッコの後ろのカンマを手がかりにします。そのため、基本的にはきれいにフォーマットするためにカンマはつけた方が良い旨が書かれています。
なお、このネストが深くなることに対して違和感を感じるアプリ開発者も多いようですが、考えようによっては Web ページの HTML も Android ネイティブの XML も、 UI を構築するための記述はネストすることが多いですし、また ネストしているからこそ各部分の記述がページの大枠を示すものなのか、それとも細かな一部のパーツを表すものなのかがネストの深さから推測しやすい 、というメリットがあったりします。
このあたりは慣れの問題もあると思いますが、とにかく Flutter ではネストするのが基本で、それをきれいに階段上に整形するためにカンマをつけるのは重要である、ということです。
以上です。
自動生成されたカウンターアプリのコードは Flutter の初学者も読む関係でここに書かれたコメントは Flutter の特徴を手っ取り早く掴むためにかなり厳選された内容になっています。
特にわれわれ日本人にとっては無視されがちな英語のコメントではあると思いますが、一方でこのコメントに従って手を動かしてみることで、効率よく Flutter の仕組みをざっと把握し、また用意されたツールや仕組みを体験できる内容になっていることがわかったのではないかと思います。
すでに何かしらの記事やドキュメントを見ながら Flutter を触っている方も多いと思いますが、一度スタート地点に戻ってここに書かれた内容を追ってみてはいかがでしょうか。
-
他のやり方もいろいろと存在し、それは「状態管理」という名称で説明されています。 ↩