Building an Interactive Quiz Widget with Timer and Score Tracking in Flutter
Interactive quizzes are an engaging way to test knowledge, provide feedback, and enhance user experience in mobile applications. Flutter, with its expressive UI and robust state management capabilities, is an excellent framework for developing such features. This article will guide you through the process of building a dynamic quiz widget in Flutter, complete with a countdown timer and a comprehensive score tracking system.
Key Components of an Interactive Quiz
- Question Presentation: Displaying questions and multiple-choice options clearly.
- User Interaction: Allowing users to select answers and providing immediate feedback.
- Timer Mechanism: Implementing a countdown timer for each question or the entire quiz.
- Score Tracking: Accumulating points based on correct answers.
- State Management: Efficiently managing the quiz's current question, timer, score, and overall flow.
- Result Display: Presenting the final score and an option to retake the quiz.
Project Setup and Data Model
First, ensure you have a basic Flutter project set up. We'll start by defining the data structure for our questions and their options.
Create a question.dart file (or similar) to define your Question and Option models:
// lib/models/question.dart
class Question {
final String questionText;
final List<Option> options;
final int timeLimitSeconds; // Optional: time limit per question
Question({
required this.questionText,
required this.options,
this.timeLimitSeconds = 15, // Default time per question
});
}
class Option {
final String text;
final bool isCorrect;
Option({required this.text, required this.isCorrect});
}
Next, let's create some dummy data to populate our quiz:
// lib/data/quiz_data.dart
import '../models/question.dart';
final List<Question> quizQuestions = [
Question(
questionText: 'What is the capital of France?',
options: [
Option(text: 'Berlin', isCorrect: false),
Option(text: 'Madrid', isCorrect: false),
Option(text: 'Paris', isCorrect: true),
Option(text: 'Rome', isCorrect: false),
],
),
Question(
questionText: 'Which planet is known as the Red Planet?',
options: [
Option(text: 'Earth', isCorrect: false),
Option(text: 'Mars', isCorrect: true),
Option(text: 'Jupiter', isCorrect: false),
Option(text: 'Venus', isCorrect: false),
],
),
Question(
questionText: 'What is 2 + 2?',
options: [
Option(text: '3', isCorrect: false),
Option(text: '4', isCorrect: true),
Option(text: '5', isCorrect: false),
Option(text: '6', isCorrect: false),
],
),
];
Building the Quiz Widget (Stateful Widget)
The core of our quiz will be a StatefulWidget to manage the various states: current question index, score, and timer.
// lib/widgets/quiz_widget.dart
import 'dart:async';
import 'package:flutter/material.dart';
import '../models/question.dart';
import '../data/quiz_data.dart';
class QuizWidget extends StatefulWidget {
@override
_QuizWidgetState createState() => _QuizWidgetState();
}
class _QuizWidgetState extends State<QuizWidget> {
int _currentQuestionIndex = 0;
int _score = 0;
bool _quizFinished = false;
Timer? _timer;
int _secondsRemaining = 0;
List<Question> _questions = [];
@override
void initState() {
super.initState();
_questions = quizQuestions; // Load questions
_startQuiz();
}
void _startQuiz() {
_currentQuestionIndex = 0;
_score = 0;
_quizFinished = false;
_loadQuestionTimer();
}
void _loadQuestionTimer() {
if (_currentQuestionIndex < _questions.length) {
_timer?.cancel(); // Cancel any existing timer
setState(() {
_secondsRemaining = _questions[_currentQuestionIndex].timeLimitSeconds;
});
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_secondsRemaining > 0) {
setState(() {
_secondsRemaining--;
});
} else {
timer.cancel();
_goToNextQuestion(); // Time's up, move to next question
}
});
} else {
_timer?.cancel();
setState(() {
_quizFinished = true;
});
}
}
void _answerQuestion(Option selectedOption) {
if (_quizFinished) return; // Prevent answering after quiz is done or time is up
_timer?.cancel(); // Stop timer immediately after answering
if (selectedOption.isCorrect) {
setState(() {
_score++;
});
}
_goToNextQuestion();
}
void _goToNextQuestion() {
setState(() {
if (_currentQuestionIndex < _questions.length - 1) {
_currentQuestionIndex++;
_loadQuestionTimer(); // Start timer for the next question
} else {
_quizFinished = true;
_timer?.cancel();
}
});
}
void _resetQuiz() {
setState(() {
_startQuiz();
});
}
@override
void dispose() {
_timer?.cancel(); // Important: Cancel timer to prevent memory leaks
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_quizFinished) {
return _buildQuizResult();
} else {
return _buildQuestionScreen();
}
}
Widget _buildQuestionScreen() {
final Question currentQuestion = _questions[_currentQuestionIndex];
return Card(
margin: EdgeInsets.all(16),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Align(
alignment: Alignment.topRight,
child: Text(
'Time: \u{23F1} $_secondsRemaining s', // Timer icon
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
SizedBox(height: 10),
Text(
'Question ${_currentQuestionIndex + 1}/${_questions.length}',
style: TextStyle(fontSize: 16, color: Colors.grey[700]),
),
SizedBox(height: 10),
Text(
currentQuestion.questionText,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
SizedBox(height: 20),
...currentQuestion.options.map((option) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ElevatedButton(
onPressed: () => _answerQuestion(option),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
option.text,
style: TextStyle(fontSize: 18),
),
),
)).toList(),
SizedBox(height: 20),
Text(
'Current Score: $_score',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildQuizResult() {
return Card(
margin: EdgeInsets.all(16),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Quiz Finished!',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
SizedBox(height: 20),
Text(
'Your Score: $_score / ${_questions.length}',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
SizedBox(height: 30),
ElevatedButton(
onPressed: _resetQuiz,
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
'Retake Quiz',
style: TextStyle(fontSize: 18),
),
),
],
),
),
);
}
}
Integrating the Quiz Widget
Finally, integrate your QuizWidget into your main.dart or any other screen in your application.
// lib/main.dart
import 'package:flutter/material.dart';
import 'widgets/quiz_widget.dart'; // Make sure this path is correct
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Interactive Quiz App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Quiz'),
),
body: Center(
child: QuizWidget(), // Your interactive quiz widget
),
),
);
}
}
Explanation of Key Concepts
_QuizWidgetStateState Variables:_currentQuestionIndex: Tracks the index of the currently displayed question._score: Stores the user's correct answers count._quizFinished: A boolean to determine if the quiz has ended._timerand_secondsRemaining: Manage the countdown timer for each question.
initStateanddispose:initStateis used to load questions and start the quiz, including the first timer.disposeis crucial for cancelling theTimerto prevent resource leaks when the widget is removed from the widget tree.Timer.periodic: This static method creates a recurring timer that fires an event every specified duration. Here, it updates_secondsRemainingevery second._answerQuestionLogic: When an option is selected, the current timer is cancelled, the score is updated if the answer is correct, and_goToNextQuestionis called._goToNextQuestionLogic: Advances the_currentQuestionIndexand calls_loadQuestionTimerfor the next question. If all questions are answered,_quizFinishedis set totrue.- Conditional UI Rendering: The
buildmethod conditionally renders either the question screen (_buildQuestionScreen) or the quiz result screen (_buildQuizResult) based on the_quizFinishedstate.
Further Enhancements
- Visual Feedback: Show immediate feedback (e.g., green for correct, red for incorrect) after an answer is selected.
- Progress Indicator: Add a progress bar or indicator to show how many questions are left.
- Difficulty Levels: Implement different sets of questions based on difficulty.
- Question Shuffling: Randomize the order of questions and options.
- Persistence: Save quiz scores or user progress using
shared_preferencesor a database. - Advanced State Management: For larger apps, consider using Provider, Riverpod, or BLoC for more robust state management.
Conclusion
Building an interactive quiz widget in Flutter is a straightforward process when broken down into manageable components. By leveraging StatefulWidget for state management, Timer.periodic for countdowns, and clear data models, you can create engaging and functional quizzes. This article provides a solid foundation, and you can further expand upon these concepts to build even more sophisticated and feature-rich interactive experiences for your users.