🐒

【Flutter】簡単なクイズアプリを作る

2022/05/09に公開

初めに

Flutterのチュートリアルに最適だと思い,4択のクイズアプリを実装したのでメモ

このようなクイズアプリを作ります。

alt


とても地味な見た目です。画像を表示させたり、◯×を大きくしたり,問題文を変えたり、たくさんカスタマイズするといいと思います。

最終的にlibファイルの中身はこのようになります。

lib
├── main.dart
├── model
│   └── quiz.dart
├── service
│   ├── load_csv.dart
│   └── suffle.dart
└── view
    ├── quiz_app.dart
    ├── quiz_page.dart
    └── result_page.dart

実装


$ flutter create quiz  

$ cd quiz

$ flutter run

依存パッケージ

pubspec.yaml
flutter:
  uses-material-design: true
  assets:
    - assets/

クイズの問題はcsvファイルをassetsフォルダ内に入れ,そこから読み込むため.

最初に呼び出すページ

main.dart
//main.dart

import 'package:flutter/material.dart';
import 'package:quiz/view/quiz_app.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return MaterialApp(home: QuizApp());
  }
}

エラーが出ると思いますが,気にしないでください.

最初にmain関数を呼び出し,MyAppを呼び出します.そこではQuizAppを返します.

次にこのアプリのメイン.クイズのスタートページであるQuizAppを実装します.

view/quiz_app.dart

//view/quiz_app.dart

import 'package:flutter/material.dart';
import 'package:quiz/service/load_csv.dart';
import 'package:quiz/service/suffle.dart';
import 'package:quiz/view/quiz_page.dart';

class QuizApp extends StatelessWidget {
  QuizApp({Key? key}) : super(key: key);
  late List<Map> quizList;

  Future<void> goToQuizApp(BuildContext context) async {
    quizList = shuffle(await getCsvData('assets/quiz1.csv'));
    for (Map row in quizList) {
      debugPrint(row["question"]);
    }

    Navigator.push(
        context, MaterialPageRoute(builder: (context) => QuizPage(quizList)));
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          //Columnの中に入れたものは縦に並べられる.Rowだと横に並べられる
          mainAxisAlignment: MainAxisAlignment.center, //Coloumの中身を真ん中に配置
          children: <Widget>[
            const Text(
              'クイズ',
            ),
            ElevatedButton(
                onPressed: () {
                  goToQuizApp(context); //クイズアプリへ遷移するQuizApp関数がよばれる
                },
                child: const Text('スタート')),
          ],
        ),
      ),
    );
  }
}

goToQuizApp関数ではクイズアプリのメイン画面であるクイズを解く画面quizPageへ移動します.

しかし,その関数の中では,このアプリで最も重要と言っても過言ではない関数 getCsvData関数とshuffle関数がよばれています.

getCsvData関数ではcsvファイルから問題を取り出して,Quizクラス(問題や選択し,回答が含まれたクラス)を作成し,Quizクラスをmapに変換し,

quizListというリストにそのmapを追加していきます.

model/quiz.dart

//model/quiz.dart

class Quiz {
  String question;
  int answer;
  String select0;
  String select1;
  String select2;
  String select3;

  Map<String, dynamic> toMap() {
    return {
      'question': question,
      'answer': answer,
      'select0': select0,
      'select1': select1,
      'select2': select2,
      'select3': select3,
    };
  }

  Quiz(
    this.question,
    this.answer,
    this.select0,
    this.select1,
    this.select2,
    this.select3,
  );
}

service/load_csv.dart

//service/load_csv.dart

import 'package:flutter/services.dart';
import 'package:quiz/model/quiz.dart';

Future<List<Map>> getCsvData(String path) async {
  List<Map> quizList = [];
  String csv = await rootBundle.loadString(path);
  for (String line in csv.split("\n")) {
    if (quizList.length + 1 == csv.split("\n").length) {
      break;
    }
    List rows = line.split(',');
    Quiz quiz = Quiz(
      rows[0],
      int.parse(rows[1]),
      rows[2],
      rows[3],
      rows[4],
      rows[5],
    );

    quizList.add(quiz.toMap());
  }
  return quizList;
}


shuffle関数では,getCsvDataで得られたリストquizListの並びをシャッフルすることで,毎回違う順番でクイズの問題が出ることになります.

service/suffle.dart

import 'dart:math';

List<Map> shuffle(List<Map> items) {
  var random = Random();
  for (var i = items.length - 1; i > 0; i--) {
    var n = random.nextInt(i + 1);
    var temp = items[i];
    items[i] = items[n];
    items[n] = temp;
  }
  return items;
}

goToQuizApp関数ではクイズアプリのメイン画面であるクイズを解く画面quizPageへ移動します.

quizPageでは,上記で解説したQuizListを渡しています.

次にそのquizPageの実装をします

view/quiz_page.dart

//view/quiz_page.dart

import 'package:flutter/material.dart';
import 'package:quiz/view/result_page.dart';

class QuizPage extends StatefulWidget {
  QuizPage(this.quizList, {Key? key}) : super(key: key);
  List<Map> quizList;

  
  State<QuizPage> createState() => QuizPageState();
}

class QuizPageState extends State<QuizPage> {
  late List<Map> quizList;
  int index = 0;
  int result = 0;
  bool isSelectNow = true;

  
  void initState() {
    quizList = widget.quizList;
    super.initState();
  }

  Future<void> updateQuiz(BuildContext context, int selectAnswer) async {
    setState(() {
      isSelectNow = false;
    });
    if (quizList[index]["answer"] == selectAnswer) {
      result++;
    }

    await Future.delayed(const Duration(seconds: 1));
    isSelectNow = true;
    setState(() {});
    index++;
    if (index == quizList.length) {
      await goToResult(context);
    }
    setState(() {});
  }

  Future<void> goToResult(BuildContext context) async {
    Navigator.push(
        context,
        MaterialPageRoute(
            builder: (context) => Result(result, quizList.length)));
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: index < quizList.length
          ? CustomScrollView(
              slivers: <Widget>[
                SliverList(
                    delegate: SliverChildListDelegate([
                  Padding(
                      padding: EdgeInsets.only(
                          top: MediaQuery.of(context).size.height / 3)),
                  Text(
                    quizList[index]['question'],
                    textAlign: TextAlign.center,
                  ),
                ])),
                SliverList(
                    delegate: SliverChildBuilderDelegate(
                  (context, key) {
                    return TextButton(
                        onPressed: () async {
                          if (!isSelectNow) return;
                          await updateQuiz(context, key);
                        },
                        child: isSelectNow
                            ? Text(quizList[index]["select$key"])
                            : quizList[index]["answer"] == key
                                ? Text(quizList[index]["select$key"] + "○")
                                : Text(quizList[index]["select$key"] + "×"));
                  },
                  childCount: 4,
                )),
              ],
            )
          : Container(),
    );
  }
}

選択肢を押すと,updateQuiz関数がよばれ,isSelectNowがfalseになります

1秒間選択肢ボタンを押しても何も起こらなくなり,その間はその答えが○か×か表示されます

クイズの数はindexで管理しています.

indexがquizList.lengthと同じになると,goToQuizApp関数がよばれ,result画面であるresultPageへ移動します.

次にそのresultPageの実装をします

view/result_page.dart
import 'package:flutter/material.dart';

class Result extends StatelessWidget {
  Result(this.result, this.quizNumber, {Key? key}) : super(key: key);
  int result;
  int quizNumber;
  late String comment;

  Future<void> goToTop(BuildContext context) async {
    Navigator.popUntil(context, (route) => route.isFirst);
  }

  
  Widget build(BuildContext context) {
    switch (result.round() * 100 ~/ quizNumber) {
      case 60:
        comment = "まあまあ";
        break;
      case 70:
        comment = "まあまあ";
        break;
      case 80:
        comment = "いいね";
        break;
      case 90:
        comment = "すごい";
        break;
      case 100:
        comment = "よくできました";
        break;
      default:
        comment = "頑張りましょう";
        break;
    }
    print("${result / quizNumber * 100}");

    return Scaffold(
      body: Center(
        child: Column(
          //Columnの中に入れたものは縦に並べられる.Rowだと横に並べられる
          mainAxisAlignment: MainAxisAlignment.center, //Coloumの中身を真ん中に配置
          children: <Widget>[
            Text(comment),
            Text('正答数$result'),
            Text('正答率${result / quizNumber * 100}%'),
            ElevatedButton(
                onPressed: () async {
                  await goToTop(context);
                },
                child: const Text('トップへ戻る')),
          ],
        ),
      ),
    );
  }
}

assets/quiz1.csv

//assets/quiz1.csv

実際に北海道に存在する川はどれ?,2,イトオシイ川,クルオシイ川,ヤリキレナイ川,チョウシデナイ川
天空の城ラピュタのムスカ大佐の年齢はいくつでしょう?,0,28歳,32歳,58歳,62歳
サザエさんに出てくるアナゴさんの年齢は?,0,28歳,32歳,58歳,62歳
ドラゴンボールのべジータは何cmでしょう?,1,159cm,164cm,179cm,184cm
世界で最も歌われている曲は次のうちどれ?,0,ハッピーバースデートゥーユー,きらきら星,ジングルベル,アメージンググレイス
俳句の季語として認定されていないものはどれ?,1,サザン,チューブ,ユーミン,山下達郎
黄色のカーネーションの花言葉は次のうちどれ?,3,家族愛,感謝,嫉妬,軽蔑
マジ?という言葉は一体いつからあったでしょう?,0,江戸時代,明治時代,昭和,平成

csvファイルは,上記のように最後の行に改行を入れてください.

頭は詰めてかいてください.

リポジトリ

https://github.com/maropook/flutter-quiz/

Discussion