🦷

Stepper classを使用して予約フローを作成する

2024/12/11に公開

👤対象者

  • 予約画面のフローを作ってみたい
  • Stepper classの活用例

記事の内容

歯医者さんの予約システムをFlutterで再現するのを今回はやってみようと思います。こちらがデモアプリの動画です。

https://youtube.com/shorts/W63g2WU3Kfg

デンタルクリニック予約アプリ

アプリケーションの概要

このFlutterアプリケーションは、歯科医院の予約システムを提供する簡単なアプリです。

主な機能

  • ホーム画面

    • クリニック名(俺ちゃんデンタルクリニック)の表示
    • 予約ページへの遷移ボタン
    • Material Design 3に基づいたモダンなUI
  • 予約画面

    • ステップ形式の予約フォーム
    • 直感的な操作性

技術的な実装について

  • 状態管理はsetState()を使用
  • checkboxの状態管理を行なう

使用している主な機能

  • MaterialApp: アプリケーションのベース設定

    • デバッグバナーの非表示化
    • カスタムテーマカラーの設定
    • Material Design 3の採用
  • Navigator: 画面遷移の実装

    • MaterialPageRouteを使用したページ遷移
    • StatefulWidgetによる状態管理

example

サンプルコード
main.dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'デンタルクリニック予約',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF40E0D0), // ターコイズブルー
        ),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context) {
    return  Scaffold(
      appBar: AppBar(
        title: const Text('俺ちゃんデンタルクリニック'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Center(
        child: ElevatedButton(onPressed: () {
          Navigator.of(context).push(MaterialPageRoute(builder: (context) => const DentalReservationPage()));
        }, 
         child: const Text('予約する')),
      )
    );
  }
}

class DentalReservationPage extends StatefulWidget {
  const DentalReservationPage({super.key});

  
  State<DentalReservationPage> createState() => _DentalReservationPageState();
}

class _DentalReservationPageState extends State<DentalReservationPage> {
  int _currentStep = 0;
  final Map<String, bool> _symptoms = {
    '歯がしみる': false,
    '歯が痛い': false,
    '歯茎が痛い': false,
    '歯が痺れる': false,
    '歯が折れた': false,
    '悪いところがないか検査してほしい': false,
  };

  List<Step> getSteps() {
    return [
      Step(
        title: const Text('症状の選択'),
        content: Column(
          children: _symptoms.entries.map((entry) {
            return CheckboxListTile(
              title: Text(entry.key),
              value: entry.value,
              onChanged: (bool? value) {
                setState(() {
                  _symptoms[entry.key] = value ?? false;
                });
              },
            );
          }).toList(),
        ),
        isActive: _currentStep >= 0,
      ),
      Step(
        title: const Text('予約内容の確認'),
        content: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('選択された症状:', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            ..._symptoms.entries
                .where((entry) => entry.value)
                .map((entry) => Padding(
                      padding: const EdgeInsets.only(left: 16, bottom: 4),
                      child: Text('・${entry.key}'),
                    ))
                .toList(),
          ],
        ),
        isActive: _currentStep >= 1,
      ),
      Step(
        title: const Text('完了'),
        content: const Column(
          children: [
            Icon(Icons.check_circle_outline, size: 48, color: Color(0xFF40E0D0)),
            SizedBox(height: 16),
            Text('予約申し込みが完了しました。\nご来院をお待ちしております。',
                textAlign: TextAlign.center),
          ],
        ),
        isActive: _currentStep >= 2,
      ),
    ];
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('予約フォーム'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Stepper(
        type: StepperType.horizontal,
        currentStep: _currentStep,
        onStepContinue: () {
          if (_currentStep < getSteps().length - 1) {
            setState(() {
              _currentStep++;
            });
          }
        },
        onStepCancel: () {
          if (_currentStep > 0) {
            setState(() {
              _currentStep--;
            });
          }
        },
        steps: getSteps(),
        controlsBuilder: (BuildContext context, ControlsDetails details) {
          return Padding(
            padding: const EdgeInsets.symmetric(vertical: 16.0),
            child: Row(
              children: [
                if (_currentStep < getSteps().length - 1)
                  ElevatedButton(
                    onPressed: details.onStepContinue,
                    child: const Text('次へ'),
                  ),
                if (_currentStep > 0) ...[
                  const SizedBox(width: 12),
                  TextButton(
                    onPressed: details.onStepCancel,
                    child: const Text('戻る'),
                  ),
                ],
              ],
            ),
          );
        },
      ),
    );
  }
}

参考リソース

Flutterの開発に関する詳細な情報は以下をご覧ください:

Discussion