この記事について
Flutter の入門記事の中には、初学者にもわかりやすく説明しようとするあまり実際のアプリ開発で重要になる内容まで抜け落ちてしまったり、意訳や曲解によって誤解を与えてしまう内容が含まれることもあったりします。
この記事は、Flutter で本格的にアプリ開発を進める上で僕が先に知りたかったこと、詳しい説明がほしかった部分を、過去の自分と同じような Flutter 初学者向けに説明する記事です。
本来は公式ドキュメントを読みながら学習を進められるのが理想ではありますが、Flutter のドキュメントは全て英語であること、またある程度のアプリ開発経験があることを想定した難易度になっている(ように感じる)ことから、まずは日本語の記事を頼りに学習を進める方も多いと思います。この記事は、そんな方の役に立つ内容を目指しています。
この記事では、中でも Flutter の Widget に焦点を当てて書いています。
本文
Everything is a Widget
Flutter には "Everything is a Widget" というスローガンがあります。
これは、UI を構築する全ての要素を Widget
として表現する(つまり、 Widget クラスのサブクラスとして定義する)ことを目指した設計で、 Flutter 最大の特徴の1つに上げられることも多い考え方です。
例えば、画面にテキストを表示する Text
は Widget のサブクラスですし、その周りのパディングを設定する Padding
もやはり Widget です。ほかにも、「この時は Text を出す、この時は出さない」という設定は Visibility
で実現でき、これもやはり Widget です。
宣言的プログラミング
そんな Widget を使って実際に画面を作るソースコードは以下のような書き方をします。
return Visibility(
visible: _isVisible,
child: const Padding(
padding: const EdgeInsets.all(16),
child: Text('Hello, Flutter!'),
),
);
これは、 _isVisible
変数が true
であれば、四方を 16 のパディングで囲まれた Text で 'Hello, Flutter' を画面に表示する 、という内容です。
もし Kotlin や Swift でのアプリ開発経験がある方であれば、画面を作るための考え方が全く違うことに気が付くのではないかと思います。
おそらく同じようなことを Kotlin でしたければ、以下のようなコードになるでしょう。
if (isVisible) {
val text = TextView(context);
text.text = "Hello, Flutter!";
text.setPadding(16, 16, 16, 16);
view.addView(text);
}
(ちょっと Kotlin を忘れ気味なので) 細かなコードの正しさや内容は置いておいて、プログラムの作りが全く異なることがひと目で分かるのではないでしょうか。
Kotlin のコードは、
- もし
isVisible
がtrue
であれば -
TextView
インスタンスを生成して -
text
フィールドに文字列を代入して -
setPadding()
でパディングを設定して - 親の View に追加する
という、手順を踏まえて1行1行処理を進めていく形で UI を構築します。とても「プログラミング的」と言えると思います。
一方で Flutter は、
-
Visibility
を配置する - 表示 / 非表示の判定には
_isVisible
の値を使用する - Visibility の
child
はPadding
である - Padding の値は
padding
プロパティにセットした通りである - Padding の
child
はText
である -
Text
が表示する文字列は "Hello, Flutter!" である
と、まるで設定ファイルを記述するようにコーディングします。少なくとも、1行1行でメソッドを呼び出したりフィールドに値をセットしたり、といった作業ではないことは明らかです。
このようなコーディングスタイルを、「宣言的」(declarative) なプログラミングと呼びます。1行ずつ処理を呼び出したり条件分岐するのではなく、「これを使う」「この値はこうである」という「宣言」であるように読めることからこのような名称がつけられています。1
注意したいのは、これも立派なプログラムである、ということです。
return Visibility();
は Visibility クラスのインスタンスを生成して返却するコードで、そこに引数(プロパティ)の visible
と child
を渡すコードを追加すると
return Visibility(visible: _isVisible, child: Padding(),);
となり、ここに改行を入れると
return Visibility(
visible: _isVisible,
child: Padding(),
);
となります。さらに、Padding も同様に padding
や child
といったプロパティを渡して改行を入れていくと、先ほどのコードのように
return Visibility(
visible: _isVisible,
child: const Padding(
padding: const EdgeInsets.all(16),
child: Text('Hello, Flutter!'),
),
);
というような「宣言的」なコードが完成する、というわけです。
言い換えると、 return するためのインスタンスを生成して、コンストラクタに渡す値もその場でインスタンス生成したもので、さらにそのコンストラクタに渡す値もその場でインスタンス生成したもので、ということを繰り返すと上記のようなコードになりますが、あくまでプログラムです。そのため、以下のような書き方も一応可能です。
final text = Text('Hello, Flutter!');
final padding = const Padding(
padding: const EdgeInsets.all(16),
child: text,
),
return Visibility(
visible: _isVisible,
child: padding,
);
しかし、 child
の中身がコード的に上の行に遡らなければ読めなくなってしまったり、どの Widget とどの Widget がどう関連しているのかがぱっと見で読みづらくなってしまう問題があることから、基本的には最初のコードのようにネストを深くしていく書き方をするのが主流です。2
1画面分のプログラム
では、以上を踏まえて画面を1つ作ってみたいと思います。
import 'package:flutter/material.dart';
void main() => runApp(LoginPage());
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('ログイン画面'),
),
body: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
decoration: InputDecoration(
hintText: 'chooyan@example.com',
),
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: () {},
child: Text('ログインメールを送る'),
),
],
),
),
),
);
}
}
このプログラムを実行すると、↓ のような画面が表示されます。
ソースコードの全てを読み解く必要はありませんが、先述した通り、ネストが深くなっていること、「命令的」ではなく「宣言的」にコーディングされていること、またマテリアルな UI を構築するための MaterialApp
やパディングを設定する Padding
、縦に Widget を並べる Column
といったものが全て Widget で表されてることに注目してみてください。
Widget を構築する Widget
先ほどのログインページを構築するためのコードは build()
メソッドに書いていましたが、この build()
メソッドを持つクラスもまた Widget です。
class LoginPage extends StatelessWidget {
Widget ですので、このクラス自体のインスタンスを先ほど出てきた Padding
の child
プロパティに渡すようなこともできますし、 AppBar
の title
のような、通常 Text を渡すような場所にも渡すことができます。
LoginPage のような StatelessWidget (または StatefulWidget) のサブクラスが Text や Column などと違うのは、それが 「Widget を構築する Widget である」 という点です。公式ドキュメントにも次のように書かれています。
A stateless widget is a widget that describes part of the user interface by building a constellation of other widgets that describe the user interface more concretely.
訳: StatelessWidget は他のより具体的な Widget を集めて UI の一部を表現する Widget です。
「他のより具体的な Widget」とは、 Padding や Container といった子 Widget の配置方法を決める Widget や、 Text や Image のような画面に何かを描画するための Widget を指します。3 4 これらの Widget は RenderObjectWidget
という、描画のための RenderObject
を生成する役割を持ったクラスで、複数の Widget を組み立てる build()
のようなメソッドは持っていません。
一方で StatelessWidget や StatefulWidget は、具体的な何かを描画しない代わりに、 build()
メソッドを実装することで「どの Widget をどのように配置するか」を指定する役割を持っています。そしてそれは Text や Image と違ってアプリごと、画面ごとに様々ですので、われわれアプリ開発者はこの 「Widget を構築する Widget」 をコーディングしていく のが Flutter でアプリ開発する上での主な仕事のひとつになる、というわけです。
もちろん、ひとつの StatelessWidget / StatefulWidget にどれだけのまとまりで Widget を集めるかは設計次第です。 1, 2個の Widget を汎用的に使えるよう配置して再利用するものもあれば、 LoginPage のように 1 ページ丸々記述する場合もあります。
どれだけ長くなったらどのように分割するかは実際にコーディングしながら試行錯誤すると良いでしょう。その点では通常のコーディングでメソッドの分割具合を考える感覚と変わりありません。正解もありません。
まとめ
Flutter は "Everything is a Widget" のスローガンの通り、アプリ開発者は Widget の使い方さえ知っていればアプリの UI が作れてしまうように API がデザインされています。
しかし、その「Widget の使い方」を知るためには、まず「宣言的」に書くことや、 Widget を自作すること、 Widget にもその役割によって種類があること、などを押さえておくことが、 Flutter を「雰囲気で」書くのをやめるためには重要です。
それを理解するための第一歩として、この記事に書いたことが役に立てれば嬉しいです。
なお、 Flutter は 標準搭載されている Widget の種類がとても豊富 なことも特徴のひとつです。もしかしたら、何かの共通パーツとして Text や Container などを駆使して Widget を自作したら、もうすでに同じような(場合によってはよりクオリティの高い) Widget が公式から提供されている、ということも珍しくありません。
すでに用意されている Widget を効率的に使ってより良いアプリを楽に作れるようになることを目指して、 Widget of the Week や Widget Catalogue などを日常的に眺める習慣をつけてみるのも良いでしょう。5
宣伝
"Everything is a Widget" とは言うものの、それはアプリ開発者向けの外向きな方針であって、実際の仕組みはもう少し複雑です。Widget の他にも Element や RenderObject というものがあり、それぞれの Element が親子の参照を保持してツリー構造を形成することで Flutter の仕組みの大部分が実現されていたりと、中身に目を向ければさらに Flutter に詳しくなり、開発や学習の効率も向上します。
そのあたりに興味のある方はぜひ 「【Flutter】Navigator.of(context) から理解する 3つのツリー」 も読んでみてください。少し Flutter に慣れた方向けの記事ですが、じっくり読めば(参照先の公式サイトなども含めて)さらにたくさんの知見を得られるはずです。
-
ちなみに Kotlin のような、1つ1つ処理を呼び出すスタイルは「命令的」(imperative) と呼ばれています。 ↩
-
当然、限度はあります。あまりにネストが深くなる場合は、適宜メソッドや別クラスに切り出したりして読みやすさを調整します。 ↩
-
厳密には、 Text は StatelessWidget、 Image は StatefulWidget で、これらの Widget も同じく「他のより具体的な Widget を集めて UI の一部を表現する Widget」です。 Text は
build()
の中でRichText
という「具体的な Widget」を、 Image は同じくbuild()
の中でRawImage
という「具体的な Widget」をそれぞれ生成して返却しています。 ↩ -
Text について、詳しく知りたい場合は 【Flutter】Text とは何か も読んでみてください。 ↩
-
僕も Zenn の方に 1 つひとつの Widget の仕組みに着目した記事をいくつか書いていますので、何かの参考に読んでみていただければ嬉しいです。 ↩