Flutter

テストとパッケージ

Widgetテストでボタンの動作を検証し、パッケージで小さな設定を保存します。

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

01

テストするアプリを揃える

このページはFlutter入門のカウンターを検証します。ほかの発展ページの画面を使っていた場合も、lib/main.dartを以下へ戻してください。プロジェクト名は入門で作ったstudy_appです。まず外部パッケージなしでテストします。

基礎練習で作ったbasics.dartは、エディターやファイル管理画面でstudy_appの外へ移して保管します。コンソール練習のprintがFlutterの解析ルールで指摘されるのを避け、ここではアプリとテストを検査します。

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: CounterPage());
  }
}

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});
  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('学習カウンター')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('学習回数: $_count'),
            ElevatedButton(
              onPressed: () {
                setState(() { _count += 1; });
              },
              child: const Text('1回追加'),
            ),
          ],
        ),
      ),
    );
  }
}
02

画面を操作するテスト

生成済みのtest/widget_test.dartを以下へ全体ごと置き換えます。pumpWidgetでWidgetを表示し、find.textで文字を探し、tapで操作します。pumpは状態変更後のフレームを進めます。

test/widget_test.dartDART
import 'package:flutter_test/flutter_test.dart';
import 'package:study_app/main.dart';

void main() {
  testWidgets('追加ボタンを2回押すと学習回数が2になる', (tester) async {
    await tester.pumpWidget(const StudyApp());
    expect(find.text('学習回数: 0'), findsOneWidget);
    await tester.tap(find.text('1回追加'));
    await tester.pump();
    expect(find.text('学習回数: 1'), findsOneWidget);
    await tester.tap(find.text('1回追加'));
    await tester.pump();
    expect(find.text('学習回数: 2'), findsOneWidget);
    expect(find.text('学習回数: 0'), findsNothing);
  });
}
pubspec.yamlのあるフォルダーで実行COMMAND
flutter analyze
flutter test
確認する結果OUTPUT
静的解析でエラーがない
1つのWidgetテストが成功する

pubspec.yamlのnameを変更した場合、importのstudy_appもその名前に揃えます。試しに期待値2を3に変えるとテストが失敗します。元へ戻して再度成功することを確認しましょう。

03

設定保存のパッケージを追加

ここからは別の練習です。shared_preferencesは小さなキーと値の保存に使えます。プロジェクト直下で次を実行すると、依存関係がpubspec.yamlに追加され、取得されます。ネットワーク接続が必要です。

依存パッケージを追加COMMAND
flutter pub add shared_preferences

次の例はSharedPreferencesAsyncを使います。このAPIはshared_preferences 2.3.0以降で提供されています。SDKの制約で古い版が選ばれた場合は、対応SDKを確認して更新します。pubspec.lockには解決された版が記録されます。

保存するのは学習設定だけです。秘密情報や失ってはいけない記録の保存には向きません。Webの保存はブラウザーとオリジンごとに分かれ、閲覧データの削除で消えることがあります。
04

設定を保存して読み戻す

ここから先の検証コマンド

この節でlib/main.dartを設定画面へ置き換えた後は、flutter test ではなく flutter analyze だけを実行してください。前半のtest/widget_test.dartはカウンター画面を前提としているため、設定画面のまま実行するとテストは失敗します。前半のテストを再実行する場合は、lib/main.dartとtest/widget_test.dartの両方を前半のコードに戻します。

lib/main.dartを以下へ全体ごと置き換えます。initStateで初回の読み込みを始めます。保存が成功した後だけ画面のスイッチを確定し、失敗時は案内を表示します。await後にはmountedを確認します。

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

void main() => runApp(const StudyApp());
class StudyApp extends StatelessWidget {
  const StudyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: SettingsPage());
  }
}
class SettingsPage extends StatefulWidget {
  const SettingsPage({super.key});
  @override
  State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
  final _prefs = SharedPreferencesAsync();
  bool _enabled = false;
  bool _busy = true;
  String _message = '読み込み中';

  @override
  void initState() {
    super.initState();
    _load();
  }

  Future<void> _load() async {
    try {
      final value = await _prefs.getBool('study_enabled') ?? false;
      if (!mounted) return;
      setState(() { _enabled = value; _message = '読み込み完了'; });
    } catch (error) {
      if (!mounted) return;
      setState(() { _message = '設定を読み込めませんでした'; });
    } finally {
      if (mounted) setState(() { _busy = false; });
    }
  }

  Future<void> _save(bool value) async {
    setState(() { _busy = true; _message = '保存中'; });
    try {
      await _prefs.setBool('study_enabled', value);
      if (!mounted) return;
      setState(() { _enabled = value; _message = '保存しました'; });
    } catch (error) {
      if (!mounted) return;
      setState(() { _message = '保存できませんでした'; });
    } finally {
      if (mounted) setState(() { _busy = false; });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('学習設定')),
      body: Column(
        children: [
          SwitchListTile(
            title: const Text('学習モード'),
            value: _enabled,
            onChanged: _busy ? null : (value) { _save(value); },
          ),
          Text(_message),
        ],
      ),
    );
  }
}
画面で確認する結果OUTPUT
初回は学習モードがオフ(未保存の場合)
オンにすると「保存しました」
同じブラウザー・同じオリジンで再読み込みしてもオンのまま

Webの保存を比較するため、実行を一度終了し、次のようにポートを固定して起動します。まず同じ実行セッション内でブラウザーを再読み込みし、保存を確認してください。

保存例を実行COMMAND
flutter run -d chrome --web-port=7357

flutter runで起動するChromeは一時プロファイルを使うことがあります。コマンド終了後の別セッションでは保存領域も変わり得るため、ブラウザーの再読み込みと、新しいプロファイルでの起動を区別します。カウンター用のWidgetテストは画面が変わったので、この設定画面には適用できません。カウンターのコードへ戻してテストするか、設定画面用に保存先を差し替えられる構成とテストを追加します。

05

練習:保存と解除を確認

学習モードをオン・オフにし、それぞれ再読み込み後も値が保たれるかを確認してください。初期値だけで成功と判断しないため、両方の値を試します。

確認手順と期待結果
確認する結果OUTPUT
オンに変更 → 保存しました → 再読み込み → オン
オフに変更 → 保存しました → 再読み込み → オフ

最後に flutter analyze を実行します。アプリとして共有するときはlib、test、pubspec.yaml、pubspec.lockなどを管理し、buildなどの生成物を直接編集しません。テストページ前半へ戻る際は、main.dartとwidget_test.dartを対応する組に揃えます。

公式ドキュメントで確認する:Widgetテスト / shared_preferences / 設定の保存