Flutterで画像・SVG・アイコンを管理する(flutter_gen最小構成) の次に手が止まりやすいのは、UI より Dart の記法です。Flutter のサンプルには final const ? required async ... が並びます。この記事では、その中でも最初の 1 本を書く前に押さえたい 5 つだけを Flutter の文脈で整理します。
1. この記事の位置づけ
対象読者
- Flutter の開発環境は整ったが、Dart の書き方で手が止まりやすい人
- JavaScript / Java / C# などの経験はあるが、Dart はまだ読み慣れていない人
- Flutter の公式サンプルや入門記事を読み始めた段階の人
この記事で到達する状態
finalとconstを場面で選び分けられる- nullable な値を
?と!で読み分けられる - Widget の constructor を named parameters 前提で追える
Future/async/awaitの最小の流れを読めるbuildメソッド内の collectionif/for/...を上から読める
非対象
- Dart の文法全体
- Stream / isolate / pattern matching の詳細
- Riverpod や BLoC などの状態管理
- Widget ライフサイクルの深掘り
Dart 全体を一気に覚える必要はありません。Flutter のサンプルで頻出する記法から先に読めるようにしたほうが、次の 1 本を書き始めやすくなります。
2. まず読むべき 5 つの記号
最初に、Flutter のコードでよく出る記法をまとめます。
| 記法 | まず読む意味 | Flutter でよく見る場所 |
|---|---|---|
final / const | 実行時に 1 回だけ決まる / コンパイル時に固定される | Widget の field・ローカル変数 / const Text(...)・const constructor |
? / ! | null になる可能性がある / ここでは null ではないと断言する | API の結果、任意入力、初期化前の状態 |
required | この named parameter は必須 | Widget の constructor |
Future / async / await | 時間のかかる処理を待つ | API 呼び出し、ファイル読み込み、ボタン押下後の更新 |
collection if / for / ... | Widget のリストを条件やループで組み立てる | children: の中 |
この表を頭に置いておくと、初見のコードでも視線が散りにくくなります。次の章から、1 つずつ具体例で見ます。
3. final と const
final と const はどちらも「あとから変えない」ための宣言です。ただし、適用できる条件が異なります。
final: 実行時に 1 回だけ決まるconst: コンパイル時に値が確定している
まずは Dart 単体の最小例です。
void main() {
final startedAt = DateTime.now();
const appName = 'Inventory App';
print('$appName started at $startedAt');
// startedAt = DateTime.now(); // コンパイルエラー
// appName = 'Another App'; // コンパイルエラー
}
DateTime.now() は実行するまで値が決まりません。そのため final は使えますが、const は使えません。文字列リテラルのように最初から確定している値は const にできます。
次の CounterLabel は、final と const が Widget でどう役割分担するかを見る最小例です。constructor と field のどちらに何を付けているかを追うと読みやすくなります。
import 'package:flutter/material.dart';
class CounterLabel extends StatelessWidget {
const CounterLabel({
super.key,
required this.count,
});
final int count;
@override
Widget build(BuildContext context) {
return Text('Count: $count');
}
}
コードのポイント
① Widget が受け取る値は final field に置く
final int count;
count は Widget 生成時に受け取ったあと、内部で書き換えません。そのため State の可変値ではなく、final field として持ちます。
② constructor に const を付けると定数 Widget として扱いやすい
const CounterLabel({
super.key,
required this.count,
});
constructor が const なら、引数も定数のとき const CounterLabel(count: 1) のように書けます。Flutter 側が「毎回同じ構成」と判断しやすくなるため、const Text(...) がよく出てくる理由もここにあります。
const を付けると、Flutter 側は「この Widget は毎回同じ値で組み立てられる」と判断しやすくなります。いきなり最適化を意識する必要はありませんが、const Text('保存') のような書き方が多い理由はここにあります。
逆に、setState で変わる値へ final を付けることはできません。State の中で更新する値は、普通の field として持ちます。
4. null safety と ? / !
Dart では String と String? は別物です。
String:nullにならないString?:nullになる可能性がある
まずは最小例を見ます。
String formatUserName(String? name) {
if (name == null || name.isEmpty) {
return 'ゲスト';
}
return name;
}
name が String? なので、そのままでは文字列として扱えません。先に null を分岐で消してから使います。この順番が Dart の null safety です。
! は「ここでは null ではない」と断言する記法です。null を安全にしてくれるわけではありません。
void printUserName(String? name) {
print(name!.toUpperCase());
}
このコードは name が null のとき実行時エラーになります。! は最後の手段にしたほうが安全です。
次の LoginStatus は、nullable な値を UI 側でどう安全に分岐するかを見る例です。String? をそのまま使わず、どこで null を消しているかに注目してください。
import 'package:flutter/material.dart';
class LoginStatus extends StatelessWidget {
const LoginStatus({
super.key,
required this.userName,
});
final String? userName;
@override
Widget build(BuildContext context) {
if (userName == null) {
return const Text('未ログイン');
}
return Text('こんにちは、$userName さん');
}
}
コードのポイント
① nullable な値は先に if で分岐する
if (userName == null) {
return const Text('未ログイン');
}
String? をそのまま表示しようとせず、まず null のケースを出口として処理しています。ここで null を除外しているので、後続では通常の文字列として読めます。
② null でない分岐に入ってから値を使う
return Text('こんにちは、$userName さん');
if (userName == null) で先に戻っているため、この行では userName が null ではないと分かります。! で断言しなくても安全に UI を組める、という流れがこの例の核心です。
null のとき別の値へ置き換えるだけなら、?? もよく使います。
final label = userName ?? 'ゲスト';
! を見つけたら、「この直前で null ではないと確認できているか」を見る癖を付けると読みやすくなります。
5. named parameters とコンストラクタ
Flutter の Widget は named parameters が多く、最初は書き方が長く見えます。理由は単純で、引数の意味を呼び出し側で読めるようにしたいからです。
import 'package:flutter/material.dart';
class BookCard extends StatelessWidget {
const BookCard({
super.key,
required this.name,
required this.price,
this.note,
});
final String name;
final int price;
final String? note;
@override
Widget build(BuildContext context) {
final note = this.note;
return ListTile(
title: Text(name),
subtitle: note == null ? null : Text(note),
trailing: Text('¥$price'),
);
}
}
呼び出し側はこうなります。
const BookCard(
name: '入門 Flutter',
price: 3300,
note: 'PDF 版あり',
)
4 つのポイントを整理します。
required: この引数は省略できないthis.name: constructor の引数を、そのまま field へ代入するthis.note:requiredがないので任意super.key: 親クラスStatelessWidgetのkeyへ渡す
named parameters では、順番より名前が大事です。name: や price: が書かれているので、呼び出し側だけでも何を渡しているか追いやすくなります。
note は field なので、build の先頭で final note = this.note; とローカル変数へ写してから使っています。public な field は null チェックの後でも String? のままで Text に渡せませんが、ローカル変数なら分岐後に String として扱えるため、! なしで Text(note) と書けます。
6. Future / async / await
Future<T> は「あとで T が返ってくる値」です。時間のかかる処理を扱うときに出てきます。
Future<String> fetchGreeting() async {
await Future.delayed(const Duration(seconds: 1));
return '読み込み完了';
}
Future<void> main() async {
print('start');
final message = await fetchGreeting();
print(message);
print('end');
}
読み方は次のとおりです。
Future<String>: 最後には文字列が返るasync: この関数の中ではawaitを使うawait: その結果が返るまで、この関数の処理をその行で止める
次の ProfileReloadButton は、ボタン押下から非同期処理を呼び、待機中と完了後で UI を切り替える最小例です。await の前後で state をどう触っているかを見ると流れを追いやすくなります。
import 'package:flutter/material.dart';
class ProfileReloadButton extends StatefulWidget {
const ProfileReloadButton({super.key});
@override
State<ProfileReloadButton> createState() => _ProfileReloadButtonState();
}
class _ProfileReloadButtonState extends State<ProfileReloadButton> {
String status = '未取得';
Future<void> reload() async {
setState(() {
status = '読み込み中...';
});
final fetched = await Future<String>.delayed(
const Duration(seconds: 1),
() => '読み込み完了',
);
if (!mounted) {
return;
}
setState(() {
status = fetched;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(status),
ElevatedButton(
onPressed: reload,
child: const Text('再読み込み'),
),
],
);
}
}
コードのポイント
① 非同期処理の前に読み込み中の state へ切り替える
setState(() {
status = '読み込み中...';
});
処理の開始を先に UI へ反映しているため、待機中であることが画面から分かります。await の前に何を表示したいかを決めるのが、非同期 UI を読む最初のポイントです。
② await で結果を受け取り、完了後の表示へ戻す
final fetched = await Future<String>.delayed(
const Duration(seconds: 1),
() => '読み込み完了',
);
Future<String> の結果を fetched に受け取ってから、次の更新へ進みます。処理順は上からそのまま読めるので、callback が入れ子になるより流れを追いやすい形です。
③ await のあとで mounted を確認してから setState する
if (!mounted) {
return;
}
setState(() {
status = fetched;
});
待機中に画面が閉じられている可能性があるため、await のあとで mounted を見ています。非同期処理後に state を触るときの基本形として、この位置を覚えておくと安全です。
sequenceDiagram
participant User as ユーザー
participant State as State
participant Future as 非同期処理
User->>State: onPressed
State->>State: reload()
State->>State: setState("読み込み中...")
State->>Future: await 非同期処理
Future-->>State: 結果を返す
State->>State: setState("読み込み完了")
初学者がやりがちなのは、build の中でそのまま非同期処理を始める書き方です。
@override
Widget build(BuildContext context) {
reload();
return Text(status);
}
build は何度も呼ばれるため、この書き方だと再描画のたびに reload() が走ります。最初の入口としては、initState かボタン押下のようなイベントから呼ぶほうが安全です。
7. build メソッド内の collection 記法
Flutter の children: には Widget のリストを渡します。Dart では、そのリストを if や for でその場で組み立てられます。
次の BookList は、条件分岐と繰り返しを children: の中へそのまま書けることをまとめて確認する例です。どこで表示条件を分け、どこで一覧展開しているかを追うと読みやすくなります。
import 'package:flutter/material.dart';
class BookList extends StatelessWidget {
const BookList({
super.key,
required this.books,
required this.isLoading,
});
final List<String> books;
final bool isLoading;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('書籍一覧'),
if (isLoading) const CircularProgressIndicator(),
if (!isLoading && books.isEmpty) const Text('書籍がありません'),
for (final book in books) Text(book),
const Divider(),
...books.map(
(book) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Chip(label: Text(book)),
),
),
],
);
}
}
コードのポイント
① 条件つきの表示は if をそのまま children: に書ける
if (isLoading) const CircularProgressIndicator(),
if (!isLoading && books.isEmpty) const Text('書籍がありません'),
ローディング中か、0 件かで出す Widget をその場で分けています。別のリストを組み立て直さなくても、UI 条件を上から読める形で表現できます。
② 一覧の展開は for と ...map() の両方が使える
for (final book in books) Text(book),
const Divider(),
...books.map(
(book) => Padding(
単純な繰り返しなら for、少し装飾を付けた変換なら map と使い分けられます。どちらも children: の中で完結するため、表示順を崩さずに一覧を組み立てられます。
if や for が読みにくいと感じたら、無理に 1 つの children: へ詰め込まないほうが安全です。途中で別 Widget へ切り出すだけでも、上から読める行数が減って追いやすくなります。
8. Flutter のサンプルコードを読む順番
ここまでの内容を、実際にコードを読む順番へ戻します。
- constructor や関数名を見る
- named parameters と
requiredを見る - field が
finalか、値がconstにできるかを見る - 型に
?が付いていないかを見る - 関数に
asyncがあり、どこでawaitしているかを見る buildのchildren:を上から追い、if/for/...を普通の日本語へ置き換える
この順番を持っておくと、記号だけに引っ張られず、コードの流れを先に掴めます。
9. まとめ
Flutter のコードを読み始めた段階では、Dart 全体より先に次の 5 つを押さえるのが近道です。
finalとconst- null safety と
?/! - named parameters と
required Future/async/awaitbuild内の collectionif/for/...
この 5 つが読めるだけで、公式サンプルや入門記事の記号で手が止まりにくくなります。値が複数回流れてくる非同期処理へ進むなら、次は DartのStream入門(非同期データの流れをつかむ) を続けて読むとつながります。そこで詰まらなくなれば、Flutter の次の題材へ進みやすくなります。