Murtaza Sulaihi I am a school professor and I also develop Android applications and Flutter applications.

How to build a chat application in Flutter with Firebase

16 min read 4503

How to build a chat application in Flutter with Firebase

Today, we will create a straightforward yet complex chat application in Flutter with Firebase at its backend. Our primary focus is on working the application and connecting it with Firebase services like Cloud Firestore, Firebase Storage, and Firebase Authentication.

What are we going to cover in the article?

Smart Talk App
We will understand the basic functionality of a chat application so that enthusiasts like you and me can learn from it. Of course, this demo app is not as complex as WhatsApp or Telegram, but after reading this article, you will understand how other famous chat applications work.

Creating a new Flutter application

Android Studio has been updated to Bumblebee, and it’s pretty colorful now on its main screen. Just click on the New Flutter Project button, and it will confirm the Flutter SDK path; click Next.

Then, enter your desired project name — make sure that it is in small letters. Choose your directory correctly and make sure you have selected all the desired platforms like Android, iOS, and the web.

With that done, you will have your Flutter starter project, famously known as the counter application, created.

Connecting to Firebase Services (the new and updated method)

If you plan to use Firebase as your backend, I recommend connecting your project to Firebase before programming your application any further.

Go to firebase.google.com and create a new Firebase project. Enter your project name, disable Google Analytics for now, and click on the Create Project button.

According to the Flutter Fire documentation, you can now initialize Firebase directly from Dart. It’s straightforward, so cheers to the Flutter team.

Run this command from the root of your project in the terminal window to add the Firebase core plugin:



flutter pub add firebase_core

Next, you have to run the FlutterFire CLI command, which depends on the Firebase CLI command. If you are not familiar with the Firebase CLI command, please go through this document to understand and install it on your system.

Run this command to activate FlutterFire CLI:

dart pub global activate flutterfire_cli

Next, run this command and choose the Firebase project you just created:

flutterfire configure

After running this command and connecting to your Firebase project, you will see that the firebase_options.dart file has been created in your project structure, containing all the necessary information.

N.B., now you no longer need to manually add the google-services.json file to Android and the GoogleService-Info.plist file to the iOS runner directory.

In your main.dart file, you need to edit the main() function and ensure WidgetFlutterBinding is initialized and then initialize Firebase like this:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(MyApp());
}

Once initialized, you are now ready to use Flutter Fire and all its services.

Since we will use Google Sign-In for our chat application, Firebase requires SHA-1 and SHA-256 certificates to be added to our Android app inside the Firebase project. Again, you can go through this document to read all about it.

In Android Studio, right click on /gradlew and open with the terminal. Then run this command:


More great articles from LogRocket:


./gradlew signingReport

With this, the signing report generates for your application. Copy the SHA-1 and SHA-256 certificate fingerprints and add them to the project settings inside your Firebase project, under Android App.

Inside your Firebase project, click on the Authentication tab, click on the Sign-in method, and under Sign-in providers, add Google.

Authentication Tab Inside Firebase Project

Click on the Firestore Database and Create a database under Test mode.

Creating A Database Under Test Mode

This is optional, but recommended: to improve iOS and Mac build time, add this line to your iOS/Podfile under Runner:

pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '8.11.0'

N.B., here it is assumed you have CocoaPods installed on your system.

FirebaseFirestore

Click on Firebase Storage and Create new storage under Test Mode.

Create New Storage Under Test Mode

Go to Firebase project settings, click on Apple Apps, and download the GoogleServices-Info.plist file.

Apple Apps In Firebase Project Settings

I know I mentioned earlier that you do not need to add the GoogleServices file to the iOS app. But since we will use the Google Sign-In package, iOS integration documentation on the pub.dev website says otherwise. You have to add CFBundleURLTypes attributes given below in the ios/Runner/Info.plist file:

!-- Put me in the [my_project]/ios/Runner/Info.plist file -->
<!-- Google Sign-in Section -->
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <!-- TODO Replace this value: -->
            <!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
            <string>com.googleusercontent.apps.861823949799-vc35cprkp249096uujjn0vvnmcvjppkn</string>
        </array>
    </dict>
</array>
<!-- End of the Google Sign-in Section -->

Now you have successfully configured your Android and iOS application with Firebase. We are creating a Flutter chat application, but platform-specific integrations are required for the Google Sign-In to work correctly.

Let’s talk about web integration before uploading our completed application to Firebase Hosting.

Building a basic UI for the chat application

We have a total of five screens given below in order.

      1. Splash page
      2. Login page
      3. Homepage
      4. Profile page (settings screen)
      5. Chat page

I’m not going into too many details about each screen because that is unimportant. So instead, I will give an overview for each screen.

Splash Page: It has two text widgets and an image at its center

Login Page: Two text widgets again, an image, and a Google Sign-In button

Home Page: Scaffold, AppBar with two action buttons for a profile page and logging out. It has a search bar for searching users. ListTile consists of the user’s name and Google profile image

Profile Page: Here, users can change their display name and add a few details of themselves. Also, upload an image of themselves

Chat Page: A very similar screen to most prominent chat applications. Message portion at the top of the screen and text field with image and send button at the bottom of the screen

Now, let us start programming the application. I will post only the vital parts of the application code, and the rest is available on the GitHub repository, for which I will leave links as we move along further in this article.

Add required dependencies in the pubspec.yaml file

firebase_core: ^1.12.0
firebase_auth: ^3.3.7
cloud_firestore: ^3.1.8
firebase_storage: ^10.2.7
google_sign_in: ^5.2.4
fluttertoast: ^8.0.8
image_picker: ^0.8.4+9
shared_preferences: ^2.0.13
intl: ^0.17.0
photo_view: ^0.13.0
provider: ^6.0.2
country_code_picker: ^2.0.2
url_launcher: ^6.0.20
equatable: ^2.0.3
google_sign_in_web: ^0.10.0+5

Please check for the latest versions on the pub.dev website.

Uncomment assets to add images to the assets of the application:

- assets/images/

Building a login page with Firebase Authentication

Building A Login Page With Firebase Authentication

Step 1: The ChatUser model

Before we get into the authentication part, we need a user model class. I have named it as ChatUser, which has five string variables: id, photoURL, displayName, phoneNumber, and aboutMe.

Our two functions inside our ChatUser class toJson() consist of a Map and a factory method to read data from the snapshot that Firebase Firestore returns:

class ChatUser extends Equatable {
 final String id;
 final String photoUrl;
 final String displayName;
 final String phoneNumber;
 final String aboutMe;

 const ChatUser(
     {required this.id,
     required this.photoUrl,
     required this.displayName,
     required this.phoneNumber,
     required this.aboutMe});

Step 2: The AuthProvider class

Next, we will add an AuthProvider class to our project to handle Google sign-in and sign-out methods. This is also to check whether the user is logged in or not:

class AuthProvider extends ChangeNotifier {
 final GoogleSignIn googleSignIn;
 final FirebaseAuth firebaseAuth;
 final FirebaseFirestore firebaseFirestore;
 final SharedPreferences prefs;

 Status _status = Status.uninitialized;

 Status get status => _status;

 AuthProvider(
     {required this.googleSignIn,
     required this.firebaseAuth,
     required this.firebaseFirestore,
     required this.prefs});

 String? getFirebaseUserId() {
   return prefs.getString(FirestoreConstants.id);
 }

 Future<bool> isLoggedIn() async {
   bool isLoggedIn = await googleSignIn.isSignedIn();
   if (isLoggedIn &&
       prefs.getString(FirestoreConstants.id)?.isNotEmpty == true) {
     return true;
   } else {
     return false;
   }
 }

 Future<bool> handleGoogleSignIn() async {
   _status = Status.authenticating;
   notifyListeners();

   GoogleSignInAccount? googleUser = await googleSignIn.signIn();
   if (googleUser != null) {
     GoogleSignInAuthentication? googleAuth = await googleUser.authentication;
     final AuthCredential credential = GoogleAuthProvider.credential(
       accessToken: googleAuth.accessToken,
       idToken: googleAuth.idToken,
     );

     User? firebaseUser =
         (await firebaseAuth.signInWithCredential(credential)).user;

     if (firebaseUser != null) {
       final QuerySnapshot result = await firebaseFirestore
           .collection(FirestoreConstants.pathUserCollection)
           .where(FirestoreConstants.id, isEqualTo: firebaseUser.uid)
           .get();
       final List<DocumentSnapshot> document = result.docs;
       if (document.isEmpty) {
         firebaseFirestore
             .collection(FirestoreConstants.pathUserCollection)
             .doc(firebaseUser.uid)
             .set({
           FirestoreConstants.displayName: firebaseUser.displayName,
           FirestoreConstants.photoUrl: firebaseUser.photoURL,
           FirestoreConstants.id: firebaseUser.uid,
           "createdAt: ": DateTime.now().millisecondsSinceEpoch.toString(),
           FirestoreConstants.chattingWith: null
         });}

Step 3: Splash page

We will create the splash page and check whether the user is logged in using our method from the authProvider class.

If the user has already signed in with the Google Sign-In method, the user will be redirected to the home page. Otherwise, the user will be directed to the login page.

Step 4: Login page

Next, we will now create our login page.

Creating A Login Page

Since we are using Provider state management in our application, we are going to create an instance of our authProvider like this:

final authProvider = Provider.of<AuthProvider>(context);

Next, we will check the status of our application if it is authenticated:

class _LoginPageState extends State<LoginPage> {
 @override
 Widget build(BuildContext context) {
   final authProvider = Provider.of<AuthProvider>(context);

   switch (authProvider.status) {
     case Status.authenticateError:
       Fluttertoast.showToast(msg: 'Sign in failed');
       break;
     case Status.authenticateCanceled:
       Fluttertoast.showToast(msg: 'Sign in cancelled');
       break;
     case Status.authenticated:
       Fluttertoast.showToast(msg: 'Sign in successful');
       break;
     default:
       break;
   }

Step 5: Sign-in function

We will now add our Google Sign-In method to our onTap function for the Google Sign-In button:

GestureDetector(
 onTap: () async {
   bool isSuccess = await authProvider.handleGoogleSignIn();
   if (isSuccess) {
     Navigator.pushReplacement(
         context,
         MaterialPageRoute(
             builder: (context) => const HomePage()));
   }
 },
 child: Image.asset('assets/images/google_login.jpg'),
),

Creating a homepage with user contacts

Step 1: The HomeProvider class

This class contains two functions:

To update data on the Cloud Firestore database:

Future<void> updateFirestoreData(
   String collectionPath, String path, Map<String, dynamic> updateData) {
 return firebaseFirestore
     .collection(collectionPath)
     .doc(path)
     .update(updateData);
}

To receive a snapshot of data from the Cloud Firestore database:

Stream<QuerySnapshot> getFirestoreData(
   String collectionPath, int limit, String? textSearch) {
 if (textSearch?.isNotEmpty == true) {
   return firebaseFirestore
       .collection(collectionPath)
       .limit(limit)
       .where(FirestoreConstants.displayName, isEqualTo: textSearch)
       .snapshots();
 } else {
   return firebaseFirestore
       .collection(collectionPath)
       .limit(limit)
       .snapshots();
 }
}

Step 2: Homepage

The homepage is divided into three sections.

Three Sections Of Homepage

      1. The AppBar — it consists of two buttons, the sign-out button and profile page button:
        Scaffold(
           appBar: AppBar(
               centerTitle: true,
               title: const Text('Smart Talk'),
               actions: [
                 IconButton(
                     onPressed: () => googleSignOut(),
                     icon: const Icon(Icons.logout)),
                 IconButton(
                     onPressed: () {
                       Navigator.push(
                           context,
                           MaterialPageRoute(
                               builder: (context) => const ProfilePage()));
                     },
                     icon: const Icon(Icons.person)),
               ]),);
      2. The search bar — for searching the logged-in users inside the application. If you have a long list of users, it comes in handy. We will use a StreamBuilder to build our search bar like this:
        Widget buildSearchBar() {
         return Container(
           margin: const EdgeInsets.all(Sizes.dimen_10),
           height: Sizes.dimen_50,
           child: Row(
             crossAxisAlignment: CrossAxisAlignment.center,
             children: [
               const SizedBox(
                 width: Sizes.dimen_10,
               ),
               const Icon(
                 Icons.person_search,
                 color: AppColors.white,
                 size: Sizes.dimen_24,
               ),
               const SizedBox(
                 width: 5,
               ),
               Expanded(
                 child: TextFormField(
                   textInputAction: TextInputAction.search,
                   controller: searchTextEditingController,
                   onChanged: (value) {
                     if (value.isNotEmpty) {
                       buttonClearController.add(true);
                       setState(() {
                         _textSearch = value;
                       });
                     } else {
                       buttonClearController.add(false);
                       setState(() {
                         _textSearch = "";
                       });
                     }
                   },
                   decoration: const InputDecoration.collapsed(
                     hintText: 'Search here...',
                     hintStyle: TextStyle(color: AppColors.white),
                   ),
                 ),
               ),
               StreamBuilder(
                   stream: buttonClearController.stream,
                   builder: (context, snapshot) {
                     return snapshot.data == true
                         ? GestureDetector(
                             onTap: () {
                               searchTextEditingController.clear();
                               buttonClearController.add(false);
                               setState(() {
                                 _textSearch = '';
                               });
                             },
                             child: const Icon(
                               Icons.clear_rounded,
                               color: AppColors.greyColor,
                               size: 20,
                             ),
                           )
                         : const SizedBox.shrink();
                   })
             ],
           ),
           decoration: BoxDecoration(
             borderRadius: BorderRadius.circular(Sizes.dimen_30),
             color: AppColors.spaceLight,
           ),
         );
        }
      3. Users — with StreamBuilder, we will show all logged-in users here. Using the ListTile widget inside the ListView separated builder method, we display the user’s profile image and the user’s name:
        Widget buildItem(BuildContext context, DocumentSnapshot? documentSnapshot) {
         final firebaseAuth = FirebaseAuth.instance;
         if (documentSnapshot != null) {
           ChatUser userChat = ChatUser.fromDocument(documentSnapshot);
           if (userChat.id == currentUserId) {
             return const SizedBox.shrink();
           } else {
             return TextButton(
               onPressed: () {
                 if (KeyboardUtils.isKeyboardShowing()) {
                   KeyboardUtils.closeKeyboard(context);
                 }
                 Navigator.push(
                     context,
                     MaterialPageRoute(
                         builder: (context) => ChatPage(
                               peerId: userChat.id,
                               peerAvatar: userChat.photoUrl,
                               peerNickname: userChat.displayName,
                               userAvatar: firebaseAuth.currentUser!.photoURL!,
                             )));
               },
               child: ListTile(
                 leading: userChat.photoUrl.isNotEmpty
                     ? ClipRRect(
                         borderRadius: BorderRadius.circular(Sizes.dimen_30),
                         child: Image.network(
                           userChat.photoUrl,
                           fit: BoxFit.cover,
                           width: 50,
                           height: 50,
                           loadingBuilder: (BuildContext ctx, Widget child,
                               ImageChunkEvent? loadingProgress) {
                             if (loadingProgress == null) {
                               return child;
                             } else {
                               return SizedBox(
                                 width: 50,
                                 height: 50,
                                 child: CircularProgressIndicator(
                                     color: Colors.grey,
                                     value: loadingProgress.expectedTotalBytes !=
                                             null
                                         ? loadingProgress.cumulativeBytesLoaded /
                                             loadingProgress.expectedTotalBytes!
                                         : null),
                               );
                             }
                           },
                           errorBuilder: (context, object, stackTrace) {
                             return const Icon(Icons.account_circle, size: 50);
                           },
                         ),
                       )
                     : const Icon(
                         Icons.account_circle,
                         size: 50,
                       ),
                 title: Text(
                   userChat.displayName,
                   style: const TextStyle(color: Colors.black),
                 ),
               ),
             );
           }
         } else {
           return const SizedBox.shrink();
         }
        }

Making a profile page that updates Firebase Firestore information

Here, users can change their display names, write something about themselves, and add their contact information.

There are three TextFields and a dropdown to select a country code before entering the mobile number. Next, users click on the profile picture and choose another to replace it, then there’s a button to update the information onto the Firebase Firestore database. Let’s get to it.

Textfields And Dropdowns

Step 1: ProfileProvider class

We will add another class to our project structure and call it ProfileProvider. There are two main functions inside this class.

To upload the image file to Firebase Storage:

UploadTask uploadImageFile(File image, String fileName) {
 Reference reference = firebaseStorage.ref().child(fileName);
 UploadTask uploadTask = reference.putFile(image);
 return uploadTask;
}

To upload updated information regarding the user to the Firestore database:

Future<void> updateFirestoreData(String collectionPath, String path,
   Map<String, dynamic> dataUpdateNeeded) {
 return firebaseFirestore
     .collection(collectionPath)
     .doc(path)
     .update(dataUpdateNeeded);
}

Step 2: Profile page

There are three main methods inside this ProfilePage stateful widget.

      1. An image picker method to pick a picture from the device and set it as a profile picture:
        Future getImage() async {
         ImagePicker imagePicker = ImagePicker();
         // PickedFile is not supported
         // Now use XFile?
         XFile? pickedFile = await imagePicker
             .pickImage(source: ImageSource.gallery)
             .catchError((onError) {
           Fluttertoast.showToast(msg: onError.toString())
         });
         File? image;
         if (pickedFile != null) {
           image = File(pickedFile.path);
         }
         if (image != null) {
           setState(() {
             avatarImageFile = image;
             isLoading = true;
           });
           uploadFile();
         }
        }
      2. Upload that picture to Firebase Storage and save its photo URL information to the Firestore database under User Information:
        Future uploadFile() async {
         String fileName = id;
         UploadTask uploadTask = profileProvider.uploadImageFile(avatarImageFile!, fileName);
         try {
           TaskSnapshot snapshot = await uploadTask;
           photoUrl = await snapshot.ref.getDownloadURL();
           ChatUser updateInfo = ChatUser(id: id,
               photoUrl: photoUrl,
               displayName: displayName,
               phoneNumber: phoneNumber,
               aboutMe: aboutMe);
           profileProvider.updateFirestoreData(
               FirestoreConstants.pathUserCollection, id, updateInfo.toJson())
               .then((value) async {
             await profileProvider.setPrefs(FirestoreConstants.photoUrl, photoUrl);
             setState(() {
               isLoading = false;
             });
           });
         } on FirebaseException catch (e) {
           setState(() {
             isLoading = false;
           });
           Fluttertoast.showToast(msg: e.toString());
         }
        }
      3. Upload data to the Firestore database and update the data under User Information:
        void updateFirestoreData() {
         focusNodeNickname.unfocus();
         setState(() {
           isLoading = true;
           if (dialCodeDigits != "+00" && _phoneController.text != "") {
             phoneNumber = dialCodeDigits + _phoneController.text.toString();
           }
         });
         ChatUser updateInfo = ChatUser(id: id,
             photoUrl: photoUrl,
             displayName: displayName,
             phoneNumber: phoneNumber,
             aboutMe: aboutMe);
         profileProvider.updateFirestoreData(
             FirestoreConstants.pathUserCollection, id, updateInfo.toJson())
             .then((value) async {
           await profileProvider.setPrefs(
               FirestoreConstants.displayName, displayName);
           await profileProvider.setPrefs(
               FirestoreConstants.phoneNumber, phoneNumber);
           await profileProvider.setPrefs(
             FirestoreConstants.photoUrl, photoUrl,);
           await profileProvider.setPrefs(
               FirestoreConstants.aboutMe,aboutMe );
        
           setState(() {
             isLoading = false;
           });
           Fluttertoast.showToast(msg: 'UpdateSuccess');
         }).catchError((onError) {
           Fluttertoast.showToast(msg: onError.toString());
         });
        }

Building the chat message page

Let’s talk about the chat page’s functionalities step by step to better understand how this section will work.

Step 1: ChatMessage Model Class

First, we will create a new model class for ChatMessages, consisting of four string variables: idFrom, idTo, timestamp, content, and an integer type. Then, again, similar to our ChatUser model, we will add two functions to Json consisting of a Map and a factory method that returns DocumentSnapshot from the Firestore database. That’s it for our model class:

class ChatMessages {
 String idFrom;
 String idTo;
 String timestamp;
 String content;
 int type;

 ChatMessages(
     {required this.idFrom,
     required this.idTo,
     required this.timestamp,
     required this.content,
     required this.type});

 Map<String, dynamic> toJson() {
   return {
     FirestoreConstants.idFrom: idFrom,
     FirestoreConstants.idTo: idTo,
     FirestoreConstants.timestamp: timestamp,
     FirestoreConstants.content: content,
     FirestoreConstants.type: type,
   };
 }

 factory ChatMessages.fromDocument(DocumentSnapshot documentSnapshot) {
   String idFrom = documentSnapshot.get(FirestoreConstants.idFrom);
   String idTo = documentSnapshot.get(FirestoreConstants.idTo);
   String timestamp = documentSnapshot.get(FirestoreConstants.timestamp);
   String content = documentSnapshot.get(FirestoreConstants.content);
   int type = documentSnapshot.get(FirestoreConstants.type);

   return ChatMessages(
       idFrom: idFrom,
       idTo: idTo,
       timestamp: timestamp,
       content: content,
       type: type);
 }
}

Step 2: ChatProvider Class

There are four main methods inside our ChatProvider class for sending and receiving text messages and images.

      1. To upload an image file to Firebase Storage:
        UploadTask uploadImageFile(File image, String filename) {
         Reference reference = firebaseStorage.ref().child(filename);
         UploadTask uploadTask = reference.putFile(image);
         return uploadTask;
        }
      2. To update the Firestore database information regarding user IDs who will be chatting with each other:
        Future<void> updateFirestoreData(
           String collectionPath, String docPath, Map<String, dynamic> dataUpdate) {
         return firebaseFirestore
             .collection(collectionPath)
             .doc(docPath)
             .update(dataUpdate);
        }
      3. To get a stream of chat messages from the Firestore database while users chat with each other:
        Stream<QuerySnapshot> getChatMessage(String groupChatId, int limit) {
         return firebaseFirestore
             .collection(FirestoreConstants.pathMessageCollection)
             .doc(groupChatId)
             .collection(groupChatId)
             .orderBy(FirestoreConstants.timestamp, descending: true)
             .limit(limit)
             .snapshots();
        }
      4. To send messages to other users with the help of the Firestore database and save those messages inside it:
        void sendChatMessage(String content, int type, String groupChatId,
           String currentUserId, String peerId) {
         DocumentReference documentReference = firebaseFirestore
             .collection(FirestoreConstants.pathMessageCollection)
             .doc(groupChatId)
             .collection(groupChatId)
             .doc(DateTime.now().millisecondsSinceEpoch.toString());
         ChatMessages chatMessages = ChatMessages(
             idFrom: currentUserId,
             idTo: peerId,
             timestamp: DateTime.now().millisecondsSinceEpoch.toString(),
             content: content,
             type: type);
        
         FirebaseFirestore.instance.runTransaction((transaction) async {
           transaction.set(documentReference, chatMessages.toJson());
         });
        }

Step 3: Chat page

First, we need to create two methods to check if:

      1. A chat message was sent:
        // checking if sent message
        bool isMessageSent(int index) {
         if ((index > 0 &&
                 listMessages[index - 1].get(FirestoreConstants.idFrom) !=
                     currentUserId) ||  index == 0) {
           return true;
         } else {
           return false;
         }
        }
      2. A chat message was received:
        // checking if received message
        bool isMessageReceived(int index) {
         if ((index > 0 &&
                 listMessages[index - 1].get(FirestoreConstants.idFrom) ==
                     currentUserId) ||  index == 0) {
           return true;
         } else {
           return false;
         }
        }

Second, we will create a method to send chat messages and execute our sendChatMessage function from our ChatProvider class:

void onSendMessage(String content, int type) {
 if (content.trim().isNotEmpty) {
   textEditingController.clear();
   chatProvider.sendChatMessage(
       content, type, groupChatId, currentUserId, widget.peerId);
   scrollController.animateTo(0,
       duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
 } else {
   Fluttertoast.showToast(
       msg: 'Nothing to send', backgroundColor: Colors.grey);
 }
}

Third, we need two more methods to pick an image from the device folder and send that image to the user.

      1. Pick image from device:
        Future getImage() async {
         ImagePicker imagePicker = ImagePicker();
         XFile? pickedFile;
         pickedFile = await imagePicker.pickImage(source: ImageSource.gallery);
         if (pickedFile != null) {
           imageFile = File(pickedFile.path);
           if (imageFile != null) {
             setState(() {
               isLoading = true;
             });
             uploadImageFile();
           }
         }
        }
      2. Send the image to the user with whom we are chatting, save the image to Firebase Storage, and save its URL information to the Firestore database:
        void uploadImageFile() async {
         String fileName = DateTime.now().millisecondsSinceEpoch.toString();
         UploadTask uploadTask = chatProvider.uploadImageFile(imageFile!, fileName);
         try {
           TaskSnapshot snapshot = await uploadTask;
           imageUrl = await snapshot.ref.getDownloadURL();
           setState(() {
             isLoading = false;
             onSendMessage(imageUrl, MessageType.image);
           });
         } on FirebaseException catch (e) {
           setState(() {
             isLoading = false;
           });
           Fluttertoast.showToast(msg: e.message ?? e.toString());
         }
        }

Fourth, we need to create an input field where the user will type the text message and click on the Send button to send the message. Also, an image picker button so that when the user clicks on it, the file picker from the device will open up to pick an image and send it to the user:

Widget buildMessageInput() {
 return SizedBox(
   width: double.infinity,
   height: 50,
   child: Row(
     children: [
       Container(
         margin: const EdgeInsets.only(right: Sizes.dimen_4),
         decoration: BoxDecoration(
           color: AppColors.burgundy,
           borderRadius: BorderRadius.circular(Sizes.dimen_30),
         ),
         child: IconButton(
           onPressed: getImage,
           icon: const Icon(
             Icons.camera_alt,
             size: Sizes.dimen_28,
           ),
           color: AppColors.white,
         ),
       ),
       Flexible(
           child: TextField(
         focusNode: focusNode,
         textInputAction: TextInputAction.send,
         keyboardType: TextInputType.text,
         textCapitalization: TextCapitalization.sentences,
         controller: textEditingController,
         decoration:
             kTextInputDecoration.copyWith(hintText: 'write here...'),
         onSubmitted: (value) {
           onSendMessage(textEditingController.text, MessageType.text);
         },
       )),
       Container(
         margin: const EdgeInsets.only(left: Sizes.dimen_4),
         decoration: BoxDecoration(
           color: AppColors.burgundy,
           borderRadius: BorderRadius.circular(Sizes.dimen_30),
         ),
         child: IconButton(
           onPressed: () {
             onSendMessage(textEditingController.text, MessageType.text);
           },
           icon: const Icon(Icons.send_rounded),
           color: AppColors.white,
         ),
       ),
     ],
   ),
 );
}

Fifth, we will create chat bubbles for the sent and received text messages with profile photos.

Widget buildItem(int index, DocumentSnapshot? documentSnapshot) {
 if (documentSnapshot != null) {
   ChatMessages chatMessages = ChatMessages.fromDocument(documentSnapshot);
   if (chatMessages.idFrom == currentUserId) {
     // right side (my message)
     return Column(
       crossAxisAlignment: CrossAxisAlignment.end,
       children: [
         Row(
           mainAxisAlignment: MainAxisAlignment.end,
           children: [
             chatMessages.type == MessageType.text
                 ? messageBubble(
                     chatContent: chatMessages.content,
                     color: AppColors.spaceLight,
                     textColor: AppColors.white,
                     margin: const EdgeInsets.only(right: Sizes.dimen_10),
                   )
                 : chatMessages.type == MessageType.image
                     ? Container(
                         margin: const EdgeInsets.only(
                             right: Sizes.dimen_10, top: Sizes.dimen_10),
                         child: chatImage(
                             imageSrc: chatMessages.content, onTap: () {}),
                       )
                     : const SizedBox.shrink(),
             isMessageSent(index)
                 ? Container(
                     clipBehavior: Clip.hardEdge,
                     decoration: BoxDecoration(
                       borderRadius: BorderRadius.circular(Sizes.dimen_20),
                     ),
                     child: Image.network(
                       widget.userAvatar,
                       width: Sizes.dimen_40,
                       height: Sizes.dimen_40,
                       fit: BoxFit.cover,
                       loadingBuilder: (BuildContext ctx, Widget child,
                           ImageChunkEvent? loadingProgress) {
                         if (loadingProgress == null) return child;
                         return Center(
                           child: CircularProgressIndicator(
                             color: AppColors.burgundy,
                             value: loadingProgress.expectedTotalBytes !=
                                         null &&
                                     loadingProgress.expectedTotalBytes !=
                                         null
                                 ? loadingProgress.cumulativeBytesLoaded /
                                     loadingProgress.expectedTotalBytes!
                                 : null,
                           ),
                         );
                       },
                       errorBuilder: (context, object, stackTrace) {
                         return const Icon(
                           Icons.account_circle,
                           size: 35,
                           color: AppColors.greyColor,
                         );
                       },
                     ),
                   )
                 : Container(
                     width: 35,
                   ),
           ],
         ),
         isMessageSent(index)
             ? Container(
                 margin: const EdgeInsets.only(
                     right: Sizes.dimen_50,
                     top: Sizes.dimen_6,
                     bottom: Sizes.dimen_8),
                 child: Text(
                   DateFormat('dd MMM yyyy, hh:mm a').format(
                     DateTime.fromMillisecondsSinceEpoch(
                       int.parse(chatMessages.timestamp),
                     ),
                   ),
                   style: const TextStyle(
                       color: AppColors.lightGrey,
                       fontSize: Sizes.dimen_12,
                       fontStyle: FontStyle.italic),
                 ),
               )
             : const SizedBox.shrink(),
       ],
     );
   } else {
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
         Row(
           mainAxisAlignment: MainAxisAlignment.start,
           children: [
             isMessageReceived(index)
                 // left side (received message)
                 ? Container(
                     clipBehavior: Clip.hardEdge,
                     decoration: BoxDecoration(
                       borderRadius: BorderRadius.circular(Sizes.dimen_20),
                     ),
                     child: Image.network(
                       widget.peerAvatar,
                       width: Sizes.dimen_40,
                       height: Sizes.dimen_40,
                       fit: BoxFit.cover,
                       loadingBuilder: (BuildContext ctx, Widget child,
                           ImageChunkEvent? loadingProgress) {
                         if (loadingProgress == null) return child;
                         return Center(
                           child: CircularProgressIndicator(
                             color: AppColors.burgundy,
                             value: loadingProgress.expectedTotalBytes !=
                                         null &&
                                     loadingProgress.expectedTotalBytes !=
                                         null
                                 ? loadingProgress.cumulativeBytesLoaded /
                                     loadingProgress.expectedTotalBytes!
                                 : null,
                           ),
                         );
                       },
                       errorBuilder: (context, object, stackTrace) {
                         return const Icon(
                           Icons.account_circle,
                           size: 35,
                           color: AppColors.greyColor,
                         );
                       },
                     ),
                   )
                 : Container(
                     width: 35,
                   ),
             chatMessages.type == MessageType.text
                 ? messageBubble(
                     color: AppColors.burgundy,
                     textColor: AppColors.white,
                     chatContent: chatMessages.content,
                     margin: const EdgeInsets.only(left: Sizes.dimen_10),
                   )
                 : chatMessages.type == MessageType.image
                     ? Container(
                         margin: const EdgeInsets.only(
                             left: Sizes.dimen_10, top: Sizes.dimen_10),
                         child: chatImage(
                             imageSrc: chatMessages.content, onTap: () {}),
                       )
                     : const SizedBox.shrink(),
           ],
         ),
         isMessageReceived(index)
             ? Container(
                 margin: const EdgeInsets.only(
                     left: Sizes.dimen_50,
                     top: Sizes.dimen_6,
                     bottom: Sizes.dimen_8),
                 child: Text(
                   DateFormat('dd MMM yyyy, hh:mm a').format(
                     DateTime.fromMillisecondsSinceEpoch(
                       int.parse(chatMessages.timestamp),
                     ),
                   ),
                   style: const TextStyle(
                       color: AppColors.lightGrey,
                       fontSize: Sizes.dimen_12,
                       fontStyle: FontStyle.italic),
                 ),
               )
             : const SizedBox.shrink(),
       ],
     );
   }
 } else {
   return const SizedBox.shrink();
 }
}

Sixth, we will create a view where all the text messages and images will be shown separately for sender and receiver.

Widget buildListMessage() {
   return Flexible(
     child: groupChatId.isNotEmpty
         ? StreamBuilder<QuerySnapshot>(
             stream: chatProvider.getChatMessage(groupChatId, _limit),
             builder: (BuildContext context,
                 AsyncSnapshot<QuerySnapshot> snapshot) {
               if (snapshot.hasData) {
                 listMessages = snapshot.data!.docs;
                 if (listMessages.isNotEmpty) {
                   return ListView.builder(
                       padding: const EdgeInsets.all(10),
                       itemCount: snapshot.data?.docs.length,
                       reverse: true,
                       controller: scrollController,
                       itemBuilder: (context, index) =>
                           buildItem(index, snapshot.data?.docs[index]));
                 } else {
                   return const Center(
                     child: Text('No messages...'),
                   );
                 }
               } else {
                 return const Center(
                   child: CircularProgressIndicator(
                     color: AppColors.burgundy,
                   ),
                 );
               }
             })
         : const Center(
             child: CircularProgressIndicator(
               color: AppColors.burgundy,
             ),
           ),
   );
 }
}

Finished Chat Application

We have finished creating our chat application in Flutter with Firebase at its backend. There are still a lot of other Dart files and code involved in programming this application that I haven’t posted here in this article, but I have linked each page with GitHub links to view the complete code.

Deploy the app as a PWA

Now, let us configure our completed application for uploading to Firebase Hosting so that everyone can experience it without the need to install it on any device.

Step 1: Creating OAuth 2.0 credentials

Please go through this documentation to create authorization credentials. Any application that uses OAuth 2.0 to access Google APIs must have authorization credentials to identify the application to the Google server.

After running this command, flutterfire configure, Google will configure OAuth 2.0 credentials for Android, iOS, and web. You can go to this credentials page, and you should see something like the image given below.

Next, copy the Client ID for the web application and save it somewhere before going to the next step.

Creating OAuth 2.0 Credentials

Step 2: Google Platform library

A web folder is created by default in your project structure when creating a new Flutter application. Open the index.html file and add this script to load the Google Platform library inside the web folder.

<script src="https://apis.google.com/js/platform.js" async defer></script>

Please look at the image for further clarity.

Configuring a Flutter and Firebase app with Google Platform Library

Step 3: Specify client ID

In the same file, index.html, add the Client ID you just copied from the credentials page.

<meta name="google-signin-client_id" content="YOUR_CLIENT_ID.apps.googleusercontent.com">

Specify Client ID

Step 4: Initialize Firebase Hosting

Since we had already configured Firebase CLI commands when we initialized our project and were authenticated into Firebase, we can run this command directly in the Android Studio terminal from the root of our project.

$ Firebase init hosting

This command will ask you to select your Firebase project when you run it. Use arrow keys to select it.

      • Next, the Firebase CLI will ask you to enter the name of your public directory. Enter build/web
      • Then, it will ask you if you want to configure it as a single-page app. Enter y (for yes)
      • Last, it will ask if you want to configure GitHub integrations, configure the automatic build, and deploy it to Firebase Hosting. Of course, you can always do it later, but for now, enter N (for no)

You will see two files created in your project, known as firebase.json and .firebaserc.

Step 5: Add Files to the build/web folder

Run this command in your terminal window from the root of your project structure. It will add all the necessary files to the build/web inside the root directory:

$ flutter build web

Step 6: Upload all files to Firebase Hosting

Finally, run this command to upload all the files to Firebase Hosting, and it will generate a web URL for anyone to access:

$ Firebase deploy

Upload Files Firebase Hosting

Conclusion

That is it! We are all done. Hope you enjoyed reading this as much as I did writing. I learned a lot when I created this application for the very first time. With FlutterFire and Dart CLI, things have become much easier to configure Flutter applications with Firebase services.

You can read this handy blog if you’d like to learn more about configuring the app as a PWA. Here is a link to the PWA for this demo project, and a link to the whole project on GitHub for you to experiment.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Murtaza Sulaihi I am a school professor and I also develop Android applications and Flutter applications.

4 Replies to “How to build a chat application in Flutter with…”

    1. It can be implemented, Google has extensive documentation on it which can referred to for its implementation.

Leave a Reply