A chat app with Firebase tutorial for the J4J - Flutter group.
Create a new flutter project with only the following platforms: android
, iOS
, and Web
with the command:
flutter create --platforms=android,ios,web chatapp_with_firebase
- Create the login page ui with no functionality
- Create the sign-up page ui with no functionality
- Create the chat page ui
- Format the date and time using the
intl
package - Create a
ChatMessageBubble
- Format the date and time using the
- Add validation to the login and signup forms
- Add validation to the login form
- Add validation to the signup form
- Firebase dependencies and setup
- Set up
firebase_auth
inmain.dart
- Set up
cloud_firestore
in the project
- Delete the code of the
MyHomePage
widget in themain.dart
file. - In the
lib
directory, create a new directory calledpages
; inside it, create a new file calledlogin_page.dart
. - In the
login_page.dart
file creates a stateful widget calledLoginPage
. - Set the
LoginPage
widget as the home of theMaterialApp
widget in themain.dart
file. - Run in the emulator or physical device to see the result.
- Create the user interface as you like:
-
Create a new file called
signup_page.dart
in thepages
directory. -
Temporarily set the
SignupPage
widget as the home of theMaterialApp
widget in themain.dart
file. -
To save time, we can copy the content of the
login_page.dart
file to thesignup_page.dart
file and add things. Change the class name to SignupPage (use F2) -
Add confirm your password field to the form
-
Add
Divider
and an option to navigate to the login page hint: You can useInkWell
to change control every pixel of the pressable area -
Update the login page to include the option to navigate to the signup page like this:
-
Add the functionality to navigate between the login and signup pages. Here is an example for the
LoginPage
:onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const SignUpPage(), ), ); },
-
How do you pop the page from the navigation stack if you're using the
push
method?
-
Create a new file called
chat_page.dart
in thepages
directory. -
Create a stateful widget called
ChatPage
in thechat_page.dart
file. -
Temporarily set the
ChatPage
widget as the home of theMaterialApp
widget in themain.dart
file. -
Create a class called
ChatMessageModel
in thechat_page.dart
file (We will refactor it later), with the properties:- message
- sender
- time
-
Create a list of
ChatMessageModel
calledmessages
and add some dummy data.final List<ChatMessageModel> messages = [ ChatMessageModel( message: "Hello world", sender: "me", time: DateTime.now(), ), ChatMessageModel( message: "Hello J4J group", sender: "Bob", time: DateTime.now().subtract(const Duration(minutes: 5)), ), ChatMessageModel( message: "Hello Bob", sender: "Alice", time: DateTime.now().subtract(const Duration(minutes: 10)), ), ];
-
Sort the dummy data by time. Use the
sort
method -
Create the body with a
Column
widget. The column will contain theListView
with the messages and theTextField
for the user to type the message. -
Create a
ListView.builder
to display the messages and show each message in aListTile
widget. -
Create a
Divider
widget to separate theListView
from theTextField
's row. -
Create a
Row
widget to contain theTextField
and theTextButton.icon
to send the message. Note: you will need to use theExpanded
widget somewhere
-
Meet Flutter's date and time formatting library,
intl
. -
Add the
intl
package to thepubspec.yaml
file by copying the following line to thedependencies
section, then save (ctrl+ s
):intl: ^0.18.1
-
Or, run this command in the terminal:
flutter pub add intl
-
In the
chat_page.dart
file, import theintl
package:import 'package:intl/intl.dart';
-
Create a
DateFormat
object to format the date and time:final DateFormat dateFormat = DateFormat("dd/MM/yyyy"); final DateFormat timeFormat = DateFormat("HH:mm");
-
Use the
dateFormat
andtimeFormat
objects to format the date and time in theListTile
widget:
return ListTile(
title: Text(message.message),
subtitle: Text(message.sender),
trailing: Text(
- message.time.toString(),
+ "${dateFormat.format(message.time)} ${timeFormat.format(message.time)}"),
);
-
Under the
lib
directory, create a new directory calledcomponents
. -
In the
components
directory, create a new file calledchat_message_bubble.dart
. -
Create a stateless widget called
ChatMessageBubble
in thechat_message_bubble.dart
file. The widget will take aChatMessageModel
object as a parameter.class ChatMessageBubble extends StatelessWidget { final ChatMessageModel message; const ChatMessageBubble( this.message, { super.key, }); @override Widget build(BuildContext context) {
-
Build the UI of the
ChatMessageBubble
widget as you like. -
Change the
ListTile
widget in theChatPage
widget to use theChatMessageBubble
widget.- return ListTile( - title: Text(message.message), - subtitle: Text(message.sender), - trailing: Text( - "${dateFormat.format(message.time)} ${timeFormat.format(message.time)}", - ), - ); + return ChatMessageBubble(message);
-
Create something like this:
- What is the validation for the
email
field? - What is the validation for the
password
field? - Check if the form is valid before submitting it.
- Add
TextEditingController
to theemail
andpassword
fields to get their values and use them in the submission.
- Email field - !!Reuse the same validator!!
- Password field - !!Reuse the same validator!!
- Password confirmation field - how to validate it?
- Add
TextEditingController
to theemail
andpassword
fields to get their values and use them in the submission. Maybe you will need to add them before validating the confirmation field
All of the docs for Firebase are available here.
In the past we had to do the setup manually, but now we can use the Firebase CLI
to do it for us. To use it, we need to install it first via the terminal:
npm install -g firebase-tools
After installing the Firebase CLI
, we need to log in to our Google account:
firebase login
-
Add the
firebase_core
package to thepubspec.yaml
file:flutter pub add firebase_core
-
We want our app to use the Firebase Authentication service, so we need to add the
firebase_auth
package to thepubspec.yaml
file:flutter pub add firebase_auth
-
We want our app to use the Firebase Cloud Firestore service as our database, so we need to add the
cloud_firestore
package to thepubspec.yaml
file:flutter pub add cloud_firestore
Or maybe add them all at once:
flutter pub add firebase_core firebase_auth cloud_firestore
-
You can create manually a new Firebase project from the Firebase console, Or you can use the
Firebase CLI
to create a new project:firebase projects:create
-
After creating the Firebase project, we need to add the Flutter project to it. To do that, we need to run the following commands in the terminal:
-
Enable the
flutterfire_cli
dart plugin:flutter pub global activate flutterfire_cli
-
Run the
flutterfire_cli
tool to register our apps. Follow the instructions:flutterfire configure
-
After registering the apps, we need to configure them in our
main.dart
file:import 'package:firebase_core/firebase_core.dart'; import 'firebase_options.dart'; // ... other imports Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); runApp(const MyApp()); }
Note: We need to finish the setup of the Firebase project in the Firebase console.
-
Back at the Firebase console, we need to enable the
Authentication
andCloud Firestore
services.- In the
Authentication
service, enable theEmail/Password
sign-in method andAnonymous
sign-in methods. - In the
Cloud Firestore
service, create a new database intest mode
. Select theCloud Firestore location
of the database and clickEnable
. Suggestion:eur3
- In the
-
Security alert: For best practices, you must add the following files to the
.gitignore
file:google-services.json GoogleService-Info.plist firebase_options.dart firebase_app_id_file.json
Those files contain sensitive information about your Firebase project. You can create them again by running the
flutterfire configure
command. -
Now, the setup is done! π
-
Based on the firebase_auth
state we want to control the navigation between the LoginPage
and the ChatPage
widgets.
-
We will listen to changes in the
firebase_auth
state using theauthStateChanges
stream.@override Widget build(BuildContext context) { - return MaterialApp( - title: 'J4J Chat App', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), - home: const LoginPage(), + return StreamBuilder<User?>( + stream: FirebaseAuth.instance.userChanges(), + builder: (_, snapshot) { + return MaterialApp( + title: 'J4J Chat App', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: snapshot.data == null ? const LoginPage() : const ChatPage(), + ); + },
You can add a print statement to see the changes in the
authStateChanges
stream.Note: in the coming steps, each function should be wrapped with a try-catch clause to handle edge cases
Consider using a
SnackBar
to show error messages -
SignUpPage
: add the functionality to sign up a new user using thefirebase_auth
package.-
Check if the form state is valid
-
Create a new user using the
createUserWithEmailAndPassword
method like:FirebaseAuth.instance.createUserWithEmailAndPassword( email: _emailController.text.trim(), password: _passwordController.text.trim(), );
Note: Hover on the methods to see edge-cases
-
Pop the
SignUpPage
widget from the navigation stack and let theStreamBuilder
in themain.dart
file handles the navigation to theChatPage
widget.
-
-
LoginPage
: add the functionality to sign in an existing user using thefirebase_auth
package.-
Check if the form state is valid
-
Sign in the user using the
signInWithEmailAndPassword
method like:FirebaseAuth.instance.signInWithEmailAndPassword( email: _emailController.text.trim(), password: _passwordController.text.trim(), );
Note: Hover on the methods to see edge-cases
-
We don't need to pop the
LoginPage
widget from the navigation stack because theStreamBuilder
in themain.dart
file will handle the navigation to theChatPage
widget. -
Add the button to enable anonymous sign-in. Use the
signInAnonymously
method like:FirebaseAuth.instance.signInAnonymously();
Note: Hover on the methods to see edge-cases
-
-
ChatPage
: add the functionality to sign out the user using thefirebase_auth
package.-
Add an option to sign out the user using the
signOut
method like:FirebaseAuth.instance.signOut();
-
Refactor the input field row to a StatefulWidget called
ChatInputForm
. Wrap with aForm
widget and add a controller and the validation to the submission. Later we will add the functionality to send the message to the database
-
-
Create a new file called
collections.dart
in thelib
directory. -
Create a variable called
kChatMsgsCollection
of typeCollectionReference
and initialize it with thechat_messages
collection in the database:final CollectionReference<Map<String, dynamic>> kChatMsgsCollection = FirebaseFirestore.instance.collection('chat_messages');
-
In
ChatPage
, remove the dummyList<ChatMessageModel> messages
created earlier. -
Wrap the
ListView.builder
widget with aStreamBuilder
widget. The stream will be thesnapshots
stream of thekChatMsgsCollection
variable. Note: the stream query should look like that:stream: kChatMsgsCollection.orderBy('time', descending: false).snapshots();
This will sort the messages by time in ascending order.
-
Handle cases when the stream is waiting,has an error, or lacks data in the database.
-
In the
ChatMessageModel
class, add two methods:fromJson
factory method to convert aMap<String, dynamic>
object to aChatMessageModel
object.toJson
method to convert aChatMessageModel
object to aMap<String, dynamic>
object.
-
Back at the
ChatPage
widget, convert theQuerySnapshot
object to aList<ChatMessageModel>
object using themap
method and thefromJson
factory method, something like:final messages = snapshot.data!.docs.map((doc) { final data = doc.data(); return ChatMessageModel.fromMap(data); }).toList();
-
In
ChatInputForm
implement the functionality to send the message to the database.-
Check if the form state is valid
-
Create a new
ChatMessageModel
object with the message, sender, and time.- The message is the value of the
message
field in the form. - The sender is the uid of the current user. Hint: use the
FirebaseAuth.instance.currentUser.uid
getter - The time is the current time. Hint: use the
DateTime.now()
constructor
- The message is the value of the
-
Add the message to the database using the
add
method like:kChatMsgsCollection.add(message.toJson());
-
Clear the input field and reset the form state,
-
-
In
ChatMessageBubble
widget, update the check of the sender to show the message on the right side if the sender is the current user. Hint: use theFirebaseAuth.instance.currentUser.uid
getter