Open10

Chat GPTに質問しながらFlutterでクイズアプリを開発するチャレンジ!

kazu_9mmkazu_9mm

こんな感じの簡易的なクイズアプリです

トップ画面

クイズページ

クイズページ

回答結果

おおまかな設計

1.問題と回答を含むデータモデルを定義する。

問題と回答を含むデータモデルは、Questionというクラスを定義して保持します。Questionクラスは、問題文、回答選択肢、正解の選択肢のインデックスを保持します。

2.クイズの開始画面と終了画面を作成する。

クイズの開始画面は、ユーザーがクイズを開始する前に表示される画面です。StartScreenというクラスを定義し、UIを設計します。StartScreenクラスには、Startボタンが表示され、ボタンを押すとクイズの問題と回答が表示されるページに遷移します。

クイズの終了画面は、ユーザーがクイズを終了した後に表示される画面です。EndScreenというクラスを定義し、UIを設計します。EndScreenクラスには、スコアが表示され、Restartボタンが表示されます。Restartボタンを押すと、クイズの開始画面に戻ります。

3.問題と回答を表示するページを作成する。

QuizPageというクラスを定義し、問題と回答を表示します。QuizPageは、問題のリストとスコアを受け取ります。問題は、1つずつ表示され、回答の選択肢が表示されます。回答を選択すると、フィードバックが表示されます。

4.ユーザーが回答を選択すると、それに対するフィードバックを表示する。

回答の選択肢をタップすると、_checkAnswerメソッドが呼び出され、選択された回答が正解かどうかをチェックします。正解の場合は、_isCorrectフラグをtrueに設定し、不正解の場合はfalseに設定します。フィードバックは、_buildFeedbackメソッドで作成され、フラグの値に基づいて表示されます。

5.最後に、ユーザーがクイズを終了した後に、スコアを表示するページを作成する。

EndScreenクラスが、クイズを終了した後にスコアを表示します。スコアは、QuizPageクラスで計算され、EndScreenクラスに渡されます。スコアに応じたテキストが表示され、Restartボタンを押すと、クイズの開始画面に戻ります。

kazu_9mmkazu_9mm

1.問題と回答を含むデータモデルを定義する。 について実装

簡単な問題データを3つ用意し、問題データをハードコードする方法でQuestionクラスを更新します。

dart
class Question {
  final String questionText;
  final List<String> answerChoices;
  final int correctAnswerIndex;

  Question({
    required this.questionText,
    required this.answerChoices,
    required this.correctAnswerIndex,
  });

  static List<Question> getSampleQuestions() {
    return [
      Question(
        questionText: '世界で最も話されている言語は何ですか?',
        answerChoices: ['英語', '中国語', 'スペイン語', 'ヒンディー語'],
        correctAnswerIndex: 1,
      ),
      Question(
        questionText: '光の速度は、1秒あたりどのくらいの距離を進みますか?',
        answerChoices: ['3,000 km', '300,000 km', '30,000 km', '3,000,000 km'],
        correctAnswerIndex: 1,
      ),
      Question(
        questionText: '太陽系で最も大きな惑星はどれですか?',
        answerChoices: ['地球', '火星', '木星', '土星'],
        correctAnswerIndex: 2,
      ),
    ];
  }
}


このコードでは、QuestionクラスにgetSampleQuestionsという静的メソッドを追加しました。このメソッドは、ハードコードされた3つの簡単な問題を含むQuestionのリストを返します。これらの問題データは、後のステップでQuizPageクラスに渡すことができます。

kazu_9mmkazu_9mm

2.クイズの開始画面と終了画面を作成する。の実装

まず、クイズの開始画面を作成します。以下のコードは、StartScreenクラスを定義し、UIを設計しています。この画面には、「Start」ボタンが表示され、ボタンを押すとクイズの問題と回答が表示されるページ(QuizPage)に遷移します。シンプルでモダンなデザインにするために、マテリアルデザインを使用しています。

import 'package:flutter/material.dart';
// 以下のインポートを追加してください。
// import 'quiz_page.dart';

class StartScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('クイズアプリ'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text(
              'クイズを開始する準備ができましたか?',
              style: TextStyle(fontSize: 24),
            ),
            SizedBox(height: 30),
            ElevatedButton(
              onPressed: () {
                // ボタンを押すとQuizPageに遷移する
                // Navigator.push(
                //   context,
                //   MaterialPageRoute(builder: (context) => QuizPage()),
                // );
              },
              child: Text('スタート', style: TextStyle(fontSize: 18)),
              style: ElevatedButton.styleFrom(
                padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
                primary: Theme.of(context).primaryColor,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(10),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

上記のコードでは、まだQuizPageが実装されていないため、遷移の部分はコメントアウトされています。次のステップでQuizPageが実装されたら、コメントアウトを解除し、遷移を正しく実装できます。

kazu_9mmkazu_9mm

忘れてた、プロジェクト構成

quiz_app/
  ├─ lib/
  │    ├─ main.dart
  │    ├─ screens/
  │    │    ├─ start_screen.dart
  │    │    ├─ quiz_page.dart
  │    │    └─ end_screen.dart
  │    └─ models/
  │         └─ question.dart
  └─ pubspec.yaml

  • main.dart: アプリケーションのエントリーポイントです。StartScreenを呼び出します。
  • screens/: 画面に関連するファイルが格納されています。
    • start_screen.dart: クイズの開始画面を定義します。
    • quiz_page.dart: 問題と回答を表示するページを定義します。
    • end_screen.dart: クイズの終了画面を定義します。
  • models/: データモデルに関連するファイルが格納されています。
  • question.dart: Questionクラスを定義します。

現在のアプリケーションでは、問題データがハードコードされているため、データベースは使用していません。ただし、将来的にデータベースを使用して問題データを保持することを検討している場合、以下のようなデータベース構成を検討できます。

テーブル: questions

  • id: INTEGER PRIMARY KEY
  • question_text: TEXT
  • answer_choices: TEXT(カンマ区切りの選択肢を保存)
  • correct_answer_index: INTEGER

上記のデータベース構成では、questionsテーブルに各問題とその選択肢、正解のインデックスが格納されます。データベースを利用する場合、QuestionクラスのgetSampleQuestionsメソッドを更新して、データベースから問題データを取得するように変更する必要があります。

kazu_9mmkazu_9mm

2.クイズの終了画面を作成する。の実装

まず、lib/screens/end_screen.dartファイルを作成し、以下のコードを記述してください。

import 'package:flutter/material.dart';

class EndScreen extends StatelessWidget {
  final int score;
  final int totalQuestions;

  EndScreen({required this.score, required this.totalQuestions});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Quiz Result'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Your Score: $score / $totalQuestions',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 30),
            ElevatedButton(
              onPressed: () {
                Navigator.popUntil(context, (route) => route.isFirst);
              },
              child: Text('Restart'),
              style: ElevatedButton.styleFrom(
                padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
                textStyle: TextStyle(fontSize: 18),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

このコードでは、終了画面にスコアを表示し、"Restart"ボタンを配置しています。ボタンが押されると、クイズの開始画面に戻るようになっています。また、シンプルかつモダンなデザインを考慮して、フォントサイズやボタンのスタイルを調整しています。

kazu_9mmkazu_9mm

main.dart修正

既に実装された終了画面(EndScreen)をmain.dartで利用し、適切なタイミングで画面遷移するように実装します。以下のようにmain.dartを編集します。

import 'package:flutter/material.dart';
import 'package:your_project_name/start_screen.dart'; // ここで開始画面をインポートしてください。
import 'package:your_project_name/end_screen.dart'; // ここで終了画面をインポートしてください。

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Quiz App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: StartScreen(), // 開始画面を表示
    );
  }
}

kazu_9mmkazu_9mm

3.「問題と回答を表示するページを作成する」

import 'package:flutter/material.dart';
import 'package:your_project_name/question.dart'; // Questionクラスをインポート

class QuizPage extends StatefulWidget {
  final List<Question> questions;

  QuizPage({required this.questions});

  @override
  _QuizPageState createState() => _QuizPageState();
}

class _QuizPageState extends State<QuizPage> {
  int _currentQuestionIndex = 0;
  bool _isCorrect = false;
  int _score = 0; // スコアを追加

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Quiz'),
      ),
      body: Column(
        children: [
          _buildQuestion(),
          _buildAnswerButtons(),
          _buildFeedback(),
        ],
      ),
    );
  }

  Widget _buildQuestion() {
    return Text(widget.questions[_currentQuestionIndex].question);
  }

  Widget _buildAnswerButtons() {
    return Column(
      children: widget.questions[_currentQuestionIndex].options
          .asMap()
          .entries
          .map((entry) {
        int index = entry.key;
        String option = entry.value;
        return ElevatedButton(
          onPressed: () {
            _checkAnswer(index == widget.questions[_currentQuestionIndex].correctAnswerIndex);
          },
          child: Text(option),
        );
      }).toList(),
    );
  }

  void _checkAnswer(bool isCorrect) {
    setState(() {
      _isCorrect = isCorrect;
      if (isCorrect) {
        _score++; // 正解の場合にスコアを加算
      }
      _currentQuestionIndex++;
      
      if (_currentQuestionIndex == widget.questions.length) {
        _showEndScreen();
      }
    });
  }

  // クイズ終了後にEndScreenに遷移するメソッド
  void _showEndScreen() {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => EndScreen(score: _score, totalQuestions: widget.questions.length),
      ),
    );
  }

  Widget _buildFeedback() {
    return Visibility(
      visible: _isCorrect != null,
      child: Text(_isCorrect ? 'Correct!' : 'Incorrect!'),
    );
  }
}

この実装では、QuizPageで問題文を表示し、回答の選択肢をボタンとして表示しています。ユーザーが回答を選択すると、_checkAnswerメソッドが呼び出され、正解かどうかを判定し、フィードバックが表示されます。

それぞれの処理を解説

  1. QuizPageクラスはStatefulWidgetを継承しています。これは、ページの状態が変更される可能性があるためです。例えば、現在の質問やフィードバックなどです。

  2. _QuizPageStateクラスはState<QuizPage>を継承しています。これは、QuizPageの状態を管理するためのクラスです。

  3. _currentQuestionIndex変数は、現在表示されている質問のインデックスを保持しています。

  4. _isCorrect変数は、ユーザーが選択した回答が正しいかどうかを保持しています。

  5. buildメソッドでは、Scaffoldウィジェットを使用してアプリの基本的なレイアウトを定義しています。AppBarとBodyが含まれています。

  6. Columnウィジェットは、子ウィジェットを縦方向に配置します。この例では、質問文、回答ボタン、フィードバックが含まれています。

  7. _buildQuestionメソッドは、現在の質問文をテキストウィジェットとして返します。

  8. _buildAnswerButtonsメソッドは、現在の質問の回答選択肢をボタンとして生成し、それらをColumnウィジェットで縦方向に配置します。回答ボタンがタップされると、_checkAnswerメソッドが呼び出されます。

  9. _checkAnswerメソッドは、引数isCorrectがtrueかfalseかに応じて、_isCorrect変数を更新します。setStateメソッドを使用することで、状態が変更されたときにUIが再描画されるようになります。

  10. Visibilityウィジェットを使用して、フィードバックの表示状態を制御しています。_isCorrect変数がnullでない場合、フィードバックが表示されます。これは、ユーザーが回答ボタンを押した後にフィードバックが表示されることを意味します。_isCorrectがtrueの場合、「Correct!」が表示され、falseの場合、「Incorrect!」が表示されます。

kazu_9mmkazu_9mm

5.最後に、ユーザーがクイズを終了した後に、スコアを表示するページを作成する。 の実装だが、、

実は、以前に提供されたEndScreenクラスのコードは、既に5. 最後に、ユーザーがクイズを終了した後に、スコアを表示するページを作成する、という要件を満たしています。しかし、スコアを計算し、EndScreenに遷移する部分を実装する必要があります。

QuizPageクラスでスコアを計算するために、_scoreという変数を追加しましょう。その後、_checkAnswerメソッドで正解の場合にスコアを加算します。最後に、問題がすべて終わった後にEndScreenへ遷移する実装を追加します。

以下に、QuizPageクラスの変更部分を示します。

class _QuizPageState extends State<QuizPage> {
  int _currentQuestionIndex = 0;
  bool _isCorrect = false;
  int _score = 0; // スコアを追加

  // ...

  void _checkAnswer(bool isCorrect) {
    setState(() {
      _isCorrect = isCorrect;
      if (isCorrect) {
        _score++; // 正解の場合にスコアを加算
      }
      _currentQuestionIndex++;
      
      if (_currentQuestionIndex == widget.questions.length) {
        _showEndScreen();
      }
    });
  }

  // クイズ終了後にEndScreenに遷移するメソッド
  void _showEndScreen() {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => EndScreen(score: _score, totalQuestions: widget.questions.length),
      ),
    );
  }

  // ...
}

この修正は、https://zenn.dev/link/comments/95fb9921f405b9 ここに書いておきました。
この変更により、スコアが計算され、クイズが終了した後にEndScreenに遷移するようになりました。

kazu_9mmkazu_9mm

プロジェクト作成、ローカルで実行

それでは、実際にプロジェクトを作成し、ローカルで動作確認する手順を説明します。

  1. Flutterのインストール:
    まずは、Flutterをインストールしていなければインストールしてください。公式ドキュメントのGetting Startedを参照して、適切な手順に従ってください。

  2. プロジェクトの作成:
    コマンドプロンプトやターミナルを開いて、プロジェクトを作成するディレクトリに移動してください。次に、以下のコマンドを実行して、新しいFlutterプロジェクトを作成します。

flutter create your_project_name

これで、新しいFlutterプロジェクトが作成されます。作成されたプロジェクトのディレクトリに移動してください。

  1. コードの追加:
    これまでに書いたコードをプロジェクトに追加します。libディレクトリ内にあるmain.dartファイルを開き、提供されたコードで置き換えてください。次に、question.dartとend_screen.dartをlibディレクトリに作成し、それぞれのコードを追加します。

  2. 依存関係の追加(必要に応じて):
    もし外部ライブラリやパッケージが必要な場合、pubspec.yamlファイルを開き、dependenciesセクションに追加してください。今回のプロジェクトでは、外部ライブラリは使用していないため、この手順は必要ありません。

  3. エミュレータの起動:
    Android StudioやVisual Studio Codeなどの統合開発環境(IDE)を使用している場合、エミュレータや実機を選択してアプリを実行できます。エミュレータや実機が起動していない場合は、まずそれを起動してください。

kazu_9mmkazu_9mm

やってみた感想

ChatGPTすげぇ…

開発時間:約6時間程度

もちろんある程度エラーは出るけど、それすらも質問して大体解決しました。
Flutterまったく触ったことないのに、簡単なアプリをローカルで動かせるくらいまで実装できたのは凄いと思った。

Flutterの基礎とかは細かく理解できてないけど、なんとなく大まかな流れは分かった気がする。
経験が浅いエンジニアで、なんか個人開発したいけど、何したらいいか分からんし難しいって思う人沢山いると思う。

実際に自分もそう思っていて、最初の1年は何も開発とかできなかったです。

でもこうして小さいながら動くものを作るっていう経験はとても重要だと思いました。
まずは作ってみてから、その後にコードを理解していく学習方法もありだと思いました。

以上!長々とありがとうございました~(´-ω-`)