Flutter

状態と非同期処理

Future、async、awaitを理解し、待機中・成功・失敗を画面に反映します。

対象:Dart 3を同梱したFlutter 3系のstable版を対象に、まずChromeで動かすWebアプリを作ります。

01

Futureをコンソールで試す

Futureは後で得られる結果を表します。asyncな関数の中でawaitすると完了まで次の処理を待ちます。これはネットワークではなく1秒の待ち時間を作る練習です。basics.dartへ保存し dart run basics.dart で実行します。

basics.dart(全体を置き換え)DART
Future<String> loadLesson() async {
  await Future<void>.delayed(const Duration(seconds: 1));
  return 'Widget';
}
Future<void> main() async {
  print('開始');
  final lesson = await loadLesson();
  print(lesson);
  print('終了');
}
実行結果OUTPUT
開始
Widget
終了

開始の後、約1秒待って残りを表示します。awaitは呼び出した処理の完了を待ちますが、重い計算を自動で別スレッドに移す機能ではありません。

02

3つの状態を画面に表示

lib/main.dartを次の内容に置き換え、flutter run -d chrome で実行します。成功と失敗のボタンで両方の経路を確かめられます。実際の通信や外部パッケージは使いません。

lib/main.dart(全体を置き換え)DART
import 'package:flutter/material.dart';

void main() => runApp(const StudyApp());
class StudyApp extends StatelessWidget {
  const StudyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: LoadPage());
  }
}
class LoadPage extends StatefulWidget {
  const LoadPage({super.key});
  @override
  State<LoadPage> createState() => _LoadPageState();
}
class _LoadPageState extends State<LoadPage> {
  bool _loading = false;
  String _message = 'まだ読み込んでいません';

  Future<String> _fetchLesson(bool fail) async {
    await Future<void>.delayed(const Duration(seconds: 1));
    if (fail) throw Exception('練習用の失敗');
    return '次の学習: Widget';
  }

  Future<void> _load(bool fail) async {
    setState(() { _loading = true; _message = '読み込み中'; });
    try {
      final message = await _fetchLesson(fail);
      if (!mounted) return;
      setState(() { _message = message; });
    } catch (error) {
      if (!mounted) return;
      setState(() { _message = '読み込みに失敗しました。再試行できます'; });
    } finally {
      if (mounted) setState(() { _loading = false; });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('非同期の練習')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(_message),
            if (_loading) const LinearProgressIndicator(),
            ElevatedButton(
              onPressed: _loading ? null : () { _load(false); },
              child: const Text('成功を試す'),
            ),
            ElevatedButton(
              onPressed: _loading ? null : () { _load(true); },
              child: const Text('失敗を試す'),
            ),
          ],
        ),
      ),
    );
  }
}
画面で確認する結果OUTPUT
初期表示: まだ読み込んでいません
「成功を試す」 → 読み込み中 → 約1秒後に次の学習: Widget
「失敗を試す」 → 読み込み中 → 約1秒後に失敗の案内
待機中は両方のボタンが無効になり、完了後に再び押せる
03

await後のmounted確認

awaitの間に画面から離れると、Stateがすでに破棄されている可能性があります。mounted を確認してからsetStateすると、破棄済みの画面を更新するエラーを避けられます。

setStateのコールバックは同期処理にします。中へasyncを付けず、外側の_loadでawaitし、更新する短い処理だけをsetStateで囲みます。finallyで_loadingを戻すことで、失敗した後も再試行できます。

画面のbuildは何度も呼ばれます。build内で_loadを直接呼ぶと、再描画のたびに読み込みを始めてしまいます。ユーザー操作やinitStateなど、開始のきっかけを明確にします。
04

受け取ったJSONを検証

実際のAPIへ進む前にJSON文字列の扱いを練習します。dart:convertのjsonDecodeは動的な値を返すため、必要な形かを検証します。これはbasics.dart用の独立した例です。

basics.dart(全体を置き換え)DART
import 'dart:convert';

String parseTitle(String source) {
  final Object? data = jsonDecode(source);
  if (data is! Map<String, dynamic>) {
    throw const FormatException('オブジェクトが必要です');
  }
  final Object? title = data['title'];
  if (title is! String || title.isEmpty) {
    throw const FormatException('titleが不正です');
  }
  return title;
}
void main() {
  for (final source in ['{"title":"Widget"}', '{"title":10}']) {
    try {
      print(parseTitle(source));
    } on FormatException {
      print('データの形式が不正です');
    }
  }
}
実行結果OUTPUT
Widget
データの形式が不正です

is! は指定した型でないかを調べます。APIへ接続するときは、この検証に加えてHTTPの状態コード、通信失敗、タイムアウトも確認する必要があります。

05

練習:待ち時間と再試行

完成例の待ち時間を2秒へ変え、失敗した後に成功を試せるかを確認します。ボタンの無効化がなければ何が起こるかも考えてみましょう。

解答例と確認結果
_fetchLesson内の待ち時間の行を置き換えDART
await Future<void>.delayed(const Duration(seconds: 2));
確認する結果OUTPUT
失敗を試す → 約2秒後に失敗の案内
その後に成功を試す → 約2秒後に次の学習: Widget
待機中の連打では新しい読み込みを開始しない

同時に複数の読み込みを始めると、先に開始した古い結果が後から届くことがあります。この例は待機中のボタンを無効にして同時実行を防いでいます。より複雑な画面では処理のキャンセルや結果の識別も設計します。

公式ドキュメントで確認する:Dartの非同期処理 / Flutterの状態管理の基礎