公開日 2026-06-15

Flutterで最初に詰まりやすいDartの書き方:final・const・null safety・async/await を最初に整理する

Flutter のサンプルコードで止まりやすい final / const・null safety・named parameters・async/await・build 内の collection if / for の読み方を、最初の1本向けに整理する。

目次

  1. 1. この記事の位置づけ
  2. 対象読者
  3. この記事で到達する状態
  4. 非対象
  5. 2. まず読むべき 5 つの記号
  6. 3. final と const
  7. コードのポイント
  8. 4. null safety と ? / !
  9. コードのポイント
  10. 5. named parameters とコンストラクタ
  11. 6. Future / async / await
  12. コードのポイント
  13. 7. build メソッド内の collection 記法
  14. コードのポイント
  15. 8. Flutter のサンプルコードを読む順番
  16. 9. まとめ

Flutterで画像・SVG・アイコンを管理する(flutter_gen最小構成) の次に手が止まりやすいのは、UI より Dart の記法です。Flutter のサンプルには final const ? required async ... が並びます。この記事では、その中でも最初の 1 本を書く前に押さえたい 5 つだけを Flutter の文脈で整理します。

1. この記事の位置づけ

対象読者

  • Flutter の開発環境は整ったが、Dart の書き方で手が止まりやすい人
  • JavaScript / Java / C# などの経験はあるが、Dart はまだ読み慣れていない人
  • Flutter の公式サンプルや入門記事を読み始めた段階の人

この記事で到達する状態

  • finalconst を場面で選び分けられる
  • nullable な値を ?! で読み分けられる
  • Widget の constructor を named parameters 前提で追える
  • Future / async / await の最小の流れを読める
  • build メソッド内の collection if / 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. finalconst

finalconst はどちらも「あとから変えない」ための宣言です。ただし、適用できる条件が異なります。

  • 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 は、finalconst が 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 では StringString? は別物です。

  • String: null にならない
  • String?: null になる可能性がある

まずは最小例を見ます。

String formatUserName(String? name) {
  if (name == null || name.isEmpty) {
    return 'ゲスト';
  }

  return name;
}

nameString? なので、そのままでは文字列として扱えません。先に null を分岐で消してから使います。この順番が Dart の null safety です。

! は「ここでは null ではない」と断言する記法です。null を安全にしてくれるわけではありません。

void printUserName(String? name) {
  print(name!.toUpperCase());
}

このコードは namenull のとき実行時エラーになります。! は最後の手段にしたほうが安全です。

次の 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) で先に戻っているため、この行では userNamenull ではないと分かります。! で断言しなくても安全に 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: 親クラス StatelessWidgetkey へ渡す

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 では、そのリストを iffor でその場で組み立てられます。

次の 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: の中で完結するため、表示順を崩さずに一覧を組み立てられます。

iffor が読みにくいと感じたら、無理に 1 つの children: へ詰め込まないほうが安全です。途中で別 Widget へ切り出すだけでも、上から読める行数が減って追いやすくなります。

8. Flutter のサンプルコードを読む順番

ここまでの内容を、実際にコードを読む順番へ戻します。

  1. constructor や関数名を見る
  2. named parameters と required を見る
  3. field が final か、値が const にできるかを見る
  4. 型に ? が付いていないかを見る
  5. 関数に async があり、どこで await しているかを見る
  6. buildchildren: を上から追い、if / for / ... を普通の日本語へ置き換える

この順番を持っておくと、記号だけに引っ張られず、コードの流れを先に掴めます。

9. まとめ

Flutter のコードを読み始めた段階では、Dart 全体より先に次の 5 つを押さえるのが近道です。

  • finalconst
  • null safety と ? / !
  • named parameters と required
  • Future / async / await
  • build 内の collection if / for / ...

この 5 つが読めるだけで、公式サンプルや入門記事の記号で手が止まりにくくなります。値が複数回流れてくる非同期処理へ進むなら、次は DartのStream入門(非同期データの流れをつかむ) を続けて読むとつながります。そこで詰まらなくなれば、Flutter の次の題材へ進みやすくなります。

シリーズ 4/14

このシリーズ

Flutter導入と基礎

  1. 1. Windows 11で始めるFlutter開発環境:Android Emulatorで動かすまで
  2. 2. Flutter + FVM で開発環境のバージョンを固定する
  3. 3. Flutterで画像・SVG・アイコンを管理する(flutter_gen最小構成)
  4. 4. Flutterで最初に詰まりやすいDartの書き方:final・const・null safety・async/await を最初に整理する 現在の記事
  5. 5. DartのStream入門(非同期データの流れをつかむ)
  6. 6. FlutterのWidgetライフサイクル入門(initState / dispose で詰まらないために)
  7. 7. FlutterでBuildContextとKeyを理解する
  8. 8. Flutterのレイアウト入門(Column / Row / Stack の使い分け)
  9. 9. Flutterのテーマ設計入門(ThemeData + Theme Extension)
  10. 10. FlutterでMediaQueryとLayoutBuilderを使って画面サイズに対応する(スマホ・タブレット両対応)
  11. 11. FlutterのContainerとSizedBoxを使いこなす(余白・サイズ・装飾の基本)
  12. 12. FlutterのListViewとGridViewで一覧画面を作る(基本パターン)
  13. 13. Flutterのダイアログ・スナックバー・ボトムシートを使う(確認・通知UIの基本)
  14. 14. FlutterのTabBarとBottomNavigationBarで複数画面を切り替える