Dart

35.【クイズアプリ開発】正解したときの処理と正答数の反映

長かったクイズアプリの開発も本日でラストになります。

前回分はこちらです。

本日は問題の選択肢で正解を当てた時の処理を書いていきます。

ラストにクイズアプリ開発で書いたクラスファイルのソースコードを載せようと思います。

正解したときの処理

まず、問題のデータの変数をおさらいします。

main.dart

/// 問題
  var _questions = [
    {
      'question':
          'The weather in Merizo is very (x) year-round, though there are showers almost daily from December through March.',
      'a': 'agreeable',
      'b': 'agree',
      'c': 'agreement',
      'd': 'agreeably',
      'correctAnswer': 'A'
    },
    {
      'question':
          '(x) for the competition should be submitted by November 28 at the latest.',
      'a': 'Enter',
      'b': 'Entered',
      'c': 'Entering',
      'd': 'Entries',
      'correctAnswer': 'D'
    }
  ];

correctAnswerのキーがあり、そこに正解の選択肢をa,b,c,d で表現しています。
今回、main.dartに新しく正答数の変数を宣言します。

main.dart

/// 正答数
var _correctAnswerCount = 0;

選択肢のボタンをタップして正解したときの処理もmain.dartに書きます。

main.dart

void _incrementCorrectIndex() {
    setState(() {
      _correctAnswerCount += 1;
    });
  }

正解したかどうかは選択肢ボタンのAnswerButtonクラスの中で処理するようにします。
AnswerButtonに_incrementCorrectIndexの関数を渡します。

main.dart

AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        incrementCorrect: _incrementCorrectIndex,
                        keyString: 'd')

AnswerButtonの引数を増やすために関数型Functionプロパティを宣言します。

answer_button.dart

import 'package:flutter/material.dart';
import 'package:flutter_quiz_app/utils/hex_color.dart';
import '../utils/constants.dart';

class AnswerButton extends StatelessWidget {
  final int questionIndex;
  final List<Map<String, Object>> questions;
  final Function answerQuestion;
  final Function incrementCorrect;  /// 追加する
  final String keyString;

  AnswerButton({
    this.questionIndex,
    this.questions,
    this.answerQuestion,
    this.incrementCorrect,  /// 追加する
    this.keyString
  });

  @override
  Widget build(BuildContext context) {
    return [略す]
  }
}

incrementCorrectプロパティは選択肢のkeyString(“a”, “b”, “c”, “d”が入っている前提)と問題変数の正解correctAnswerの文字列が等しいときに呼び出しますので

onPressed: () {
              /// "b".toUpperCase() == "D" みたいな感じ
              if (keyString.toUpperCase() == questions[questionIndex]["correctAnswer"].toString()) {
                incrementCorrect();
              }
              answerQuestion();
            }

このようにAnswerButtonのボタン処理部分を修正します。

変更後のAnswerButtonクラスの全概要は次のようになります。

answer_button.dart

import 'package:flutter/material.dart';
import 'package:flutter_quiz_app/utils/hex_color.dart';
import '../utils/constants.dart';

class AnswerButton extends StatelessWidget {
  final int questionIndex;
  final List<Map<String, Object>> questions;
  final Function answerQuestion;
  final Function incrementCorrect;
  final String keyString;

  AnswerButton({
    this.questionIndex,
    this.questions,
    this.answerQuestion,
    this.incrementCorrect,
    this.keyString
  });

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: BorderRadius.circular(Constants().answerButtonHeight / 2),
      child: SizedBox(
          width: double.infinity,
          height: Constants().answerButtonHeight,
          child: RaisedButton(
            color: HexColor('#D6D7D7'),
            child: Text(questions[questionIndex][keyString]),
            onPressed: () {
              /// "b".toUpperCase() == "D" みたいな感じ
              if (keyString.toUpperCase() == questions[questionIndex]["correctAnswer"].toString()) {
                incrementCorrect();
              }
              answerQuestion();
            },
          )),
    );
  }
}

問題に正解した時にincrementCorrectが呼ばれて正答数の変数_correctAnswerCountが1プラスされます。
これで正解したときの処理が出来上がりました。

リセット画面で正答数を表示させる

前回リセット画面のデザインを実装しました。
残りのタスクは

'正答数: 1 / 2'

の部分を変数展開のテクニックを使って反映させるだけですね。

Dartにおいて変数展開の仕方は割と簡単で、$hogeとすれば展開されます。PHPみたいで可愛いですね。
また、変数の式展開は${1 + 2}みたいに書けば展開できます。

final hoge = '1';
Text('$hoge')
Text('${1 + 2}')

こんな感じですね。
よってResultPageクラスに新しく正答数と問題数のプロパティを宣言すれば良いですね。

result_page.dart

class ResultPage extends StatelessWidget {
  /// タップ時の処理
  final Function _tapResetButton;

  /// 全問題数
  final int questionCount;

  /// 正答数
  final int correctAnswerCount;

  ResultPage(
      this._tapResetButton,
      this.questionCount,
      this.correctAnswerCount
      );
/// 省略

そして、Textのところは

Padding(
          padding: EdgeInsets.all(20),
          child: Text(
            '正答数: $correctAnswerCount / $questionCount',
            style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
          ),
        )

というふうに変数展開する形で入れてやればOKですね。
これで正答数を動的に変更することができました。
クイズアプリの出来上がりになります。

ソースコード

ということで、まとめとしてこれまで開発してきたクイズアプリで作成したクラスのソースコードを載せます。

プロジェクトディレクトの構成は添付画像の通りです。

constants.dart (定数クラス)

class Constants {
  /// 問題文ウィジェットの高さ
  final double questionAreaHeight = 70.0;
  /// 回答の選択肢ボタンの高さ
  final double answerButtonHeight = 50.0;
}

hex_color.dart (カラークラスの拡張)

import 'package:flutter/material.dart';

class HexColor extends Color {
  static int _getColorFromHex(String hexColor) {
    hexColor = hexColor.toUpperCase().replaceAll("#", "");
    if (hexColor.length == 6) {
      hexColor = "FF" + hexColor;
    }
    return int.parse(hexColor, radix: 16);
  }

  HexColor(final String hexColor) : super(_getColorFromHex(hexColor));
}

answer_button.dart (選択肢ボタン)

import 'package:flutter/material.dart';
import 'package:flutter_quiz_app/utils/hex_color.dart';
import '../utils/constants.dart';

class AnswerButton extends StatelessWidget {
  final int questionIndex;
  final List<Map<String, Object>> questions;
  final Function answerQuestion;
  final Function incrementCorrect;
  final String keyString;

  AnswerButton({
    this.questionIndex,
    this.questions,
    this.answerQuestion,
    this.incrementCorrect,
    this.keyString
  });

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: BorderRadius.circular(Constants().answerButtonHeight / 2),
      child: SizedBox(
          width: double.infinity,
          height: Constants().answerButtonHeight,
          child: RaisedButton(
            color: HexColor('#D6D7D7'),
            child: Text(questions[questionIndex][keyString]),
            onPressed: () {
              /// "b".toUpperCase() == "D" みたいな感じ
              if (keyString.toUpperCase() == questions[questionIndex]["correctAnswer"].toString()) {
                incrementCorrect();
              }
              answerQuestion();
            },
          )),
    );
  }
}

question_view.dart (問題文)

import 'package:flutter/material.dart';

class QuestionView extends StatelessWidget {
  /// 問題文のidかindexを渡す想定
  final int questionIndex;
  /// 問題文オブジェクト
  final List<Map<String, Object>> questions;

  QuestionView({@required this.questionIndex, @required this.questions});

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Center(
          child: Text(
            questions[questionIndex]['question'],
            style: TextStyle(fontSize: 20),
          ),
        ),
      ),
    );
  }
}

result_page.dart (リセット画面)

import 'package:flutter/material.dart';
import 'package:flutter_quiz_app/utils/hex_color.dart';

class ResultPage extends StatelessWidget {
  /// タップ時の処理
  final Function _tapResetButton;

  /// 全問題数
  final int questionCount;

  /// 正答数
  final int correctAnswerCount;

  ResultPage(
      this._tapResetButton,
      this.questionCount,
      this.correctAnswerCount
      );

  @override
  Widget build(BuildContext context) {
    final hoge = '1';
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Padding(
          padding: EdgeInsets.all(20),
          child: Text('$hoge',
              style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
        ),
        Padding(
          padding: EdgeInsets.all(20),
          child: Text(
            '正答数: $correctAnswerCount / $questionCount',
            style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
          ),
        ),
        Padding(
          padding: EdgeInsets.all(20),
          child: Container(
            height: 50,
            width: MediaQuery.of(context).size.width - 100,
            child: RaisedButton(
              color: HexColor('#6DDE00'),
              child: Text(
                'リセットする',
                style: TextStyle(color: Colors.white, fontSize: 17),
              ),
              onPressed: _tapResetButton,
            ),
          ),
        )
      ],
    );
  }
}

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_quiz_app/widget/result_page.dart';
import './utils/constants.dart';
import './widget/answer_button.dart';
import './widget/question_view.dart';


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
        fontFamily: "Hiragino Sans"
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  /// 問題文のindex
  var _questionIndex = 0;
  /// 正答数
  var _correctAnswerCount = 0;
  /// 問題
  var _questions = [
    {
      'question':
          'The weather in Merizo is very (x) year-round, though there are showers almost daily from December through March.',
      'a': 'agreeable',
      'b': 'agree',
      'c': 'agreement',
      'd': 'agreeably',
      'correctAnswer': 'A'
    },
    {
      'question':
          '(x) for the competition should be submitted by November 28 at the latest.',
      'a': 'Enter',
      'b': 'Entered',
      'c': 'Entering',
      'd': 'Entries',
      'correctAnswer': 'D'
    }
  ];

  void _answerQuestion() {
    setState(() {
      _questionIndex++;
    });
  }

  void _incrementCorrectIndex() {
    setState(() {
      _correctAnswerCount += 1;
    });
  }

  void _resetIndex() {
    setState(() {
      _questionIndex = 0;
      _correctAnswerCount = 0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('文法問題'),
      ),
      body: Center(
        child: _questionIndex < _questions.length ? Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: [
            Container(
              height: Constants().questionAreaHeight,
              child: Center(
                child: Text(
                  '(x)に入る単語を答えよ。',
                  style: TextStyle(fontSize: 20),
                ),
              ),
            ),
            Container(
              height: Constants().questionAreaHeight,
              child: Center(
                child: Text(
                  'Q${_questionIndex + 1}',
                  style: TextStyle(fontSize: 18),
                ),
              ),
            ),
            QuestionView(questionIndex: _questionIndex, questions: _questions),
            Expanded(
              child: Padding(
                padding: const EdgeInsets.fromLTRB(50.0, 30.0, 50.0, 50.0),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        incrementCorrect: _incrementCorrectIndex,
                        keyString: 'a'),
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        incrementCorrect: _incrementCorrectIndex,
                        keyString: 'b'),
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        incrementCorrect: _incrementCorrectIndex,
                        keyString: 'c'),
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        incrementCorrect: _incrementCorrectIndex,
                        keyString: 'd'),
                  ],
                ),
              ),
            )
          ],
        ) : ResultPage(_resetIndex, _questions.length, _correctAnswerCount),
      ),
    );
  }
}

以上がクイズアプリの全クラスのソースコードになります。
特にAPIは絡んでいませんので現地点2020/3/xx から3ヶ月ぐらいまでのFlutter SDKバージョンであれば動くと思いますので
今Flutter勉強中の方はこれをコピペして動かしてみてはいかがでしょうか。

私はこれでクイズアプリの開発はひとまず終了にします。

ABOUT ME
tamappe
都内で働くiOSアプリエンジニアのTamappeです。 当ブログではモバイルアプリの開発手法について紹介しています。メインはiOS、サブでFlutter, Android も対応できます。 執筆・講演のご相談は tamapppe@gmail.com までお問い合わせください。