Building Real-Time Chat Applications with Flutter and Firebase

In today’s mobile-first world, real-time chat applications have become an integral part of our daily communication. Flutter, Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, combined with Firebase, a powerful backend-as-a-service (BaaS) platform, provides a seamless and efficient way to build such applications.

Why Flutter and Firebase for Chat Applications?

  • Rapid Development: Flutter’s hot reload and expressive UI framework accelerate development.
  • Cross-Platform Compatibility: Develop for iOS, Android, and web using the same codebase.
  • Real-Time Capabilities: Firebase Realtime Database and Cloud Firestore offer real-time data synchronization.
  • Scalability: Firebase’s scalable infrastructure handles growing user bases.
  • Cost-Effective: Firebase provides generous free tiers suitable for initial development and small-scale applications.

Setting Up the Project

To start building a real-time chat application, follow these steps:

Step 1: Flutter Project Setup

Create a new Flutter project:

flutter create flutter_chat_app

Step 2: Firebase Project Setup

Go to the Firebase Console and create a new project. Follow the wizard to set up your project.

Step 3: Firebase Configuration

  1. Add Firebase to your Flutter app. Follow the Firebase console instructions for Android and iOS setup.
  2. Install the necessary Firebase packages in your Flutter project:
flutter pub add firebase_core
flutter pub add firebase_auth
flutter pub add cloud_firestore
flutter pub add firebase_storage

Note: Ensure that you configure both Android and iOS setup on Firebase. Download the google-services.json for Android, and GoogleService-Info.plist for iOS to their respective destination in your flutter project for each platforms. If the setup isn’t completed, running flutter applications for both platforms, might cause unexpected behaviors, from building failure to missing configuration/permission errors.

Step 4: Initialize Firebase

Initialize Firebase in your main.dart file:

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Chat App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ChatScreen(),
    );
  }
}

class ChatScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Chat App'),
      ),
      body: Center(
        child: Text('Chat Screen Content'),
      ),
    );
  }
}

Implementing Authentication

Authentication is crucial for identifying users. Firebase Authentication simplifies the process.

Step 1: Add Authentication Logic

Implement user registration and login functionalities using Firebase Authentication:

import 'package:firebase_auth/firebase_auth.dart';

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  Future signUp(String email, String password) async {
    try {
      UserCredential userCredential = await _auth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );
      return userCredential;
    } catch (e) {
      print("Error signing up: \$e");
      return null;
    }
  }

  Future signIn(String email, String password) async {
    try {
      UserCredential userCredential = await _auth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
      return userCredential;
    } catch (e) {
      print("Error signing in: \$e");
      return null;
    }
  }

  Future signOut() async {
    await _auth.signOut();
  }
}

Step 2: Create UI for Authentication

Create simple UI components for login and registration using Flutter’s widgets:

import 'package:flutter/material.dart';
import 'auth_service.dart'; // Assuming AuthService is in auth_service.dart

class AuthScreen extends StatefulWidget {
  @override
  _AuthScreenState createState() => _AuthScreenState();
}

class _AuthScreenState extends State {
  final AuthService _authService = AuthService();
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  bool _isSigningUp = false; // To toggle between sign-in and sign-up

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_isSigningUp ? 'Sign Up' : 'Sign In'),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextFormField(
              controller: _emailController,
              keyboardType: TextInputType.emailAddress,
              decoration: InputDecoration(
                labelText: 'Email',
                border: OutlineInputBorder(),
              ),
            ),
            SizedBox(height: 12),
            TextFormField(
              controller: _passwordController,
              obscureText: true,
              decoration: InputDecoration(
                labelText: 'Password',
                border: OutlineInputBorder(),
              ),
            ),
            SizedBox(height: 24),
            ElevatedButton(
              onPressed: () async {
                if (_isSigningUp) {
                  // Sign up logic
                  final userCredential = await _authService.signUp(
                    _emailController.text.trim(),
                    _passwordController.text.trim(),
                  );
                  if (userCredential != null) {
                    // Navigate to chat screen or show success message
                    print('Sign up successful: ${userCredential.user?.email}');
                  } else {
                    // Show error message
                    print('Sign up failed');
                  }
                } else {
                  // Sign in logic
                  final userCredential = await _authService.signIn(
                    _emailController.text.trim(),
                    _passwordController.text.trim(),
                  );
                  if (userCredential != null) {
                    // Navigate to chat screen or show success message
                    print('Sign in successful: ${userCredential.user?.email}');
                  } else {
                    // Show error message
                    print('Sign in failed');
                  }
                }
              },
              child: Text(_isSigningUp ? 'Sign Up' : 'Sign In'),
            ),
            SizedBox(height: 16),
            TextButton(
              onPressed: () {
                setState(() {
                  _isSigningUp = !_isSigningUp;
                });
              },
              child: Text(
                _isSigningUp
                    ? 'Already have an account? Sign In'
                    : 'Need an account? Sign Up',
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}

Building the Chat Interface

The chat interface displays messages and allows users to send new ones.

Step 1: Design the UI

Create the basic structure for the chat screen, including a list of messages and an input field:

import 'package:flutter/material.dart';

class ChatScreen extends StatefulWidget {
  @override
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State {
  final TextEditingController _messageController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Chat'),
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: 10, // Dummy value for now
              itemBuilder: (context, index) {
                return ChatBubble(
                  message: 'Message \$index',
                  isMe: index % 2 == 0,
                );
              },
            ),
          ),
          _buildChatInput(),
        ],
      ),
    );
  }

  Widget _buildChatInput() {
    return Container(
      padding: EdgeInsets.all(8.0),
      child: Row(
        children: [
          Expanded(
            child: TextField(
              controller: _messageController,
              decoration: InputDecoration(
                hintText: 'Enter message...',
                border: OutlineInputBorder(),
              ),
            ),
          ),
          IconButton(
            icon: Icon(Icons.send),
            onPressed: () {
              // Send message logic here
              print('Sending message: ${_messageController.text}');
              _messageController.clear();
            },
          ),
        ],
      ),
    );
  }
}

class ChatBubble extends StatelessWidget {
  final String message;
  final bool isMe;

  ChatBubble({required this.message, required this.isMe});

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
      padding: EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: isMe ? Colors.blue[100] : Colors.grey[300],
        borderRadius: BorderRadius.circular(8),
      ),
      alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
      child: Text(message),
    );
  }
}

Step 2: Connect to Firebase

Fetch and display messages from Cloud Firestore:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';

class ChatScreen extends StatefulWidget {
  @override
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State {
  final TextEditingController _messageController = TextEditingController();
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  final FirebaseAuth _auth = FirebaseAuth.instance;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Chat'),
        actions: [
          IconButton(
            icon: Icon(Icons.exit_to_app),
            onPressed: () async {
              await _auth.signOut();
              // Navigate to the AuthScreen after signing out
              Navigator.of(context).pushReplacement(MaterialPageRoute(
                builder: (context) => AuthScreen(), // Assuming AuthScreen is the name of your authentication screen
              ));
            },
          ),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: StreamBuilder(
              stream: _firestore.collection('messages').orderBy('createdAt', descending: false).snapshots(),
              builder: (context, snapshot) {
                if (!snapshot.hasData) {
                  return Center(child: CircularProgressIndicator());
                }
                return ListView.builder(
                  itemCount: snapshot.data!.docs.length,
                  itemBuilder: (context, index) {
                    var message = snapshot.data!.docs[index];
                    return ChatBubble(
                      message: message['text'],
                      isMe: message['senderId'] == _auth.currentUser?.uid,
                      timestamp: message['createdAt'], // Display the timestamp in your chat bubble
                    );
                  },
                );
              },
            ),
          ),
          _buildChatInput(),
        ],
      ),
    );
  }

  Widget _buildChatInput() {
    return Container(
      padding: EdgeInsets.all(8.0),
      child: Row(
        children: [
          Expanded(
            child: TextField(
              controller: _messageController,
              decoration: InputDecoration(
                hintText: 'Enter message...',
                border: OutlineInputBorder(),
              ),
            ),
          ),
          IconButton(
            icon: Icon(Icons.send),
            onPressed: () {
              _sendMessage(_messageController.text);
              _messageController.clear();
            },
          ),
        ],
      ),
    );
  }

  void _sendMessage(String text) async {
    User? user = _auth.currentUser;
    if (user != null) {
      await _firestore.collection('messages').add({
        'text': text,
        'senderId': user.uid,
        'createdAt': FieldValue.serverTimestamp(), // Add the current server timestamp
      });
    }
  }
}

class ChatBubble extends StatelessWidget {
  final String message;
  final bool isMe;
  final Timestamp timestamp;

  ChatBubble({required this.message, required this.isMe, required this.timestamp});

  @override
  Widget build(BuildContext context) {
        // Format the timestamp into a readable date/time
    String formattedTime = DateTime.fromMillisecondsSinceEpoch(timestamp.millisecondsSinceEpoch).toString();

    return Container(
      margin: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
      padding: EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: isMe ? Colors.blue[100] : Colors.grey[300],
        borderRadius: BorderRadius.circular(8),
      ),
      alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
      child: Column(
        crossAxisAlignment: isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
        children: [
          Text(
            message,
            style: TextStyle(fontSize: 16),
          ),
          Text(
            formattedTime, // Show the formatted time here
            style: TextStyle(fontSize: 10, color: Colors.grey[600]),
          ),
        ],
      ),
    );
  }
}

Advanced Features

  • Message Encryption: Implement end-to-end encryption for enhanced security.
  • Media Sharing: Integrate Firebase Storage to allow users to share images, videos, and files.
  • Typing Indicators: Display real-time typing indicators to enhance user engagement.
  • Read Receipts: Implement read receipts to notify users when their messages have been read.
  • Push Notifications: Use Firebase Cloud Messaging (FCM) to send push notifications for new messages.

Conclusion

Building a real-time chat application with Flutter and Firebase is an efficient and scalable solution. By leveraging Flutter’s UI capabilities and Firebase’s backend services, developers can create engaging and interactive chat experiences. This guide covers the foundational steps for setting up the project, implementing authentication, and building the chat interface. Incorporating advanced features will further enhance the application, providing a robust and user-friendly communication platform.