© Khmer Angkor Academy - sophearithput168

Project: Chat App

សាងសង់ Chat App

បង្កើត Real-time Chat Application ដោយប្រើ Firebase Cloud Firestore និង Firebase Authentication។ Project នេះបង្ហាញពី real-time data synchronization, user authentication, និង advanced UI patterns សម្រាប់ messaging apps។

🏗️ Real-Time Chat Architecture

Firebase Chat Architecture:

Chat App Architecture (Firebase)

┌────────────────────────────────────────┐
│        Presentation Layer              │
│  - LoginScreen (Authentication UI)     │
│  - UsersListScreen (All users)         │
│  - ChatScreen (Messages + Input)       │
│  - MessageBubble (Chat bubble widget)  │
└───────────┬────────────────────────────┘
            │
            ↓ User Actions & Real-time Updates
            │
┌───────────┴────────────────────────────┐
│      Firebase Services Layer           │
│  - Firebase Auth (User management)     │
│  - Firestore (Real-time database)      │
│  - Cloud Storage (Images/files)        │
│  - FCM (Push notifications)            │
└───────────┬────────────────────────────┘
            │
            ↓ Real-time Synchronization
            │
┌───────────┴────────────────────────────┐
│       Firestore Data Structure         │
│  /users/{userId}                       │
│    - name, email, isOnline, avatar     │
│                                        │
│  /chats/{chatId}/messages/{messageId}  │
│    - senderId, text, timestamp         │
│                                        │
│  chatId = sorted([user1Id, user2Id])   │
└────────────────────────────────────────┘

Real-time Flow:
1. User A sends message
2. Firestore.add() → Cloud
3. Firestore triggers update
4. User B's StreamBuilder receives
5. UI rebuilds automatically!

Why Firebase for Chat Apps?

Feature Firebase Solution Benefit for Chat
Real-time Sync Firestore snapshots() Messages appear instantly for all users
Authentication Firebase Auth Secure login with email/Google/phone
Offline Support Firestore cache Messages load even without internet
Scalability Auto-scaling Handles millions of messages
Security Security Rules Users can only read their own chats
Cloud Storage Firebase Storage Store images, videos, files
Push Notifications FCM Notify users of new messages

🎯 មុខងារ (Features)

  • 🔐 Authentication: ចុះឈ្មោះ និង ចូលដោយប្រើ email/password
  • 👥 Users List: បង្ហាញអ្នកប្រើប្រាស់ទាំងអស់
  • 💬 Real-time Messaging: ផ្ញើ និង ទទួលសារភ្លាមៗ
  • 🟢 Online Status: បង្ហាញស្ថានភាព online/offline
  • Timestamps: បង្ហាញពេលវេលាផ្ញើសារ
  • ✓✓ Read Receipts: ដឹងថាសារត្រូវបានអាន
  • 🔄 Auto-scroll: Scroll to bottom when new message
  • 💾 Offline Support: Messages cached locally

📝 Complete Chat Implementation

Below is the complete implementation with all features integrated:

// main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(ChatApp());
}

// Authentication Service
class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  
  Future<void> signUp(String email, String password, String name) async {
    try {
      UserCredential userCredential = await _auth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );
      
      await _firestore.collection('users').doc(userCredential.user!.uid).set({
        'name': name,
        'email': email,
        'createdAt': FieldValue.serverTimestamp(),
        'isOnline': true,
      });
    } on FirebaseAuthException catch (e) {
      throw Exception('Sign up failed: ' + e.message.toString());
    }
  }
  
  Future<void> signIn(String email, String password) async {
    try {
      UserCredential userCredential = await _auth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
      
      await _firestore.collection('users').doc(userCredential.user!.uid).update({
        'isOnline': true,
      });
    } on FirebaseAuthException catch (e) {
      throw Exception('Login failed: ' + e.message.toString());
    }
  }
  
  Future<void> signOut() async {
    String? userId = _auth.currentUser?.uid;
    if (userId != null) {
      await _firestore.collection('users').doc(userId).update({
        'isOnline': false,
        'lastSeen': FieldValue.serverTimestamp(),
      });
    }
    await _auth.signOut();
  }
}

// Chat Screen
class ChatScreen extends StatefulWidget {
  final String userId;
  final String userName;
  
  const ChatScreen({
    required this.userId,
    required this.userName,
  });
  
  @override
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  final TextEditingController _messageController = TextEditingController();
  final FirebaseAuth _auth = FirebaseAuth.instance;
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  
  String get currentUserId => _auth.currentUser!.uid;
  
  String getChatId() {
    List<String> ids = [currentUserId, widget.userId];
    ids.sort();
    return ids.join('_');
  }
  
  Future<void> sendMessage() async {
    if (_messageController.text.trim().isEmpty) return;
    
    final chatId = getChatId();
    
    await _firestore
        .collection('chats')
        .doc(chatId)
        .collection('messages')
        .add({
      'senderId': currentUserId,
      'receiverId': widget.userId,
      'text': _messageController.text.trim(),
      'timestamp': FieldValue.serverTimestamp(),
      'isRead': false,
    });
    
    _messageController.clear();
  }
  
  @override
  Widget build(BuildContext context) {
    final chatId = getChatId();
    
    return Scaffold(
      appBar: AppBar(
        title: Row(
          children: [
            CircleAvatar(
              radius: 18,
              child: Text(widget.userName[0]),
            ),
            SizedBox(width: 12),
            Text(widget.userName),
          ],
        ),
      ),
      body: Column(
        children: [
          Expanded(
            child: StreamBuilder<QuerySnapshot>(
              stream: _firestore
                  .collection('chats')
                  .doc(chatId)
                  .collection('messages')
                  .orderBy('timestamp', descending: true)
                  .snapshots(),
              builder: (context, snapshot) {
                if (!snapshot.hasData) {
                  return Center(child: CircularProgressIndicator());
                }
                
                final messages = snapshot.data!.docs;
                
                if (messages.isEmpty) {
                  return Center(
                    child: Text('No messages yet'),
                  );
                }
                
                return ListView.builder(
                  reverse: true,
                  itemCount: messages.length,
                  itemBuilder: (context, index) {
                    final message = messages[index];
                    final isMe = message['senderId'] == currentUserId;
                    
                    return MessageBubble(
                      text: message['text'],
                      isMe: isMe,
                      timestamp: message['timestamp']?.toDate(),
                    );
                  },
                );
              },
            ),
          ),
          Container(
            padding: EdgeInsets.all(8),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _messageController,
                    decoration: InputDecoration(
                      hintText: 'Type a message...',
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(24),
                      ),
                    ),
                  ),
                ),
                SizedBox(width: 8),
                CircleAvatar(
                  child: IconButton(
                    icon: Icon(Icons.send),
                    onPressed: sendMessage,
                    color: Colors.white,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
  
  @override
  void dispose() {
    _messageController.dispose();
    super.dispose();
  }
}

// Message Bubble Widget
class MessageBubble extends StatelessWidget {
  final String text;
  final bool isMe;
  final DateTime? timestamp;
  
  const MessageBubble({
    required this.text,
    required this.isMe,
    this.timestamp,
  });
  
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
      child: Align(
        alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
        child: Container(
          constraints: BoxConstraints(
            maxWidth: MediaQuery.of(context).size.width * 0.7,
          ),
          padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
          decoration: BoxDecoration(
            color: isMe ? Colors.blue : Colors.grey[300],
            borderRadius: BorderRadius.circular(20),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                text,
                style: TextStyle(
                  color: isMe ? Colors.white : Colors.black,
                ),
              ),
              if (timestamp != null)
                Padding(
                  padding: EdgeInsets.only(top: 4),
                  child: Text(
                    timestamp!.hour.toString() + ':' + 
                    timestamp!.minute.toString().padLeft(2, '0'),
                    style: TextStyle(
                      fontSize: 10,
                      color: isMe ? Colors.white70 : Colors.black54,
                    ),
                  ),
                ),
            ],
          ),
        ),
      ),
    );
  }
}

🔥 Firestore Security Rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    
    match /users/{userId} {
      allow read: if request.auth != null;
      allow write: if request.auth.uid == userId;
    }
    
    match /chats/{chatId}/messages/{messageId} {
      allow read: if request.auth != null && 
                     chatId.matches('.*' + request.auth.uid + '.*');
      allow create: if request.auth != null &&
                       request.resource.data.senderId == request.auth.uid;
    }
  }
}

🚀 Advanced Features to Add

  • 📸 Image Messaging: Upload and send images via Firebase Storage
  • 👥 Group Chats: Multi-user conversations
  • 🔔 Push Notifications: FCM for new message alerts
  • ⌨️ Typing Indicator: Show when user is typing
  • 🔍 Search Messages: Full-text search in chats
  • 🎤 Voice Messages: Record and send audio
  • 📎 File Sharing: Share documents and files
  • ❤️ Message Reactions: Emoji reactions to messages
  • 🗑️ Delete Messages: Remove sent messages
  • ✏️ Edit Messages: Modify sent messages

💡 Best Practices:

  • StreamBuilder: Use for real-time data updates
  • Sorted Chat IDs: Prevent duplicate conversations
  • Server Timestamps: Use FieldValue.serverTimestamp()
  • Security Rules: Protect user privacy
  • Offline Cache: Firestore handles offline automatically
  • Dispose Controllers: Always clean up resources
  • Pagination: Limit messages loaded (use .limit())
  • Online Status: Update on login/logout

⚠️ Common Mistakes:

  • ❌ Not sorting user IDs (creates duplicate chats)
  • ❌ Using DateTime.now() instead of serverTimestamp
  • ❌ No security rules (privacy vulnerability!)
  • ❌ Forgetting to dispose TextEditingController
  • ❌ Not handling loading/error states
  • ❌ Sending empty messages (no validation)
  • ❌ Not updating online status on logout

✅ លទ្ធផល: អ្នកបានបង្កើត Real-time Chat App ពេញលេញដោយប្រើ Firebase! Project នេះបង្ហាញពី real-time synchronization, authentication, StreamBuilder patterns, security rules។ បន្ថែម features ដូចជា group chat, voice/video calls, file sharing, typing indicators, push notifications។ នេះជា foundation ល្អសម្រាប់ production messaging apps!