Asia/Jakarta
Projects

Building a Comprehensive E-Learning Platform with Flutter and Firebase

Building a Comprehensive E-Learning Platform with Flutter and Firebase
July 10, 2025
GitHub Repository:
Bash
git clone https://github.com/Afrizal236/RamySmart.git
Building a screen that displays a list of courses is simple. Building a complete learning platform that tracks progress, grades assignments automatically, streams YouTube videos, handles purchases with promo codes, and works offline — that is where mobile engineering meets product thinking. RamySmart bridges this gap — a comprehensive e-learning application built with Flutter that combines course management, interactive MCQ and essay assignments, YouTube video integration, and a full e-commerce system into a single cohesive experience. Data lives both in Firebase for real-time sync and SQLite for offline access. This article covers the complete architecture: from state management patterns and local database schema to YouTube integration, auto-grading, and Firebase authentication.
+----------------+     +----------------+     +----------------+
|   UI Layer     |     |  Data Layer    |     |   Storage      |
|   (Flutter)    |     |  (Provider)    |     |                |
|                |     |                |     |                |
| Course Catalog |---->| CourseManager  |---->| Firebase       |
| Assignments    |     | Singleton      |     | Firestore      |
| Video Player   |     | ChangeNotifier |     |                |
| E-Commerce     |     |                |     | SQLite         |
|                |     | MathJS API     |     | Local DB       |
+----------------+     +----------------+     +----------------+

Yaml
dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0                    # HTTP requests
  youtube_player_flutter: ^8.1.2  # YouTube integration
  image_picker: ^1.0.4            # Image selection
  path_provider: ^2.1.1           # File system access
  path: ^1.8.3                    # Path manipulation
  sqflite: ^2.3.0                 # Local database
  provider: ^6.0.5                # State management
  intl: ^0.18.1                   # Internationalization
  firebase_core: ^2.15.1
  firebase_auth: ^4.7.3
  cloud_firestore: ^4.8.5
lib/
├── main.dart
├── screens/
│   └── Home/
│       ├── course_home.dart
│       └── widget/
│           ├── account_profile.dart
│           ├── assignment_model.dart
│           ├── cart.dart
│           ├── course_detail_page.dart
│           ├── course_list.dart
│           ├── course_manager.dart
│           ├── essay_assignment_screen.dart
│           ├── featured_courses.dart
│           ├── main_screen.dart
│           ├── my_courses.dart
│           ├── notification_page.dart
│           └── wishlist.dart
└── util/
    └── constants.dart
Bash
# Install Firebase CLI
npm install -g firebase-tools

# Login and configure
firebase login
flutterfire configure

RamySmart uses two complementary patterns — Provider for reactive UI updates and Singleton for shared course state across the app.
Dart
// Course Manager Provider — reactive state
class CourseManagerProvider extends ChangeNotifier {
  final CourseManager _courseManager = CourseManager();

  List<Course> get purchasedCourses => _courseManager.purchasedCourses;
  List<Course> get cartItems        => _courseManager.cartItems;
  List<Course> get wishlistItems    => _courseManager.wishlistItems;

  void purchaseCourse(Course course) {
    _courseManager.purchaseCourse(course);
    notifyListeners();
  }

  void addToCart(Course course) {
    _courseManager.addToCart(course);
    notifyListeners();
  }

  void toggleWishlist(Course course) {
    _courseManager.toggleWishlist(course);
    notifyListeners();
  }
}
Dart
// Course Manager Singleton — single source of truth
class CourseManager {
  static final CourseManager _instance = CourseManager._internal();
  factory CourseManager() => _instance;
  CourseManager._internal();

  final List<Course> _purchasedCourses = [];
  final List<Course> _cartItems        = [];
  final List<Course> _wishlistItems    = [];

  List<Course> get purchasedCourses => List.unmodifiable(_purchasedCourses);
}

SQLite powers the offline experience — storing user profiles and course progress locally so the app works without internet.
Sql
-- User Profile table
CREATE TABLE user_profiles (
  id          INTEGER PRIMARY KEY,
  name        TEXT NOT NULL,
  email       TEXT UNIQUE NOT NULL,
  phone       TEXT,
  image_path  TEXT
);

-- Course Progress table
CREATE TABLE course_progress (
  id                  INTEGER PRIMARY KEY,
  course_id           TEXT NOT NULL,
  lesson_id           TEXT NOT NULL,
  completed           BOOLEAN DEFAULT FALSE,
  progress_percentage REAL DEFAULT 0.0
);
Dart
// Save course progress
Future<void> saveProgress(String courseId, String lessonId, double percentage) async {
  final db = await database;
  await db.insert(
    'course_progress',
    {
      'course_id':           courseId,
      'lesson_id':           lessonId,
      'completed':           percentage >= 100 ? 1 : 0,
      'progress_percentage': percentage,
    },
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

// Get course progress
Future<double> getProgress(String courseId, String lessonId) async {
  final db = await database;
  final result = await db.query(
    'course_progress',
    where: 'course_id = ? AND lesson_id = ?',
    whereArgs: [courseId, lessonId],
  );
  if (result.isEmpty) return 0.0;
  return result.first['progress_percentage'] as double;
}

Each course lesson streams directly from YouTube using the embedded player — no separate video hosting required.
Dart
// Initialize YouTube controller per lesson
YoutubePlayerController _initController(String videoId) {
  return YoutubePlayerController(
    initialVideoId: videoId,
    flags: const YoutubePlayerFlags(
      autoPlay: false,
      mute:     false,
    ),
  );
}

// Player widget with progress tracking
YoutubePlayer(
  controller: _controller,
  showVideoProgressIndicator: true,
  onEnded: (metaData) {
    // Mark lesson as complete when video ends
    saveProgress(courseId, lessonId, 100.0);
  },
)

RamySmart supports two assignment types — MCQ for auto-graded multiple choice and Essay for open-ended responses with word count validation.
Dart
class MCQAssignment {
  final List<Question> questions;
  final Duration timeLimit;

  int calculateScore(Map<int, String> answers) {
    int correct = 0;
    for (int i = 0; i < questions.length; i++) {
      if (answers[i] == questions[i].correctAnswer) {
        correct++;
      }
    }
    return ((correct / questions.length) * 100).round();
  }
}

// Countdown timer for assignments
Timer _startTimer(Duration duration, VoidCallback onExpired) {
  return Timer.periodic(const Duration(seconds: 1), (timer) {
    if (timer.tick >= duration.inSeconds) {
      timer.cancel();
      onExpired();
    }
  });
}
Dart
class EssayAssignmentScreen extends StatefulWidget {
  final int minWords;
  final int maxWords;

  int _countWords(String text) {
    return text.trim().isEmpty
        ? 0
        : text.trim().split(RegExp(r'\s+')).length;
  }

  bool _isValidSubmission(String text) {
    final count = _countWords(text);
    return count >= minWords && count <= maxWords;
  }
}

Dart
class DiscountSystem {
  static const Map<String, double> promoCodes = {
    'BELAJAR10': 0.10,  // 10% discount
    'SMART20':   0.20,  // 20% discount
    'GRATIS50':  0.50,  // 50% discount
  };

  static double applyPromo(double price, String code) {
    final discount = promoCodes[code.toUpperCase()] ?? 0.0;
    return price * (1 - discount);
  }
}
Dart
// Add to cart with duplicate check
void addToCart(Course course) {
  final alreadyInCart = _cartItems.any((c) => c.id == course.id);
  if (!alreadyInCart) {
    _cartItems.add(course);
  }
}

// Calculate total with discount
double calculateTotal({String? promoCode}) {
  final subtotal = _cartItems.fold(0.0, (sum, c) => sum + c.price);
  if (promoCode == null) return subtotal;
  return DiscountSystem.applyPromo(subtotal, promoCode);
}

Dart
// Load courses in batches as user scrolls
final ScrollController _scrollController = ScrollController();

void _initScrollListener() {
  _scrollController.addListener(() {
    if (_scrollController.position.pixels >=
        _scrollController.position.maxScrollExtent - 200) {
      _loadMoreCourses();
    }
  });
}
Dart
// Use cached network image to avoid re-downloading
CachedNetworkImage(
  imageUrl: course.thumbnailUrl,
  placeholder: (context, url) => const ShimmerPlaceholder(),
  errorWidget: (context, url, error) => const Icon(Icons.error),
  memCacheWidth: 300,
)
| Optimization | Before | After | |---|---|---| | Course list load | 800ms | 120ms | | Image load (no cache) | 600ms per image | 10ms (cached) | | Progress query | Full table scan | Indexed query | | Widget rebuilds | Full tree | Scoped Provider |
The journey from a simple course list to a complete e-learning platform requires deliberate architecture decisions at every layer — from choosing between Firebase and SQLite for the right data, to managing assignment timers, YouTube player lifecycle, and cart state across screens. The key lesson from RamySmart: offline-first and real-time are not opposites. SQLite handles the fast, local reads while Firebase handles the sync. Design both layers from day one, and the user will never notice which one is doing the work.