DEV Community

Cover image for How to Build a Real-time AI Multiplayer Quiz App with Flutter & Firebase
AwsmAsim
AwsmAsim

Posted on

How to Build a Real-time AI Multiplayer Quiz App with Flutter & Firebase

Introduction

What We'll Build

In this tutorial, we'll walk through building a real-time multiplayer quiz application that generates questions using AI. Players can create custom quizzes and compete with up to 8 friends simultaneously.
This is based on the implementation of a real world mobile application that you an find at: https://quizzlerai-play.netlify.app/

Tech Stack Overview

Technology Purpose Key Benefits
Flutter Cross-platform UI Single codebase for iOS/Android
Firebase Firestore Real-time database Built-in multiplayer sync
OpenAI API Question generation Dynamic content creation
GetX State management Reactive programming

Prerequisites

  • Flutter SDK installed
  • Firebase project setup
  • OpenAI API key
  • Basic knowledge of Dart and Flutter

Architecture Overview

Note: This article uses Mermaid diagrams for visual representation. If the diagrams don't render on your platform, the concepts are also explained in text and code examples throughout the tutorial.

System Architecture Diagram

Quiz App System Architecture Mermaid Diagram

Data Flow Architecture

Flutter Quiz App Data Flow

Project Structure

lib/
├── controllers/     # Business logic & state
├── models/         # Data structures
├── services/       # API integrations
├── views/          # UI components
└── utils/          # Helper functions
Enter fullscreen mode Exit fullscreen mode

MVC Pattern Implementation

We'll use the Model-View-Controller pattern with GetX for clean separation:

// Example controller structure
class QuizController extends GetxController {
  // Observable variables for reactive UI
  final currentQuestion = Rxn<Question>();
  final timeLeft = 60.obs;
  final selectedOption = RxnString();

  // Methods for quiz management
  void startQuiz() { /* implementation */ }
  void submitAnswer(String answer) { /* implementation */ }
}
Enter fullscreen mode Exit fullscreen mode

Step 1: Setting Up Firebase Integration

Firebase Configuration

First, configure Firebase in your Flutter project:

// firebase_service.dart
class FirebaseService {
  static Future<void> initializeFirebase() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    // Enable offline persistence
    await FirebaseFirestore.instance.enablePersistence();
  }
}
Enter fullscreen mode Exit fullscreen mode

Authentication Setup

Implement multi-provider authentication:

// auth_service.dart
class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  // Current user getter
  User? get currentUser => _auth.currentUser;

  // Sign in methods
  Future<UserCredential> signInWithEmail(String email, String password) async {
    return await _auth.signInWithEmailAndPassword(
      email: email, 
      password: password
    );
  }

  // Additional auth methods...
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Real-time Database Design

Firestore Database Schema

AI Quiz App Database

Real-time Synchronization Flow

AI Quiz App Real-time Synchronization

Firestore Data Structure

Design your database schema for multiplayer functionality:

// Contest document structure
{
  "contest_id": "unique_id",
  "active_users": ["user1", "user2"],
  "current_question": {
    "id": "q1",
    "start_time": timestamp,
    "status": "active"
  },
  "leaderboard": [
    {"user_id": "user1", "points": 100},
    {"user_id": "user2", "points": 80}
  ]
}
Enter fullscreen mode Exit fullscreen mode

Real-time Listeners

Set up Firestore listeners for live updates:

class ContestController extends GetxController {
  StreamSubscription? _contestSubscription;

  void setupContestListener(String contestId) {
    _contestSubscription = FirebaseFirestore.instance
        .collection('contests')
        .doc(contestId)
        .snapshots()
        .listen((snapshot) {
      if (snapshot.exists) {
        _handleContestUpdate(snapshot.data()!);
      }
    });
  }

  void _handleContestUpdate(Map<String, dynamic> data) {
    // Watch for current question changes
    final currentQuestionData = data['current_question'];
    final String questionId = currentQuestionData['id'];

    // Update UI when question changes
    if (currentQuestion.value?.id != questionId) {
      _loadNewQuestion(questionId, data['questions']);
      _resetQuestionState();
    }

    // Update other game state...
    _updateLeaderboard(data['leaderboard']);
    _updateParticipants(data['active_users']);
  }

  void _resetQuestionState() {
    selectedOption.value = null;
    timeLeft.value = /* question time limit */;
    hasAnswered.value = false;
  }
}
Enter fullscreen mode Exit fullscreen mode

AI Question Generation Flow

AI Quiz Generation Flow

OpenAI Prompt Engineering Strategy

Prompt Strategy for AI Quiz App

Step 3: OpenAI Integration

API Service Setup

Create a service for OpenAI API calls:

class QuestionGenerationService {
  final String apiKey = /* your API key */;
  final String apiUrl = "https://api.openai.com/v1/chat/completions";

  Future<String> generateQuestions({
    required String topic,
    required String difficulty,
    required int questionCount,
  }) async {

    final payload = {
      "model": "gpt-3.5-turbo",
      "messages": [
        {
          "role": "system",
          "content": "You are an expert quiz creator..."
        },
        {
          "role": "user",
          "content": _buildPrompt(topic, difficulty, questionCount)
        }
      ]
    };

    final response = await http.post(
      Uri.parse(apiUrl),
      headers: {
        "Authorization": "Bearer $apiKey",
        "Content-Type": "application/json"
      },
      body: jsonEncode(payload)
    );

    if (response.statusCode == 200) {
      final jsonResponse = jsonDecode(response.body);
      return jsonResponse['choices'][0]['message']['content'];
    } else {
      throw Exception('Failed to generate questions');
    }
  }

  String _buildPrompt(String topic, String difficulty, int count) {
    return """
Create $count multiple choice questions about "$topic" 
with $difficulty difficulty level.

Return as JSON format:
{
  "questions": [
    {
      "id": "q1",
      "question": "Question text here",
      "options": [
        {"id": "A", "text": "Option 1"},
        {"id": "B", "text": "Option 2"},
        {"id": "C", "text": "Option 3"},
        {"id": "D", "text": "Option 4"}
      ],
      "correct_answer": "A",
      "explanation": "Why this answer is correct"
    }
  ]
}
    """;
  }
}
Enter fullscreen mode Exit fullscreen mode

Multiplayer Game Flow

AI Quiz App Multiplayer Game Flow

Step 4: Multiplayer Synchronization

Contest Creation

Implement contest creation with proper state management:

class QuizController extends GetxController {

  Future<void> createContest() async {
    try {
      final contestId = _generateUniqueId();
      final userId = FirebaseAuth.instance.currentUser!.uid;

      await FirebaseFirestore.instance
          .collection('contests')
          .doc(contestId)
          .set({
        'contest_id': contestId,
        'creator_id': userId,
        'questions': questions.map((q) => q.toJson()).toList(),
        'active_users': [userId],
        'current_question': {
          'id': questions.first.id,
          'start_time': null,
          'status': 'waiting'
        },
        'user_answers': {
          userId: {}
        },
        'created_at': FieldValue.serverTimestamp(),
      });

      currentContestId.value = contestId;

    } catch (e) {
      // Handle error
      print('Error creating contest: $e');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Joining Contests

Handle user joining with race condition prevention:

Future<void> joinContest(String contestId) async {
  final userId = FirebaseAuth.instance.currentUser!.uid;

  // Use transaction to prevent race conditions
  await FirebaseFirestore.instance.runTransaction((transaction) async {
    final contestRef = FirebaseFirestore.instance
        .collection('contests')
        .doc(contestId);

    final contestDoc = await transaction.get(contestRef);

    if (!contestDoc.exists) {
      throw Exception('Contest not found');
    }

    final data = contestDoc.data()!;
    final List<dynamic> activeUsers = data['active_users'] ?? [];

    // Check participant limit
    if (activeUsers.length >= 8) {
      throw Exception('Contest is full');
    }

    // Add user to contest
    transaction.update(contestRef, {
      'active_users': FieldValue.arrayUnion([userId]),
      'user_answers.$userId': {}
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Timer Synchronization Architecture

AI Quiz App Timer Synchronization

Question State Management

AI Quiz Quesion State Management

Step 5: Question Timer & Synchronization

Server-side Timing

Use Firestore server timestamps for perfect sync:

class TimerController extends GetxController {
  Timer? _questionTimer;
  final timeLeft = 60.obs;

  void startQuestionTimer(int duration) {
    timeLeft.value = duration;

    _questionTimer = Timer.periodic(Duration(seconds: 1), (timer) {
      if (timeLeft.value > 0) {
        timeLeft.value--;
      } else {
        timer.cancel();
        _handleTimeUp();
      }
    });
  }

  void _handleTimeUp() {
    // Auto-submit or move to next question
    if (!hasAnswered.value) {
      submitAnswer(null); // No answer submitted
    }
  }

  @override
  void onClose() {
    _questionTimer?.cancel();
    super.onClose();
  }
}
Enter fullscreen mode Exit fullscreen mode

Question Progression

Coordinate question changes across all players:

Future<void> moveToNextQuestion() async {
  final nextQuestionIndex = currentQuestionIndex.value + 1;

  if (nextQuestionIndex < questions.length) {
    final nextQuestion = questions[nextQuestionIndex];

    await FirebaseFirestore.instance
        .collection('contests')
        .doc(currentContestId.value)
        .update({
      'current_question': {
        'id': nextQuestion.id,
        'start_time': FieldValue.serverTimestamp(),
        'status': 'active'
      }
    });

    currentQuestionIndex.value = nextQuestionIndex;
  } else {
    _endQuiz();
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Real-time Leaderboard

Score Calculation

Update scores in real-time:

Future<void> submitAnswer(String? selectedAnswer) async {
  if (hasAnswered.value) return;

  hasAnswered.value = true;
  final userId = FirebaseAuth.instance.currentUser!.uid;
  final questionId = currentQuestion.value!.id;
  final isCorrect = selectedAnswer == currentQuestion.value!.correctAnswer;

  // Calculate points (example: 100 points for correct, time bonus)
  int points = 0;
  if (isCorrect) {
    points = 100 + timeLeft.value; // Time bonus
  }

  await FirebaseFirestore.instance
      .collection('contests')
      .doc(currentContestId.value)
      .update({
    'user_answers.$userId.$questionId': selectedAnswer,
    'leaderboard': FieldValue.arrayUnion([{
      'user_id': userId,
      'question_id': questionId,
      'points': points,
      'timestamp': FieldValue.serverTimestamp()
    }])
  });
}
Enter fullscreen mode Exit fullscreen mode

Leaderboard Updates

Display real-time rankings:

void updateLeaderboard(List<dynamic> leaderboardData) {
  // Group by user and calculate total points
  Map<String, int> userPoints = {};

  for (var entry in leaderboardData) {
    String userId = entry['user_id'];
    int points = entry['points'] ?? 0;
    userPoints[userId] = (userPoints[userId] ?? 0) + points;
  }

  // Sort and create ranked list
  var sortedUsers = userPoints.entries.toList()
    ..sort((a, b) => b.value.compareTo(a.value));

  List<Map<String, dynamic>> rankedLeaderboard = [];
  for (int i = 0; i < sortedUsers.length; i++) {
    rankedLeaderboard.add({
      'user_id': sortedUsers[i].key,
      'total_points': sortedUsers[i].value,
      'rank': i + 1
    });
  }

  leaderboard.assignAll(rankedLeaderboard);
}
Enter fullscreen mode Exit fullscreen mode

Error Handling Architecture

Error Handling Architecture

Common Challenges & Solutions

Challenge 1: Rate Limiting

Problem: OpenAI API rate limits during peak usage.

Solution: Implement retry logic with exponential backoff:

Future<String> generateWithRetry({required int maxRetries}) async {
  for (int attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await generateQuestions(/* parameters */);
    } catch (e) {
      if (e.toString().contains('429') && attempt < maxRetries) {
        await Future.delayed(Duration(seconds: pow(2, attempt).toInt()));
        continue;
      }
      rethrow;
    }
  }
  throw Exception('Max retries exceeded');
}
Enter fullscreen mode Exit fullscreen mode

Challenge 2: Network Disconnections

Problem: Players losing connection during games.

Solution: Implement reconnection handling:

void handleConnectionState() {
  _connectivitySubscription = Connectivity()
      .onConnectivityChanged
      .listen((ConnectivityResult result) {
    if (result != ConnectivityResult.none) {
      _reconnectToContest();
    }
  });
}

void _reconnectToContest() {
  if (currentContestId.value.isNotEmpty) {
    setupContestListener(currentContestId.value);
  }
}
Enter fullscreen mode Exit fullscreen mode

Challenge 3: Memory Management

Problem: Memory leaks from real-time listeners.

Solution: Proper disposal in controllers:

@override
void onClose() {
  _contestSubscription?.cancel();
  _questionTimer?.cancel();
  _connectivitySubscription?.cancel();
  super.onClose();
}
Enter fullscreen mode Exit fullscreen mode

Performance Optimization Tips

Firestore Optimization

  1. Use compound queries to reduce read operations
  2. Implement pagination for large datasets
  3. Cache frequently accessed data locally
  4. Use transactions for atomic operations

Memory Management

  1. Dispose streams properly in controller onClose()
  2. Use weak references for large objects
  3. Implement object pooling for frequent allocations
  4. Monitor memory usage during development

UI Performance

  1. Use const constructors for static widgets
  2. Implement ListView.builder for large lists
  3. Avoid rebuilding entire widget trees
  4. Use RepaintBoundary for complex animations

Deployment Considerations

Environment Configuration

// Use environment variables for sensitive data
class Config {
  static String get openAIKey => dotenv.env['OPENAI_API_KEY'] ?? '';
  static String get environment => dotenv.env['ENVIRONMENT'] ?? 'dev';
}
Enter fullscreen mode Exit fullscreen mode

Error Handling

// Centralized error handling
class ErrorHandler {
  static void handleError(dynamic error) {
    if (error.toString().contains('permission-denied')) {
      // Handle authentication errors
    } else if (error.toString().contains('unavailable')) {
      // Handle network errors
    }
    // Log to crash reporting service
    FirebaseCrashlytics.instance.recordError(error, null);
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

Architecture Benefits

  • Separation of concerns makes debugging easier
  • Reactive programming simplifies state management
  • Modular design enables parallel development
  • Real-time listeners provide seamless UX

Performance Insights

  • Server timestamps eliminate sync issues
  • Proper indexing improves query performance
  • Memory management prevents crashes
  • Error handling improves reliability

Development Tips

  • Start with MVP and iterate
  • Test multiplayer scenarios early
  • Monitor API usage and costs
  • Implement comprehensive error handling

Visual Architecture Summary

Complete System Overview

Complete System Overview

Development Workflow

If the Mermaid diagrams in this article are not rendering properly, here are some solutions:

Platform-Specific Issues:

GitHub/GitLab: Mermaid should render natively
Hashnode: Enable Mermaid in post settings
dev.to: Mermaid is supported but may need refresh
Local Markdown Viewers: Install Mermaid extension

Alternative Solutions:

  1. View on GitHub: Copy the markdown to a GitHub README for guaranteed rendering
  2. Online Viewers: Use Mermaid Live Editor to view diagrams
  3. Browser Extensions: Install Mermaid preview extensions
  4. Text Alternatives: ASCII diagrams are provided where complex visuals are needed

Conclusion

Building a real-time multiplayer quiz app requires careful consideration of architecture, state management, and performance. The combination of Flutter, Firebase, and OpenAI provides a powerful foundation for creating engaging multiplayer experiences.

Key success factors:

  • Proper data modeling for real-time sync
  • Efficient state management with reactive programming
  • Robust error handling for production reliability
  • Performance optimization for scale

The result is a scalable application that demonstrates modern mobile development capabilities while providing genuine value to users through AI-powered content and real-time competition.


Try the Final Result

Ready to see this architecture in action? Check out QuizzlerAI - a real-time multiplayer quiz app built using these exact techniques.

🍎 App Store: https://apps.apple.com/us/app/quizzlerai-play/id6746399137

🌐 Landing Page: https://quizzlerai-play.netlify.app/

The app features daily coin allocation (100 free coins every 24 hours) for creating custom AI-generated quizzes, supporting up to 8 concurrent players in real-time competition.

Top comments (0)