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

How to Build a Learning Management System (LMS) with FlutterFlow

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.

What you'll learn

  • How to design Firestore collections for courses, lessons, and enrollments
  • How to build a course catalog with search and category filtering
  • How to track lesson completion and display a progress bar per course
  • How to play video lessons and mark them complete inside FlutterFlow
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read25-35 minFlutterFlow Free+March 2026RapidDev Engineering Team
TL;DR

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

1

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).

2

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.

3

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.

4

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.

5

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.

6

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

LMS Architecture
1Firestore Data Model:
2 courses/{courseId}
3 title: String ("Intro to UX Design")
4 description: String
5 instructorId: String (user UID)
6 thumbnailUrl: String
7 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: String
14 order: Integer (1)
15 durationMinutes: Integer (12)
16 enrollments/{enrollmentId}
17 userId: String
18 courseId: String
19 completedLessonIds: List<String> ["les_001", "les_002"]
20 enrolledAt: Timestamp
21
22CourseCatalogPage:
23 TextField (search by title)
24 Row ChoiceChips (Design | Development | Marketing | Business)
25 GridView (crossAxisCount: 2, query: courses + filters)
26 CourseCard Component
27 Image (thumbnailUrl)
28 Text (title, maxLines: 2)
29 Text (instructor name)
30 Row Text (price) + Icon+Text (enrollmentCount)
31 On Tap Navigate CourseDetailPage
32
33CourseDetailPage:
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 Row
41 Text (order number)
42 Column Text (title) + Text (duration)
43 Icon (checkmark, Cond. Vis: lessonId in completedLessonIds)
44 On Tap Navigate LessonPlayerPage
45 Button (Enroll / Continue Learning, conditional)
46
47LessonPlayerPage:
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 Pop

Common 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.

ChatGPT Prompt

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.

FlutterFlow Prompt

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.

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.