feat: quiz play logic

This commit is contained in:
akhdanre 2025-04-28 19:30:09 +07:00
parent d4d9f0d85d
commit b80303b9c0
7 changed files with 317 additions and 1 deletions

View File

@ -13,6 +13,8 @@ import 'package:quiz_app/feature/navigation/views/navbar_view.dart';
import 'package:quiz_app/feature/profile/binding/profile_binding.dart';
import 'package:quiz_app/feature/quiz_creation/binding/quiz_creation_binding.dart';
import 'package:quiz_app/feature/quiz_creation/view/quiz_creation_view.dart';
import 'package:quiz_app/feature/quiz_play/binding/quiz_play_binding.dart';
import 'package:quiz_app/feature/quiz_play/view/quiz_play_view.dart';
import 'package:quiz_app/feature/quiz_preview/binding/quiz_preview_binding.dart';
import 'package:quiz_app/feature/quiz_preview/view/quiz_preview.dart';
import 'package:quiz_app/feature/register/binding/register_binding.dart';
@ -71,6 +73,11 @@ class AppPages {
name: AppRoutes.detailQuizPage,
page: () => DetailQuizView(),
binding: DetailQuizBinding(),
),
GetPage(
name: AppRoutes.playQuizPage,
page: () => QuizPlayView(),
binding: QuizPlayBinding(),
)
];
}

View File

@ -12,4 +12,6 @@ abstract class AppRoutes {
static const quizPreviewPage = "/quiz/preview";
static const detailQuizPage = "/quiz/detail";
static const playQuizPage = "/quiz/play";
}

View File

@ -1,4 +1,5 @@
import 'package:get/get.dart';
import 'package:quiz_app/app/routes/app_pages.dart';
import 'package:quiz_app/data/models/quiz/library_quiz_model.dart';
class DetailQuizController extends GetxController {
@ -12,4 +13,6 @@ class DetailQuizController extends GetxController {
void loadData() {
data = Get.arguments as QuizData;
}
void goToPlayPage() => Get.toNamed(AppRoutes.playQuizPage, arguments: data);
}

View File

@ -70,7 +70,7 @@ class DetailQuizView extends GetView<DetailQuizController> {
const SizedBox(height: 20),
const SizedBox(height: 20),
GlobalButton(text: "Kerjakan", onPressed: () {}),
GlobalButton(text: "Kerjakan", onPressed: controller.goToPlayPage),
const SizedBox(height: 20),
GlobalButton(text: "buat ruangan", onPressed: () {}),

View File

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import 'package:quiz_app/feature/quiz_play/controller/quiz_play_controller.dart';
class QuizPlayBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<QuizPlayController>(() => QuizPlayController());
}
}

View File

@ -0,0 +1,164 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:quiz_app/core/utils/logger.dart';
import 'package:quiz_app/data/models/quiz/library_quiz_model.dart';
import 'package:quiz_app/data/models/quiz/question_listings_model.dart';
class QuizPlayController extends GetxController {
late final QuizData quizData;
final currentIndex = 0.obs;
final timeLeft = 0.obs;
final isStarting = true.obs;
final isAnswerSelected = false.obs;
final selectedAnswer = ''.obs;
final List<AnsweredQuestion> answeredQuestions = [];
final answerTextController = TextEditingController();
final choosenAnswerTOF = 0.obs;
Timer? _timer;
QuestionListing get currentQuestion => quizData.questionListings[currentIndex.value];
@override
void onInit() {
super.onInit();
quizData = Get.arguments as QuizData;
_startCountdown();
// Listener untuk fill the blank
answerTextController.addListener(() {
if (answerTextController.text.trim().isNotEmpty) {
isAnswerSelected.value = true;
} else {
isAnswerSelected.value = false;
}
});
// Listener untuk pilihan true/false
ever(choosenAnswerTOF, (value) {
if (value != 0) {
isAnswerSelected.value = true;
}
});
}
void _startCountdown() async {
await Future.delayed(const Duration(seconds: 3));
isStarting.value = false;
_startTimer();
}
void _startTimer() {
timeLeft.value = currentQuestion.duration;
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (timeLeft.value > 0) {
timeLeft.value--;
} else {
_submitAnswerIfNeeded();
_nextQuestion();
}
});
isAnswerSelected.value = false;
}
void selectAnswer(String answer) {
selectedAnswer.value = answer;
isAnswerSelected.value = true;
}
void onChooseTOF(bool value) {
choosenAnswerTOF.value = value ? 1 : 2;
selectedAnswer.value = value ? "Ya" : "Tidak";
}
void _submitAnswerIfNeeded() {
final question = currentQuestion;
String userAnswer = "";
if (question.type == "fill_the_blank") {
userAnswer = answerTextController.text.trim();
} else if (question.type == "option") {
userAnswer = selectedAnswer.value.trim();
} else if (question.type == "true_false") {
userAnswer = selectedAnswer.value.trim();
}
// Masukkan ke answeredQuestions
answeredQuestions.add(AnsweredQuestion(
index: currentIndex.value,
question: question.question,
selectedAnswer: userAnswer,
correctAnswer: question.targetAnswer.trim(),
isCorrect: userAnswer.toLowerCase() == question.targetAnswer.trim().toLowerCase(),
));
}
void nextQuestion() {
_submitAnswerIfNeeded();
_nextQuestion();
}
void _nextQuestion() {
_timer?.cancel();
if (currentIndex.value < quizData.questionListings.length - 1) {
currentIndex.value++;
answerTextController.clear();
selectedAnswer.value = '';
choosenAnswerTOF.value = 0;
_startTimer();
} else {
_finishQuiz();
}
}
void _finishQuiz() {
_timer?.cancel();
logC.i(answeredQuestions.map((e) => e.toJson()).toList());
Get.defaultDialog(
title: "Selesai",
middleText: "Kamu telah menyelesaikan semua soal!",
onConfirm: () {
Get.back();
Get.back();
},
textConfirm: "OK",
);
}
@override
void onClose() {
_timer?.cancel();
answerTextController.dispose();
super.onClose();
}
}
class AnsweredQuestion {
final int index;
final String question;
final String selectedAnswer;
final String correctAnswer;
final bool isCorrect;
AnsweredQuestion({
required this.index,
required this.question,
required this.selectedAnswer,
required this.correctAnswer,
required this.isCorrect,
});
Map<String, dynamic> toJson() {
return {
'index': index,
'question': question,
'selectedAnswer': selectedAnswer,
'correctAnswer': correctAnswer,
'isCorrect': isCorrect,
};
}
}

View File

@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:quiz_app/component/global_text_field.dart';
import 'package:quiz_app/feature/quiz_play/controller/quiz_play_controller.dart';
class QuizPlayView extends GetView<QuizPlayController> {
const QuizPlayView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF9FAFB),
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
automaticallyImplyLeading: false,
title: const Text(
'Kerjakan Soal',
style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Obx(() {
final question = controller.currentQuestion;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LinearProgressIndicator(
value: controller.timeLeft.value / question.duration,
minHeight: 8,
backgroundColor: Colors.grey[300],
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF2563EB)),
),
const SizedBox(height: 20),
Text(
'Soal ${controller.currentIndex.value + 1} dari ${controller.quizData.questionListings.length}',
style: const TextStyle(
fontSize: 16,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 12),
Text(
question.question,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 30),
// Jawaban Berdasarkan Tipe Soal
if (question.type == 'option' && question.options != null)
...List.generate(question.options!.length, (index) {
final option = question.options![index];
return Container(
margin: const EdgeInsets.only(bottom: 12),
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.grey),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () => controller.selectAnswer(option),
child: Text(option),
),
);
})
else if (question.type == 'true_false')
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildTrueFalseButton('Ya', true, controller.choosenAnswerTOF),
_buildTrueFalseButton('Tidak', false, controller.choosenAnswerTOF),
],
)
else
GlobalTextField(controller: controller.answerTextController),
const Spacer(),
Obx(() {
return ElevatedButton(
onPressed: controller.nextQuestion,
style: ElevatedButton.styleFrom(
backgroundColor: controller.isAnswerSelected.value ? const Color(0xFF2563EB) : Colors.grey,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'Next',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
);
}),
],
);
}),
),
),
);
}
Widget _buildTrueFalseButton(String label, bool value, RxInt choosenAnswer) {
return Obx(() {
bool isSelected = (choosenAnswer.value == (value ? 1 : 2));
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: isSelected ? (value ? Colors.green[100] : Colors.red[100]) : Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.grey),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () => controller.onChooseTOF(value),
icon: Icon(value ? Icons.check_circle_outline : Icons.cancel_outlined),
label: Text(label),
);
});
}
}