Make Your First Chat App in Flutter with Firebase
Building a chat application is an excellent way to learn real-time features in Flutter using Firebase. In this tutorial, we'll create a simple but functional chat app that demonstrates user authentication, real-time messaging, and basic UI implementation.
Prerequisites
Before starting, make sure you have:
- Flutter SDK installed
- Firebase account
- Basic understanding of Flutter widgets
- Firebase CLI installed
Setting Up Firebase
- Create a new Firebase project in the Firebase Console
- Add Flutter application to your Firebase project
- Download the
google-services.json
(Android) andGoogleService-Info.plist
(iOS) - Add the necessary dependencies to your
pubspec.yaml
:
dependencies: flutter: sdk: flutter firebase_core: ^2.24.2 firebase_auth: ^4.15.3 cloud_firestore: ^4.13.6 provider: ^6.1.1
Project Structure
Create the following files in your project:
lib/
├── main.dart
├── screens/
│ ├── login_screen.dart
│ ├── chat_screen.dart
│ └── registration_screen.dart
├── models/
│ ├── message.dart
│ └── user.dart
└── services/
├── auth_service.dart
└── database_service.dart
Implementing Authentication
First, let's create the authentication service:
// lib/services/auth_service.dart import 'package:firebase_auth/firebase_auth.dart'; class AuthService { final FirebaseAuth _auth = FirebaseAuth.instance; // Sign in with email and password Future<UserCredential?> signInWithEmailAndPassword( String email, String password) async { try { return await _auth.signInWithEmailAndPassword( email: email, password: password, ); } catch (e) { print(e.toString()); return null; } } // Register with email and password Future<UserCredential?> registerWithEmailAndPassword( String email, String password) async { try { return await _auth.createUserWithEmailAndPassword( email: email, password: password, ); } catch (e) { print(e.toString()); return null; } } // Sign out Future<void> signOut() async { await _auth.signOut(); } }
Creating the Login Screen
Now, let's implement the login screen:
// lib/screens/login_screen.dart import 'package:flutter/material.dart'; import '../services/auth_service.dart'; class LoginScreen extends StatefulWidget { @override _LoginScreenState createState() => _LoginScreenState(); } class _LoginScreenState extends State<LoginScreen> { final _formKey = GlobalKey<FormState>(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _authService = AuthService(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Chat App Login')), body: Padding( padding: EdgeInsets.all(16.0), child: Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextFormField( controller: _emailController, decoration: InputDecoration(labelText: 'Email'), validator: (value) => value!.isEmpty ? 'Please enter an email' : null, ), SizedBox(height: 16.0), TextFormField( controller: _passwordController, decoration: InputDecoration(labelText: 'Password'), obscureText: true, validator: (value) => value!.isEmpty ? 'Please enter a password' : null, ), SizedBox(height: 24.0), ElevatedButton( onPressed: () async { if (_formKey.currentState!.validate()) { final result = await _authService.signInWithEmailAndPassword( _emailController.text, _passwordController.text, ); if (result != null) { Navigator.pushReplacementNamed(context, '/chat'); } } }, child: Text('Login'), ), ], ), ), ), ); } }
Implementing the Chat Screen
Let's create the chat screen with real-time messaging:
// lib/screens/chat_screen.dart import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; class ChatScreen extends StatefulWidget { @override _ChatScreenState createState() => _ChatScreenState(); } class _ChatScreenState extends State<ChatScreen> { final _messageController = TextEditingController(); final _firestore = FirebaseFirestore.instance; final _auth = FirebaseAuth.instance; void _sendMessage() async { if (_messageController.text.isNotEmpty) { await _firestore.collection('messages').add({ 'text': _messageController.text, 'sender': _auth.currentUser!.email, 'timestamp': FieldValue.serverTimestamp(), }); _messageController.clear(); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Chat Room'), actions: [ IconButton( icon: Icon(Icons.exit_to_app), onPressed: () async { await FirebaseAuth.instance.signOut(); Navigator.pushReplacementNamed(context, '/login'); }, ), ], ), body: Column( children: [ Expanded( child: StreamBuilder<QuerySnapshot>( stream: _firestore .collection('messages') .orderBy('timestamp', descending: true) .snapshots(), builder: (context, snapshot) { if (!snapshot.hasData) { return Center(child: CircularProgressIndicator()); } final messages = snapshot.data!.docs; return ListView.builder( reverse: true, itemCount: messages.length, itemBuilder: (context, index) { final message = messages[index].data() as Map<String, dynamic>; final isMe = message['sender'] == _auth.currentUser!.email; return Padding( padding: EdgeInsets.all(8.0), child: Row( mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start, children: [ Container( padding: EdgeInsets.symmetric( horizontal: 16.0, vertical: 8.0), decoration: BoxDecoration( color: isMe ? Colors.blue : Colors.grey[300], borderRadius: BorderRadius.circular(20.0), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( message['sender'] ?? '', style: TextStyle( fontSize: 12.0, color: isMe ? Colors.white70 : Colors.black54, ), ), Text( message['text'] ?? '', style: TextStyle( color: isMe ? Colors.white : Colors.black, ), ), ], ), ), ], ), ); }, ); }, ), ), Padding( padding: EdgeInsets.all(8.0), child: Row( children: [ Expanded( child: TextField( controller: _messageController, decoration: InputDecoration( hintText: 'Type a message...', border: OutlineInputBorder( borderRadius: BorderRadius.circular(20.0), ), ), ), ), SizedBox(width: 8.0), IconButton( icon: Icon(Icons.send), onPressed: _sendMessage, ), ], ), ), ], ), ); } }
Setting Up the Main App
Finally, let's set up the main app with navigation:
// lib/main.dart import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; import 'screens/login_screen.dart'; import 'screens/chat_screen.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); runApp(ChatApp()); } class ChatApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Chat App', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), initialRoute: '/login', routes: { '/login': (context) => LoginScreen(), '/chat': (context) => ChatScreen(), }, ); } }
Testing the App
To test the app:
- Run
flutter pub get
to install dependencies - Ensure Firebase is properly configured
- Run the app using
flutter run
- Create a new account or log in with existing credentials
- Start sending messages!
Best Practices and Security
-
Error Handling
- Implement proper error handling for authentication
- Show loading states during operations
- Display user-friendly error messages
-
Security Rules Set up proper Firestore security rules:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /messages/{messageId} { allow read: if request.auth != null; allow create: if request.auth != null; } } }
-
Performance Optimization
- Implement pagination for messages
- Cache messages locally
- Optimize image loading if implementing file sharing
Conclusion
You now have a basic but functional chat application built with Flutter and Firebase! This foundation can be extended with features like:
- File sharing
- User profiles
- Group chats
- Message reactions
- Typing indicators
- Push notifications
Remember to:
- Keep your Firebase configuration secure
- Regularly update dependencies
- Test on both Android and iOS
- Handle edge cases and errors
- Implement proper state management for larger applications