🎉

【Flutter】ランダムに問題を10個取ってきて抜き打ちテストをするページの実装

2023/03/05に公開

やりたいこと

  • ランダムに抜き打ちテストをしたい。全問題から10個選んで出題する。
    firebaseにはランダムにデータを取ってくる処理は無い?ようなので、
  1. このページがビルドされるたびにDart側で「全問題数から10問takeする」処理を実行する

  2. その処理で得た番号と同じquestion_idをquestionsコレクションからQuery検索として(whereメソッド)取ってくる。(問題はQuestionsコレクション(テーブル)に持たせる)

って感じにしているのですがご意見あれば教えていただきたいです

個人的ポイント

  • 全問題数はこのページに飛んでくる際にすでに渡している状態にしたい
unannounced_test_page.dart
class UnannouncedTestPage extends StatelessWidget {
  final int totalNumberOfQuestions;//questionsテーブルの全問題数を取得
  //このページに飛んでくる時にFirebaseから全問題数を取得。
  • 全問題内のランダムな数字を生成して10個ランダムに選ぶ処理(takeメソッド)
unannounced_test_page.dart

  Widget build(BuildContext context) {
    final List <String> randomNumberList = [];// ランダムな"数字"を格納する配列
    for (var i = 1; i < totalNumberOfQuestions + 1; i++) {//全問題数分だけ足していく。
    //["1", "2", "3", "4", "5".....]的な感じ。あくまで数字も文字列にしているがこれに関しては設計ミスかなあ。。
      randomNumberList.add("$i");
    }
    randomNumberList.shuffle();// 上のリストをシャッフルして並び替える(ここで寓意性を確立させている。つもりである)
    final List<String> gotRandomNumberList = randomNumberList.take(10).toList();//ここで先頭から10個取り出して新しいListとする。
    //toListがないとIterable型になる(はず)なのでtoListメソッドでList型にしておく
  • ランダムに作ったListを含むものを取ってきてFuturemとする。(後でFutureBuilderのfutureプロパティに渡す)
unannounced_test_page.dart
//ランダムに作ったListを含むものを取ってきて表示
    final questionFuture = QuestionFireStore.questions.where("question_id", whereIn: gotRandomNumberList).get();
    final answerFuture = AnswerFireStore.answers.where('answer_id', whereIn: gotRandomNumberList).get();

↓それ以外のところは今回重要ではないので見せておくだけにします↓

サンプルコード

unannounced_test_page.dart
import 'いっぱいあるが割愛';
class UnannouncedTestPage extends StatelessWidget {
  final int totalNumberOfQuestions;//questionsテーブルの全問題数を取得
  //このページに飛んでくる時にFirebaseから全問題数を取得。
  const UnannouncedTestPage({Key? key, required this.totalNumberOfQuestions})
      : super(key: key);

  
  Widget build(BuildContext context) {
    final Size size = MediaQuery.of(context).size;
    final List <String> randomNumberList = [];// ランダムな"question_id"を格納する配列

    for (var i = 1; i < totalNumberOfQuestions + 1; i++) {
      //iは総問題数(totalNumberOfQuestions)個分。
      randomNumberList.add("$i");
    }
    randomNumberList.shuffle();
    final List<String> gotRandomNumberList = randomNumberList.take(10).toList();

    final questionFuture = QuestionFireStore.questions.where("question_id", whereIn: gotRandomNumberList).get();
    final answerFuture = AnswerFireStore.answers.where('answer_id', whereIn: gotRandomNumberList).get();

    return Scaffold(
      body: Consumer(builder: (context, ref, child){
        return Center(
          child: SafeArea(
            child: FutureBuilder<QuerySnapshot>(
                future: questionFuture,
                builder: (context, snapshot) {
                  //現在の問題のIndexを管理。問題の番数はこれ+1。
                  final int currentQuestionIndex = ref.watch(currentQuestionIndexProvider);
                  final bool isAnswered = ref.watch(buttonProvider);//回答したか
                  final bool isCorrect = ref.watch(isCorrectProvider);//正解or不正解
                  if(snapshot.hasData){//これ忘れると「null check Operator」の例外吐かれるので対策しておく
                    int questionLength = snapshot.data!.size;//問題のListの長さを先に取得する
                    print("questionLength:$questionLength");
                    print("question_id:${snapshot.data!.docs[currentQuestionIndex].get("question_id")}");
                    print("currentQuestionIndex:$currentQuestionIndex");
                    return Column(
                      children: [
                        Container(
                            width: double.infinity,
                            padding: const EdgeInsets.only(top: 40),
                            child: Center(
                                child: Text(isAnswered ? "" : "${currentQuestionIndex + 1} 問目",
                                  style: const TextStyle(fontSize: 30),)
                            )),
                        Container(
                            color: AppColors.mainBlue,
                            width: double.infinity, height: size.height * 0.28,
                            child: Center(
                              //questionのタイトル
                              child: isAnswered
                                  ? Container(height: 150,
                                  child: Image.asset(
                                    //TODO:正解と不正解のImageを作って貼り付ける。一旦はpngで作る
                                      isCorrect ?
                                      'images/main_image.png':
                                      'images/main_image.png'
                                  ))
                                  : Text(snapshot.data!.docs[currentQuestionIndex].get("title"),
                                  style: TextStyle(fontSize: 26)),
                            )
                        ),
                        Expanded(
                          child: Container(
                            padding: EdgeInsets.fromLTRB(12,12,12,0),
                            child: FutureBuilder<QuerySnapshot>(
                                future: answerFuture,
                                builder: (context, snapshot) {
                                  if(snapshot.hasData) {
                                    return
                                      isAnswered == false ? //未解答状態の時
                                      GridView.count(
                                          physics: const NeverScrollableScrollPhysics(),
                                          //上記でスクロール固定
                                          crossAxisCount: 2,
                                          mainAxisSpacing: 24,
                                          crossAxisSpacing: 24,
                                          children: List.generate(4, (index) => GestureDetector(
                                            onTap: () async{
                                              //Tap時に先にやることは正誤判定を行うこと
                                              if(index.toString() == snapshot.data!.docs[currentQuestionIndex].get("correct_answer_index_number")){
                                                //正答時
                                                ref.read(isCorrectProvider.notifier).state = true;
                                                ref.read(numberOfCorrectAnswersProvider.notifier).state++;//正答数を+1
                                              }
                                              if(index.toString() != snapshot.data!.docs[currentQuestionIndex].get("correct_answer_index_number")){
                                                //不正答時
                                                ref.read(isCorrectProvider.notifier).state = false;
                                              }
                                              if(ref.read(currentQuestionIndexProvider) < questionLength - 1){
                                                //回答題が最後の問題ではない時→回答して次の問題へ
                                                ref.read(buttonProvider.notifier).state = true;
                                                //解答状態(isAnswered)をfalse => trueにする処理。
                                                ref.read(currentQuestionIndexProvider.notifier).state++;
                                              }
                                              if(currentQuestionIndex == questionLength - 1){
                                                //回答題が最後の問題の時→Result画面へ
                                                Navigator.pushReplacement(context, MaterialPageRoute(
                                                    builder: (context)=> QuestionResultPage(
                                                        questionLength: currentQuestionIndex + 1,
                                                        numberOfCorrectAnswers: ref.read(numberOfCorrectAnswersProvider)
                                                    ))
                                                );
                                                ref.refresh(currentQuestionIndexProvider.notifier).state;
                                              }
                                              print(ref.read(numberOfCorrectAnswersProvider));
                                            },
                                            child: Card(
                                                shape: RoundedRectangleBorder(
                                                  borderRadius: BorderRadius.circular(10),
                                                ),
                                                color: AppColors.mainWhite,
                                                elevation: 5,
                                                child: Center(
                                                    child: Text('${snapshot.data!.docs[currentQuestionIndex].get("answer${index + 1}")}', style: AppTextStyles.textBold)
                                                )
                                            ),
                                          ))
                                      ) :
                                      GestureDetector(//解答すると
                                        onTap: (){
                                          ref.read(buttonProvider.notifier).state = false;
                                        },
                                        child: Center(
                                            child: Column(
                                              mainAxisAlignment: MainAxisAlignment.start,
                                              children: [
                                                Expanded(
                                                  child: Container(
                                                    padding: EdgeInsets.fromLTRB(20, 0, 20, 50),
                                                    child: Center(
                                                        child: Text(
                                                          "${snapshot.data!.docs[currentQuestionIndex - 1].get("commentary")}",
                                                          //この処理より先にquestionCounterが++されてしまうので-1しておくことで帳尻合わせる。
                                                          style: TextStyle(fontSize: 22),)),
                                                    color: AppColors.subGreen,
                                                  ),
                                                ),
                                              ],
                                            )),
                                      );
                                  } else {
                                    return Container();
                                  }
                                }
                            ),
                          ),
                        )
                      ],
                    );
                  } else {
                    return const NormalCircularIndicator();
                  }
                }
            ),
          ),
        );
      })
    );
  }
}
GitHubで編集を提案

Discussion