Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Create an Interactive Quiz with Scoring in FlutterFlow

Build an interactive quiz in FlutterFlow by storing quizzes and questions in Firestore subcollections. A PageView displays one question at a time with RadioButton answer options. Score tracking lives in Page State. Answer correctness is verified in a Cloud Function — never in the client — to prevent cheating. A results page shows score breakdown, and a leaderboard ranks all players by score.

What you'll learn

  • Design a Firestore schema with quizzes and questions subcollections
  • Display questions in a PageView with RadioButton answer selection
  • Track scores in Page State and verify answers server-side via Cloud Function
  • Build a results page with per-question breakdown and a live leaderboard
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner10 min read45-60 minFlutterFlow Free+March 2026RapidDev Engineering Team
TL;DR

Build an interactive quiz in FlutterFlow by storing quizzes and questions in Firestore subcollections. A PageView displays one question at a time with RadioButton answer options. Score tracking lives in Page State. Answer correctness is verified in a Cloud Function — never in the client — to prevent cheating. A results page shows score breakdown, and a leaderboard ranks all players by score.

Tamper-Proof Multi-Question Quiz with Live Leaderboard

This tutorial builds a complete quiz experience in FlutterFlow. Quizzes and questions are stored in Firestore. A PageView presents one question per page with answer RadioButtons. The key design decision is answer verification: the correct answer is never sent to the client. Instead, the client sends the user's selected answer to a Cloud Function, which checks it against the Firestore question document (accessible only to server-side code via Admin SDK), returns a boolean result, and the app updates the score. After the last question, a results page shows a question-by-question breakdown. A leaderboard subcollection stores each user's best score, rendered as a real-time ListView sorted by score descending.

Prerequisites

  • FlutterFlow account with Firebase connected and Firestore enabled
  • Firebase Authentication enabled
  • Firebase Cloud Functions enabled (for server-side answer checking)
  • Basic familiarity with FlutterFlow Backend Queries and Custom Functions

Step-by-step guide

1

Design the Firestore Schema for Quizzes and Questions

In Firestore, create a quizzes collection with fields: title (String), description (String), category (String), totalQuestions (Integer), timeLimit (Integer — seconds per question, 0 for no limit), createdAt (Timestamp), isActive (Boolean). Under each quiz document, create a questions subcollection with fields: questionText (String), answers (Array of Strings — the answer options to display, shuffled), imageUrl (String — optional question image), order (Integer — display sequence). Notice that correctIndex (Integer — the index of the correct answer) is NOT included in this schema. Store correctIndex in a separate server-only correctAnswers subcollection under each quiz, with a matching document ID to the question. Firestore Security Rules will block clients from reading correctAnswers — only the Cloud Function can read them using the Admin SDK.

Expected result: Firestore has a quizzes collection with a questions subcollection. Clients can read questions but cannot read the correctAnswers subcollection.

2

Build the Quiz Screen with PageView and RadioButtons

Create a QuizScreen page with parameters: quizId (String) and quizTitle (String). Add Page State variables: currentQuestionIndex (Integer, default 0), selectedAnswerIndex (Integer, default -1 meaning no selection), score (Integer, default 0), correctCount (Integer, default 0), and answeredQuestions (List of Maps — stores questionId, selectedIndex, isCorrect for each answered question). Add a Backend Query at the page level that fetches the questions subcollection for the quizId, ordered by the order field. Add a PageView widget with its controller bound to currentQuestionIndex. Inside the PageView, create a question card with: a question number indicator Text, the questionText Text, an optional Image widget conditionally visible when imageUrl is not empty, and a Column of RadioButton widgets for each answer option. Each RadioButton sets selectedAnswerIndex to its index when tapped.

Expected result: The quiz shows one question at a time in a PageView. Tapping an answer option selects it and highlights the chosen RadioButton.

3

Verify Answers Server-Side with a Cloud Function

Create a Cloud Function named checkAnswer. It accepts quizId (String), questionId (String), selectedIndex (Integer), and userId (String). The function reads the correctAnswers subcollection document for this question using the Admin SDK (bypassing Security Rules), compares the selectedIndex to the correctIndex, and returns a result object containing isCorrect (Boolean), correctIndex (Integer — to display which answer was right), and explanation (String — optional). In FlutterFlow, create an API call to this Cloud Function. In the quiz screen's Next button On Tap action: first call checkAnswer with the current question's data, then update Page State based on the result (increment score if correct, add to answeredQuestions list), then advance the PageView to the next question. If it is the last question, navigate to the ResultsScreen.

index.js
1// Cloud Function: checkAnswer (index.js)
2const functions = require('firebase-functions');
3const admin = require('firebase-admin');
4admin.initializeApp();
5
6exports.checkAnswer = functions.https.onCall(async (data, context) => {
7 if (!context.auth) {
8 throw new functions.https.HttpsError('unauthenticated', 'Login required');
9 }
10 const { quizId, questionId, selectedIndex } = data;
11 if (!quizId || !questionId || selectedIndex === undefined) {
12 throw new functions.https.HttpsError('invalid-argument', 'Missing required fields');
13 }
14 const db = admin.firestore();
15 const correctDoc = await db
16 .collection('quizzes').doc(quizId)
17 .collection('correctAnswers').doc(questionId)
18 .get();
19 if (!correctDoc.exists) {
20 throw new functions.https.HttpsError('not-found', 'Question not found');
21 }
22 const { correctIndex, explanation } = correctDoc.data();
23 const isCorrect = Number(selectedIndex) === Number(correctIndex);
24 // Log the attempt
25 await db.collection('quiz_attempts').add({
26 userId: context.auth.uid,
27 quizId, questionId,
28 selectedIndex: Number(selectedIndex),
29 isCorrect,
30 timestamp: admin.firestore.FieldValue.serverTimestamp(),
31 });
32 return { isCorrect, correctIndex, explanation: explanation || '' };
33});

Expected result: Tapping Next sends the selected answer to the Cloud Function. The app receives isCorrect and shows a correct/incorrect feedback indicator before advancing to the next question.

4

Build the Results Page with Question Breakdown

Create a ResultsScreen page with parameters: quizId (String), score (Integer), totalQuestions (Integer), answeredQuestions (List of Maps — from Page State). Display the final score prominently at the top with a percentage and a grade indicator (A, B, C, D based on percentage). Below the score, add a ListView that iterates over answeredQuestions. Each item shows the question number, whether it was correct (green checkmark) or incorrect (red X), the selected answer text, and the correct answer text. Use Conditional Visibility to show or hide the correct answer reveal based on whether isCorrect is false. At the bottom, add a Play Again button that resets Page State and navigates back to the quiz start, and a Leaderboard button that opens the leaderboard page.

Expected result: The results page shows the final score, a percentage, a colored grade indicator, and a per-question breakdown showing correct and incorrect answers.

5

Build the Live Leaderboard with Firestore

Create a quiz_scores Firestore collection with fields: userId (String), displayName (String), avatarUrl (String), quizId (String), score (Integer), totalQuestions (Integer), percentage (Double), completedAt (Timestamp). After showing the results screen, write the user's score to quiz_scores — but only if it is their best score for this quiz. In FlutterFlow, create a Cloud Function named submitScore that checks if a better score already exists for this user/quiz pair before writing. Create a Leaderboard page that streams the quiz_scores collection filtered by quizId and ordered by score descending with a limit of 50. Display results in a ListView with rank number, avatar, display name, and score. Highlight the current user's row in a different color using Conditional Styling.

Expected result: The leaderboard shows the top 50 scores for the quiz in real time. The current user's row is highlighted. New scores appear within 1-2 seconds of submission.

Complete working example

index.js
1// Cloud Functions for Quiz App
2// checkAnswer: verifies answer server-side
3// submitScore: saves best score to leaderboard
4
5const functions = require('firebase-functions');
6const admin = require('firebase-admin');
7admin.initializeApp();
8
9// ---- CHECK ANSWER ----
10exports.checkAnswer = functions.https.onCall(async (data, context) => {
11 if (!context.auth) {
12 throw new functions.https.HttpsError('unauthenticated', 'Login required');
13 }
14 const { quizId, questionId, selectedIndex } = data;
15 if (!quizId || !questionId || selectedIndex === undefined || selectedIndex === null) {
16 throw new functions.https.HttpsError('invalid-argument', 'Missing quizId, questionId, or selectedIndex');
17 }
18 const db = admin.firestore();
19 // Rate limit: max 10 answer attempts per user per quiz session (prevent brute force)
20 const recentRef = db.collection('quiz_attempts')
21 .where('userId', '==', context.auth.uid)
22 .where('quizId', '==', quizId)
23 .where('questionId', '==', questionId)
24 .orderBy('timestamp', 'desc')
25 .limit(10);
26 const recent = await recentRef.get();
27 if (recent.size >= 10) {
28 throw new functions.https.HttpsError('resource-exhausted', 'Too many attempts for this question');
29 }
30 // Get correct answer (Admin SDK bypasses Security Rules)
31 const correctSnap = await db
32 .collection('quizzes').doc(quizId)
33 .collection('correctAnswers').doc(questionId)
34 .get();
35 if (!correctSnap.exists) {
36 throw new functions.https.HttpsError('not-found', 'Question answer not found');
37 }
38 const { correctIndex, explanation } = correctSnap.data();
39 const isCorrect = Number(selectedIndex) === Number(correctIndex);
40 // Log attempt
41 await db.collection('quiz_attempts').add({
42 userId: context.auth.uid,
43 quizId, questionId,
44 selectedIndex: Number(selectedIndex),
45 isCorrect,
46 timestamp: admin.firestore.FieldValue.serverTimestamp(),
47 });
48 return { isCorrect, correctIndex: Number(correctIndex), explanation: explanation || '' };
49});
50
51// ---- SUBMIT SCORE ----
52exports.submitScore = functions.https.onCall(async (data, context) => {
53 if (!context.auth) {
54 throw new functions.https.HttpsError('unauthenticated', 'Login required');
55 }
56 const { quizId, score, totalQuestions } = data;
57 if (!quizId || score === undefined || !totalQuestions) {
58 throw new functions.https.HttpsError('invalid-argument', 'Missing score data');
59 }
60 const db = admin.firestore();
61 const userId = context.auth.uid;
62 const percentage = Math.round((score / totalQuestions) * 100);
63 // Check for existing best score
64 const bestScoreRef = db.collection('quizzes').doc(quizId)
65 .collection('userScores').doc(userId);
66 const existing = await bestScoreRef.get();
67 if (existing.exists && existing.data().score >= score) {
68 return { saved: false, message: 'Existing score is better' };
69 }
70 // Get user profile for display name
71 const userSnap = await db.collection('users').doc(userId).get();
72 const userData = userSnap.data() || {};
73 await bestScoreRef.set({
74 userId, quizId, score,
75 totalQuestions,
76 percentage,
77 displayName: userData.displayName || 'Anonymous',
78 avatarUrl: userData.avatarUrl || '',
79 completedAt: admin.firestore.FieldValue.serverTimestamp(),
80 });
81 return { saved: true, percentage };
82});

Common mistakes when creating an Interactive Quiz with Scoring in FlutterFlow

Why it's a problem: Storing the correctIndex in the question document that is fetched by the client

How to avoid: Store correct answers in a separate correctAnswers subcollection with Firestore Security Rules that deny all client reads. Only the Cloud Function (using Admin SDK) can read correct answers.

Why it's a problem: Calculating and trusting the final score sent from the client to save in the leaderboard

How to avoid: Either recalculate the score server-side from the quiz_attempts collection, or increment a server-side score counter in the Cloud Function each time checkAnswer returns isCorrect: true.

Why it's a problem: Loading all questions at once in the client when the quiz has hundreds of questions

How to avoid: For large question banks, implement random question selection in the Cloud Function that returns only 10-20 questions per quiz session. Store the selected question IDs in a Firestore session document so the same questions are served consistently.

Best practices

  • Never expose correct answer data in Firestore documents that are readable by clients.
  • Verify all answers in a Cloud Function and calculate the final score server-side to prevent cheating.
  • Store only the best score per user per quiz to keep the leaderboard clean — use the user's UID as the document ID.
  • Show answer feedback immediately after each question (correct/incorrect with an explanation) to make the quiz educational rather than just competitive.
  • Add a countdown timer per question using a Page State variable and a periodic Timer action for timed quiz modes.
  • Rate-limit the checkAnswer Cloud Function to prevent brute-force answer guessing.
  • Archive completed quiz_attempts to a separate collection after 30 days using a Cloud Scheduler job to keep Firestore read costs down.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a quiz app in FlutterFlow with Firestore. I want to prevent cheating by not sending correct answers to the client. Design a Firestore schema where questions are readable by clients but correct answers are only accessible server-side. Then write a Firebase Cloud Function called checkAnswer that takes quizId, questionId, and selectedIndex as parameters, reads the correct answer using the Admin SDK, and returns isCorrect and correctIndex.

FlutterFlow Prompt

Create a FlutterFlow API call to a Firebase Cloud Function named checkAnswer. The call should pass the current quizId (String), questionId (String), and selectedAnswerIndex (Integer) as parameters. Handle two response cases: isCorrect: true (increment the score Page State variable) and isCorrect: false (store the correctIndex to show the right answer in the UI).

Frequently asked questions

How do I add a countdown timer per question?

Create a Page State variable timeRemaining (Integer) set to the quiz's timeLimit value on each question load. Add a Timer action that fires every 1 second and decrements timeRemaining. When timeRemaining reaches 0, automatically call the Next button's action chain with the current selectedAnswerIndex (or -1 if no answer was selected). Show timeRemaining in a Text widget with a circular progress indicator.

Can I add image or video questions?

Yes. Add imageUrl (String) and videoUrl (String) fields to the questions Firestore documents. Use Conditional Visibility in the question card to show an Image widget when imageUrl is not empty, and a VideoPlayer Custom Widget when videoUrl is not empty. Use Firebase Storage for hosting question media files.

How do I create multiple-choice questions where more than one answer is correct?

Change the question document to store correctIndices (Array of Integers) instead of a single correctIndex. Replace RadioButtons with Checkboxes to allow multi-select. Update the Cloud Function to compare the user's Array of selected indices against the correctIndices array, marking the question correct only when all correct indices are selected and no incorrect ones are included.

How do I prevent users from retaking the quiz immediately after failing?

Add a cooldownMinutes field to the quiz document. In the submitScore Cloud Function, write a lastAttemptAt timestamp to the userScores document. On the quiz start page, check this timestamp against the current time. If the cooldown has not elapsed, show a 'You can retake this quiz in X minutes' message and disable the Start button.

Can the quiz work offline?

Firestore's offline persistence will cache already-loaded question documents. However, the Cloud Function for answer checking requires an internet connection. For offline-capable quizzes, consider a simpler approach: store all correct answers locally in encrypted form and only sync scores when connectivity returns.

How do I randomize question order each time the quiz is taken?

In the Cloud Function that starts a quiz session, fetch all question IDs for the quiz and shuffle them using Fisher-Yates shuffle. Store the shuffled order in a Firestore quiz_sessions document. The client fetches questions by their position in the shuffled order rather than by their Firestore order field.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.