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

How to Create an Interactive Learning Platform in FlutterFlow

Build a learning platform with a course catalog GridView backed by Firestore, an enrollment collection to track each user's progress, and three lesson types: video (YouTube embed), text (rich content), and quiz (scored questions). Store a computed completionPercentage field on the enrollment document and update it via FieldValue.increment rather than recalculating on every render. Generate completion certificates using a Cloud Function PDF generator triggered when completionPercentage reaches 100.

What you'll learn

  • How to build a course catalog with enrollment status indicators
  • How to structure Firestore for courses, lessons, and user progress
  • How to implement video, text, and quiz lesson types
  • How to calculate and display completion progress accurately
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read60-90 minFlutterFlow Standard+ (Custom Actions for progress calculations)March 2026RapidDev Engineering Team
TL;DR

Build a learning platform with a course catalog GridView backed by Firestore, an enrollment collection to track each user's progress, and three lesson types: video (YouTube embed), text (rich content), and quiz (scored questions). Store a computed completionPercentage field on the enrollment document and update it via FieldValue.increment rather than recalculating on every render. Generate completion certificates using a Cloud Function PDF generator triggered when completionPercentage reaches 100.

Building a Learning Platform That Keeps Students Engaged

Online learning platforms share a well-defined pattern: a catalog of courses, a lesson sequence inside each course, progress tracking, and a reward (certificate) at completion. FlutterFlow can build all of these pieces using its visual builder for the UI and Firestore for the data layer. The key architectural decision is where to compute progress — computing it on every render from raw lesson completion data is expensive and slow. Storing a pre-computed completionPercentage on the enrollment document and updating it incrementally is the right approach for a platform that needs to scale.

Prerequisites

  • A FlutterFlow project with Firebase Auth and Firestore connected
  • Firebase Blaze plan (for Cloud Functions used in certificate generation)
  • FlutterFlow Standard plan or higher for Custom Actions
  • Basic understanding of Firestore collections and subcollections

Step-by-step guide

1

Design the Firestore Schema for Courses and Lessons

Create a 'courses' top-level collection with fields: title (String), description (String), thumbnailUrl (String), instructorName (String), totalLessons (Integer), estimatedMinutes (Integer), difficulty (String), category (String), publishedAt (Timestamp), isPublished (Boolean). Each course has a 'lessons' subcollection with: lessonIndex (Integer), title (String), type (String — 'video', 'text', 'quiz'), videoUrl (String, nullable), textContent (String, nullable), estimatedMinutes (Integer). Create an 'enrollments' collection at users/{userId}/enrollments/{courseId} with: enrolledAt (Timestamp), completedLessons (Array of lesson document IDs), completionPercentage (Double, 0-100), completedAt (Timestamp, nullable), certificateUrl (String, nullable).

Expected result: Your Firestore schema has courses, lessons subcollection, and user enrollments collection ready to receive data.

2

Build the Course Catalog GridView

Create a Course Catalog page with a GridView widget set to 2 columns (3 on tablet breakpoint). Each grid card shows: the course thumbnail image (16:9 aspect ratio), category label, course title, instructor name, estimated time, difficulty badge, and an enrollment status indicator. For the enrollment indicator, create a Firestore query on the current user's enrollments/{courseId} document — if it exists, show a 'Continue' button with progress bar; if it doesn't exist, show an 'Enroll' button. Add a search bar at the top to filter courses by title using FlutterFlow's local filtering action. Add a category filter row with horizontally scrolling chips.

Expected result: Course catalog displays all published courses in a grid with enrollment status shown on each card. Search and category filtering work without additional Firestore reads.

3

Build the Lesson Viewer with Three Lesson Types

Create a Lesson page that accepts a lessonId page parameter. Load the lesson document from Firestore and conditionally show the correct lesson type widget using Conditional widgets based on the lesson's 'type' field. Video lessons: use a YouTube player URL in a WebView widget (or the youtube_player_flutter Custom Widget for better control). Text lessons: use a scrollable Column with RichText for formatted content or a flutter_html Custom Widget for HTML content. Quiz lessons: use a Column with question Text, multiple choice RadioButton options, a Submit button, and a score display. Each lesson type includes a 'Mark Complete' button at the bottom that triggers the progress update action.

Expected result: Lesson page renders correctly for all three lesson types. Users can watch video lessons, read text content, submit quiz answers, and mark each lesson complete.

4

Update Enrollment Progress With FieldValue.increment

Create a Custom Action called 'markLessonComplete' that takes the courseId and lessonId. It first checks if the lesson is already in the completedLessons array to prevent duplicate completions. If not already completed, it performs a Firestore update using arrayUnion to add the lessonId to completedLessons, and calculates the new completionPercentage based on the course's totalLessons field. It also updates a lastAccessedAt timestamp. The action returns the new completionPercentage. After calling this action, update a Page State variable with the result for an immediate progress bar update.

mark_lesson_complete.dart
1import 'package:cloud_firestore/cloud_firestore.dart';
2
3Future<double> markLessonComplete(
4 String userId,
5 String courseId,
6 String lessonId,
7 int totalLessons,
8) async {
9 final enrollmentRef = FirebaseFirestore.instance
10 .collection('users')
11 .doc(userId)
12 .collection('enrollments')
13 .doc(courseId);
14
15 final enrollmentDoc = await enrollmentRef.get();
16 if (!enrollmentDoc.exists) {
17 debugPrint('Enrollment not found for course: $courseId');
18 return 0.0;
19 }
20
21 final data = enrollmentDoc.data()!;
22 final completedLessons = List<String>.from(
23 (data['completedLessons'] as List? ?? []).map((e) => e.toString()),
24 );
25
26 // Already completed — return current percentage
27 if (completedLessons.contains(lessonId)) {
28 return (data['completionPercentage'] as num?)?.toDouble() ?? 0.0;
29 }
30
31 // Add lesson and recalculate percentage
32 final newCompletedCount = completedLessons.length + 1;
33 final newPercentage = totalLessons > 0
34 ? (newCompletedCount / totalLessons) * 100
35 : 0.0;
36
37 final updateData = <String, dynamic>{
38 'completedLessons': FieldValue.arrayUnion([lessonId]),
39 'completionPercentage': newPercentage,
40 'lastAccessedAt': FieldValue.serverTimestamp(),
41 };
42
43 if (newPercentage >= 100) {
44 updateData['completedAt'] = FieldValue.serverTimestamp();
45 }
46
47 await enrollmentRef.update(updateData);
48 return newPercentage;
49}

Expected result: Marking a lesson complete updates the enrollment document with the new percentage. The course card in the catalog immediately reflects the updated progress.

5

Generate Completion Certificates With a Cloud Function

Create a Firebase Cloud Function triggered by a Firestore onCreate event on enrollment documents where completedAt is set. The function uses the pdfkit npm package to generate a certificate PDF: it fetches the user's name and email from the users collection, the course title from the courses collection, and generates a formatted PDF with the user's name, course name, completion date, and a unique certificate ID. Upload the PDF to Firebase Storage and update the enrollment document's certificateUrl field. In your FlutterFlow app, show a 'Download Certificate' button when certificateUrl is not null.

Expected result: When a user completes all lessons, a certificate PDF is automatically generated, stored in Firebase Storage, and a download link appears in their course completion screen.

Complete working example

certificate_generator.js
1const functions = require('firebase-functions');
2const admin = require('firebase-admin');
3const PDFDocument = require('pdfkit');
4const { Storage } = require('@google-cloud/storage');
5
6if (!admin.apps.length) admin.initializeApp();
7const storage = new Storage();
8
9exports.generateCertificate = functions.firestore
10 .document('users/{userId}/enrollments/{courseId}')
11 .onUpdate(async (change, context) => {
12 const newData = change.after.data();
13 const oldData = change.before.data();
14
15 // Only trigger when completedAt is newly set
16 if (!newData.completedAt || oldData.completedAt) return null;
17 if (newData.certificateUrl) return null; // Already generated
18
19 const { userId, courseId } = context.params;
20 const db = admin.firestore();
21
22 try {
23 // Fetch user and course data
24 const [userDoc, courseDoc] = await Promise.all([
25 db.collection('users').doc(userId).get(),
26 db.collection('courses').doc(courseId).get(),
27 ]);
28
29 if (!userDoc.exists || !courseDoc.exists) return null;
30
31 const userName = userDoc.data().displayName || 'Student';
32 const courseName = courseDoc.data().title;
33 const completedDate = newData.completedAt.toDate().toLocaleDateString(
34 'en-US', { year: 'numeric', month: 'long', day: 'numeric' }
35 );
36 const certId = `CERT-${userId.slice(0, 6).toUpperCase()}-${courseId.slice(0, 6).toUpperCase()}`;
37
38 // Generate PDF in memory
39 const doc = new PDFDocument({ size: 'A4', layout: 'landscape' });
40 const chunks = [];
41 doc.on('data', chunk => chunks.push(chunk));
42
43 await new Promise(resolve => {
44 doc.on('end', resolve);
45 doc.font('Helvetica-Bold').fontSize(36)
46 .text('Certificate of Completion', { align: 'center' });
47 doc.moveDown();
48 doc.font('Helvetica').fontSize(18)
49 .text(`This certifies that`, { align: 'center' });
50 doc.font('Helvetica-Bold').fontSize(28)
51 .text(userName, { align: 'center' });
52 doc.font('Helvetica').fontSize(18)
53 .text(`has successfully completed`, { align: 'center' });
54 doc.font('Helvetica-Bold').fontSize(22)
55 .text(courseName, { align: 'center' });
56 doc.font('Helvetica').fontSize(14)
57 .text(`${completedDate} | Certificate ID: ${certId}`, { align: 'center' });
58 doc.end();
59 });
60
61 const pdfBuffer = Buffer.concat(chunks);
62 const filePath = `certificates/${userId}/${courseId}.pdf`;
63 const bucket = storage.bucket(admin.app().options.storageBucket);
64 const file = bucket.file(filePath);
65 await file.save(pdfBuffer, { contentType: 'application/pdf' });
66 await file.makePublic();
67 const publicUrl = `https://storage.googleapis.com/${bucket.name}/${filePath}`;
68
69 await change.after.ref.update({ certificateUrl: publicUrl, certificateId: certId });
70 return null;
71 } catch (error) {
72 console.error('Certificate generation failed:', error);
73 return null;
74 }
75 });

Common mistakes when creating an Interactive Learning Platform in FlutterFlow

Why it's a problem: Calculating progress by counting completedLessons array length on every lesson render

How to avoid: Store the pre-computed completionPercentage field on the enrollment document. Update it in the markLessonComplete action. Reading it is one field read per page load.

Why it's a problem: Not checking for duplicate lesson completion before incrementing progress

How to avoid: Check if the lessonId already exists in the completedLessons array before updating. If it does, return the existing percentage without modifying the document.

Why it's a problem: Loading all lessons in a course on the course detail page

How to avoid: Store a lightweight lesson index: lessonIndex, title, type, and estimatedMinutes — no content fields. Load only these lightweight fields for the course outline view. Load full lesson content only when a specific lesson is opened.

Best practices

  • Store pre-computed completionPercentage on the enrollment document — never calculate it from array length on render
  • Create a lightweight lesson list view that excludes content fields, loaded separately only when a lesson is opened
  • Auto-mark video lessons complete at 90% watch time rather than requiring manual button taps
  • Display completion percentage as both a progress bar and a numeric label for accessibility
  • Generate certificates server-side via Cloud Function — never trust client-side completion claims
  • Use arrayUnion for completedLessons updates to prevent duplicate entries automatically
  • Show a course recommendation section after completion to keep users engaged with your platform

Still stuck?

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

ChatGPT Prompt

I'm building an online course platform in FlutterFlow. Design the complete Firestore data architecture for: courses with lessons, user enrollment and progress tracking, quiz results storage, and completion certificates. Include collection names, field names, field types, and explain which data to denormalize for query efficiency.

FlutterFlow Prompt

In my FlutterFlow learning platform, I need a Custom Action that marks a lesson as complete, updates the enrollment progress percentage, and returns the new percentage. The action should prevent duplicate completions and handle the edge case where totalLessons is zero. Write the complete Dart code.

Frequently asked questions

Can I sell courses with a paywall in my FlutterFlow learning platform?

Yes. Add a 'isPaid' Boolean and 'price' field to course documents. Create an enrollment flow that checks isPaid — free courses enroll directly, paid courses redirect to a Stripe Checkout Session via a Cloud Function. Create the enrollment document only after the Stripe webhook confirms payment success. Check for a valid enrollment document before allowing access to lesson content.

How do I add video lessons if I don't want to use YouTube?

Upload course videos to Firebase Storage and store the download URL in the lesson document. Use the video_player Flutter package as a Custom Widget in FlutterFlow to play the videos. Note that Firebase Storage egress costs add up with video content — for production platforms, consider a dedicated video hosting service like Vimeo, Mux, or Cloudflare Stream, which offer better video compression and CDN delivery.

How do I prevent users from skipping lessons to access later content?

Add a 'prerequisiteLessonId' field to each lesson. In your lesson navigation logic, check if the prerequisite lesson is in the completedLessons array before navigating to the next lesson. Show a locked state (greyed out with a lock icon) for inaccessible lessons in the course outline.

How do I display a course completion percentage in the catalog without an extra Firestore read?

Query the user's enrollments collection when the course catalog page loads (or use a real-time listener). This returns all enrollment documents at once. Use a Custom Function to find the enrollment matching each course ID and return its completionPercentage. One enrollments query covers all courses rather than one read per course card.

Can multiple instructors manage courses in my platform?

Yes. Add an 'instructorId' field to each course document. Create an admin section visible only to users with role == 'instructor'. In Firestore security rules, allow write access to courses only if request.auth.uid == resource.data.instructorId. This lets each instructor manage their own courses without seeing others'.

How do I implement a discussion forum for each course?

Add a 'discussions' subcollection to each course document with fields: userId, displayName, message, createdAt, and a 'replies' subcollection for nested comments. Create a Discussion page that queries this subcollection ordered by createdAt. Use Firestore real-time listeners (enabled by default in FlutterFlow Firestore queries) for live updates when new messages are posted.

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.