Build a learning management system using Firestore collections for courses, a lessons subcollection per course, and an enrollments collection tracking each user's progress. Display courses in a filtered GridView, play video lessons with FlutterFlowVideoPlayer, and track completion with a LinearPercentIndicator bound to the ratio of completed lessons to total lessons.
Self-paced course platform with progress tracking in FlutterFlow
An LMS lets users browse courses, enroll, watch video lessons, and track their progress. This tutorial builds one end-to-end in FlutterFlow: a Firestore data model stores courses and their lessons as a subcollection, an enrollments collection tracks which users are enrolled and which lessons they have completed, and the UI uses a GridView catalog, a video player page, and a progress bar that updates as lessons are marked done. No custom code is required except for the progress calculation.
Prerequisites
- A FlutterFlow project with Firebase/Firestore connected
- Firebase Authentication enabled with user sign-in working
- Basic familiarity with Backend Queries and ListView widgets
- At least one video URL (YouTube, Vimeo, or Firebase Storage) for testing
Step-by-step guide
Create the Firestore data model for courses, lessons, and enrollments
Create the Firestore data model for courses, lessons, and enrollments
In Firestore, create a top-level courses collection with fields: title (String), description (String), instructorId (String), thumbnailUrl (String), price (Double), category (String), enrollmentCount (Integer, default 0). Under each course document, add a lessons subcollection with fields: title (String), content (String), videoUrl (String), order (Integer), durationMinutes (Integer). Create a separate top-level enrollments collection with fields: userId (String), courseId (String), completedLessonIds (List of Strings, default empty), enrolledAt (Timestamp). Set Firestore rules so any authenticated user can read courses and lessons, but enrollments are restricted to the owning user.
Expected result: Three Firestore structures are ready: courses (top-level), lessons (subcollection under each course), and enrollments (top-level, scoped by userId).
Build the course catalog page with search and category filtering
Build the course catalog page with search and category filtering
Create a CourseCatalogPage. At the top, add a TextField with a search icon prefix for keyword search and a Row of ChoiceChips bound to an Option Set of categories (Design, Development, Marketing, Business). Below, add a GridView with crossAxisCount 2 bound to a Backend Query on the courses collection. Apply filters: if a category chip is selected, filter where category equals the selected value. For search, use a whereGreaterThanOrEqualTo on the title field with the search text. Each grid item is a Component showing the course thumbnail Image, title Text, instructor name Text, price Text, and enrollmentCount with a people icon. Tap navigates to CourseDetailPage passing the course document reference.
Expected result: A responsive grid of course cards appears with working category chip filters and a search field that narrows results by title.
Build the course detail page with lesson list and enroll button
Build the course detail page with lesson list and enroll button
Create CourseDetailPage that receives the course document reference as a parameter. Display the course thumbnail as a hero Image at the top, followed by the title, description, instructor name, and price. Below, add a ListView bound to a Backend Query on the courses/{courseId}/lessons subcollection ordered by the order field ascending. Each lesson row shows an order number, title, durationMinutes, and a checkmark icon. Add an Enroll button at the bottom. The Enroll button's On Tap Action Flow: create a document in the enrollments collection with userId set to the current user's UID, courseId set to this course's ID, completedLessonIds as an empty list, and enrolledAt as the current timestamp. Then use Update Document to increment the course's enrollmentCount by 1 using FieldValue.increment(1). After enrollment, hide the Enroll button and show a Continue Learning button instead using Conditional Visibility based on whether an enrollment doc exists for this user and course.
Expected result: The course detail page shows course info, a scrollable lesson list, and an Enroll button that creates the enrollment record and switches to Continue Learning.
Create the lesson player page with video and Mark Complete action
Create the lesson player page with video and Mark Complete action
Create LessonPlayerPage that receives the lesson document reference and the enrollment document reference as parameters. At the top, place a FlutterFlowVideoPlayer widget with its URL set from the lesson's videoUrl field. Below the player, display the lesson title as a heading and the content field as body text. Add a Mark Complete button at the bottom. The On Tap Action Flow: use Update Document on the enrollment doc to add this lesson's ID to the completedLessonIds array using FieldValue.arrayUnion([lessonId]). After the update, show a SnackBar confirming completion and navigate back to the CourseDetailPage. Use Conditional Visibility to hide the Mark Complete button if the lesson ID is already in the enrollment's completedLessonIds array.
Expected result: Users can watch a video lesson and tap Mark Complete. The lesson ID is appended to completedLessonIds on their enrollment document.
Display course progress with a LinearPercentIndicator
Display course progress with a LinearPercentIndicator
On the CourseDetailPage, add a LinearPercentIndicator widget between the course info section and the lesson list. Create a Custom Function called calculateProgress that accepts two parameters: completedCount (Integer) and totalCount (Integer). It returns completedCount / totalCount as a Double (handling division by zero by returning 0.0). Bind the indicator's percent value to this function, passing completedLessonIds.length from the enrollment doc and the lesson list length from the Backend Query result count. Set the progress bar color to your theme's primary color and the background to grey. Next to the indicator, add a Text widget showing the fraction as a string like '3 of 8 lessons complete'. On each lesson row in the ListView, use Conditional Visibility on a checkmark Icon: show it when the lesson's ID is contained in completedLessonIds.
Expected result: A progress bar shows the percentage of completed lessons. Each completed lesson row displays a green checkmark.
Add instructor dashboard for managing courses and lessons
Add instructor dashboard for managing courses and lessons
Create an InstructorDashboardPage accessible only to users with an instructor role or matching instructorId. Add a ListView bound to courses where instructorId equals the current user's UID. Each item shows the course title, enrollmentCount, and Edit/Delete IconButtons. The Edit button navigates to a CourseEditPage pre-filled with the course fields. Add a FloatingActionButton to create a new course. On the CourseEditPage, include a Manage Lessons section: a ListView of the course's lessons subcollection with Add Lesson and Delete buttons. The Add Lesson form includes fields for title, videoUrl, content, durationMinutes, and order. On save, create a new document in the lessons subcollection. This lets instructors manage their content without leaving FlutterFlow.
Expected result: Instructors can create, edit, and delete courses and their lessons from a dedicated dashboard page.
Complete working example
1Firestore Data Model:2├── courses/{courseId}3│ ├── title: String ("Intro to UX Design")4│ ├── description: String5│ ├── instructorId: String (user UID)6│ ├── thumbnailUrl: String7│ ├── price: Double (29.99)8│ ├── category: String ("Design")9│ ├── enrollmentCount: Integer (47)10│ └── lessons/{lessonId} (subcollection)11│ ├── title: String ("What is UX?")12│ ├── content: String (lesson body text)13│ ├── videoUrl: String14│ ├── order: Integer (1)15│ └── durationMinutes: Integer (12)16└── enrollments/{enrollmentId}17 ├── userId: String18 ├── courseId: String19 ├── completedLessonIds: List<String> ["les_001", "les_002"]20 └── enrolledAt: Timestamp2122CourseCatalogPage:23├── TextField (search by title)24├── Row → ChoiceChips (Design | Development | Marketing | Business)25└── GridView (crossAxisCount: 2, query: courses + filters)26 └── CourseCard Component27 ├── Image (thumbnailUrl)28 ├── Text (title, maxLines: 2)29 ├── Text (instructor name)30 ├── Row → Text (price) + Icon+Text (enrollmentCount)31 └── On Tap → Navigate CourseDetailPage3233CourseDetailPage:34├── Image (hero thumbnail)35├── Text (title, heading)36├── Text (description)37├── LinearPercentIndicator (completedLessons / totalLessons)38├── Text ("3 of 8 lessons complete")39├── ListView (lessons ordered by 'order' ASC)40│ └── Row41│ ├── Text (order number)42│ ├── Column → Text (title) + Text (duration)43│ ├── Icon (checkmark, Cond. Vis: lessonId in completedLessonIds)44│ └── On Tap → Navigate LessonPlayerPage45└── Button (Enroll / Continue Learning, conditional)4647LessonPlayerPage:48├── FlutterFlowVideoPlayer (videoUrl)49├── Text (lesson title, heading)50├── Text (lesson content, body)51└── Button (Mark Complete)52 └── On Tap → Update enrollment: arrayUnion(lessonId) → SnackBar → PopCommon mistakes when building a Learning Management System (LMS) with FlutterFlow
Why it's a problem: Storing progress as a separate document per completed lesson
How to avoid: Store an array of completedLessonIds on the single enrollment document. One read gives you the full progress. Check array length versus total lesson count for the percentage.
Why it's a problem: Not incrementing enrollmentCount atomically on enroll
How to avoid: Use FieldValue.increment(1) in the Update Document action instead of reading the count, adding 1, and writing it back.
Why it's a problem: Using Page State for enrollment status instead of querying Firestore
How to avoid: Run a Backend Query on page load checking enrollments where userId == currentUser AND courseId == thisCourse. Bind the Enroll/Continue button visibility to whether this query returns a result.
Best practices
- Use a subcollection for lessons under each course to keep queries scoped and Firestore rules simple
- Store completedLessonIds as an array on the enrollment doc for single-read progress calculation
- Use FieldValue.increment for enrollmentCount to avoid race conditions on concurrent enrollments
- Order lessons by an explicit order Integer field so instructors can reorder content without renaming
- Set Single Time Query to OFF on enrollment queries so the progress bar updates in real time
- Paginate the course catalog GridView with limit 10 and infinite scroll for large catalogs
- Add Firestore Security Rules ensuring users can only write to their own enrollment documents
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Design a Firestore data model for a learning management system with courses, lessons (as a subcollection), and enrollments that track which lessons each user has completed. Include the Firestore security rules.
Create a course catalog page with a GridView of course cards showing thumbnail, title, price, and enrollment count. Add a search TextField and category ChoiceChips filter. Each card should navigate to a course detail page.
Frequently asked questions
How do I calculate course progress as a percentage in FlutterFlow?
Create a Custom Function that takes completedCount and totalCount as integers. Return completedCount / totalCount as a double, with a guard for zero division. Bind this to a LinearPercentIndicator's percent property, passing completedLessonIds.length and the total lesson count from your query.
Can I add certificate generation when a course is completed?
Yes. When the last lesson is marked complete and completedLessonIds.length equals total lessons, trigger a Cloud Function that generates a PDF certificate using the pdf Dart package, uploads it to Firebase Storage, and saves the URL on the enrollment doc.
How do I restrict lesson access to enrolled users only?
Set Firestore Security Rules on the lessons subcollection to check if a document exists in enrollments where userId matches the requester and courseId matches. Alternatively, check enrollment status on the FlutterFlow page load and navigate away if not enrolled.
Should I store videos in Firebase Storage or use external URLs?
For small catalogs, Firebase Storage works. For larger catalogs, use a video hosting service like Vimeo, Mux, or YouTube (unlisted) and store the embed or playback URL in the videoUrl field. This reduces storage costs and provides adaptive bitrate streaming.
How do I add lesson ordering and let instructors reorder?
Use an Integer order field on each lesson doc. On the instructor dashboard, display lessons sorted by order. To reorder, update the order field on affected lessons. A simple approach is to use order values with gaps (10, 20, 30) so you can insert between them without updating every lesson.
Can RapidDev help build a production-ready LMS?
Yes. A production LMS needs instructor payouts, certificate generation, drip-release scheduling, completion emails, analytics dashboards, and mobile offline access. RapidDev can architect the full system including Cloud Functions, payment integration, and video hosting.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation