Flutter Chat App With Firebase

0

Building a chat app is one of the best ways to learn real-world app development in Flutter. In this blog, we’ll walk through how to build a Flutter Chat App with Firebase that supports real-time messaging, friend requests, video/audio calls, and many more.

Key Features of the Chat App

Here’s what our chat app will cover:
✅ Real-Time Messaging
✅ Online & Typing Indicators
✅ Last Seen & Read/Delivered Receipts
✅ Friend Requests (Accept/Reject before chat)
✅ Unfriend/Block option
✅ Unread Count
✅ Send Emojis, Images, and Files
✅ Video & Audio Calls (via ZegoCloud)
✅ Change User Profile
✅ Display/Search All Users
✅ Google + Email/Password Authentication
✅ Online/Offline Status
✅ Light/Dark Theme
✅ Auto-scroll to bottom on new messages
✅ Responsive for both Android & iOS
✅ Proper error handling & smooth real-time refresh

We built this app using Flutter as the framework and Firebase for the backend and database. All features are integrated in Flutter, while ZegoCloud powers video and audio calls. Now, let’s get started.

Let’s get started by adding the package that will be required in our project.

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.8
  firebase_core: ^4.0.0
  firebase_auth: ^6.0.1
  flutter_riverpod: ^2.6.1
  google_sign_in: ^7.1.1
  cloud_firestore: ^6.0.0
  cherry_toast: ^1.13.0
  intl: ^0.20.2
  uuid: ^4.5.1
  firebase_storage: ^13.0.1
  image_picker: ^1.2.0
  zego_uikit_prebuilt_call: ^4.18.2
  permission_handler: ^12.0.1
  zego_uikit_signaling_plugin: ^2.8.16
  zego_uikit: ^2.28.26

Run flutter pub get to install these packages.

Project Setup

Before writing any code, create a new Flutter project or continue with an existing project with the latest update of Flutter, and integrate Firebase with the Flutter project.

If you face any problem during Firebase setup, then watch this tutorial.

After the Firebase setup is completed, we continue with the project, starting with authentication.

Firebase Email Password Authentication Flutter

Implement functions to allow users to create accounts and sign in using email and password:(auth_model.dart)

class AuthFormState {
  final String name;
  final String email;
  final String password;
  final String? nameError;
  final String? emailError;
  final String? passwordError;
  final bool isLoading;
  final bool isPasswordHidden;

  AuthFormState({
    this.name = '',
    this.email = '',
    this.password = '',
    this.nameError,
    this.emailError,
    this.passwordError,
    this.isLoading = false,
    this.isPasswordHidden = true,
  });

  bool get isFormValid =>
      emailError == null &&
      passwordError == null &&
      (name.isEmpty || nameError == null);

  AuthFormState copyWith({
    String? name,
    String? email,
    String? password,
    String? nameError,
    String? emailError,
    String? passwordError,
    bool? isLoading,
    bool? isPasswordHidden,
  }) {
    return AuthFormState(
      name: name ?? this.name,
      email: email ?? this.email,
      password: password ?? this.password,
      nameError: nameError,
      emailError: emailError,
      passwordError: passwordError,
      isLoading: isLoading ?? this.isLoading,
      isPasswordHidden: isPasswordHidden ?? this.isPasswordHidden,
    );
  }
}

auth_service.dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class AuthMethod {
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  final FirebaseAuth _auth = FirebaseAuth.instance;

  Future<String> signUpUser({
    required String email,
    required String password,
    required String name,
  }) async {
    try {
      if (email.isEmpty || password.isEmpty || name.isEmpty) {
        return "Please enter all fields";
      }

      UserCredential cred = await _auth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );

      // Update user profile
      await cred.user!.updateDisplayName(name);
      // FIXED: Store user data with consistent field names
      await _firestore.collection("users").doc(cred.user!.uid).set({
        "uid": cred.user!.uid,
        "name": name,
        "email": email,
        "photoURL": null,
        "isOnline": false, // Set to true when user signs up
        "provider": "email",
        'lastSeen': FieldValue.serverTimestamp(),
        "createdAt": FieldValue.serverTimestamp(),
      });
      return "success";
    } catch (e) {
      return e.toString();
    }
  }

  // Login with online status update
  Future<String> loginUser({
    required String email,
    required String password,
  }) async {
    try {
      if (email.isEmpty || password.isEmpty) {
        return "Please enter all fields";
      }
      await _auth.signInWithEmailAndPassword(email: email, password: password);
      // Update online status after login
      if (_auth.currentUser != null) {
        await _firestore.collection('users').doc(_auth.currentUser!.uid).update(
          {'isOnline': true, 'lastSeen': FieldValue.serverTimestamp()},
        );
      }
      return "success";
    } catch (e) {
      return e.toString();
    }
  }

  //Logout with online status update
  Future<void> signOut() async {
    if (_auth.currentUser != null) {
      // Set offline before signing out
      await _firestore.collection('users').doc(_auth.currentUser!.uid).update({
        'isOnline': false,
        'lastSeen': FieldValue.serverTimestamp(),
      });
    }
    await _auth.signOut();
  }
}

final authMethodProvider = Provider<AuthMethod>((ref) {
  return AuthMethod();
});

auth_providder.dart

import 'package:flutter_firebase_chat_app/auth/model/auth_model.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class AuthFormNotifier extends StateNotifier<AuthFormState> {
  AuthFormNotifier() : super(AuthFormState());

  void togglePasswordVisibility() {
    state = state.copyWith(isPasswordHidden: !state.isPasswordHidden);
  }

  void updateName(String name) {
    String? nameError;
    if (name.isNotEmpty && name.length < 6) {
      nameError = "Provide your full name";
    }
    state = state.copyWith(name: name, nameError: nameError);
  }

  void updateEmail(String email) {
    String? emailError;
    if (email.isNotEmpty &&
        !RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email)) {
      emailError = 'Enter a valid email';
    }
    state = state.copyWith(email: email, emailError: emailError);
  }

  void updatePassword(String password) {
    String? passwordError;
    if (password.isNotEmpty && password.length < 6) {
      passwordError = 'Password must be at least 6 characters';
    }
    state = state.copyWith(password: password, passwordError: passwordError);
  }

  void setLoading(bool isLoading) {
    state = state.copyWith(isLoading: isLoading);
  }
}

final authFormProvider = StateNotifierProvider<AuthFormNotifier, AuthFormState>(
  (ref) {
    return AuthFormNotifier();
  },
);

After creating an authentication service and a riverpod provider. Now we will implement it in the signup and login screens

signup_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_firebase_chat_app/auth/screen/user_login_screen.dart';
import 'package:flutter_firebase_chat_app/auth/service/auth_provider.dart';
import 'package:flutter_firebase_chat_app/auth/service/auth_service.dart';
import 'package:flutter_firebase_chat_app/core/utils/utils.dart';
import 'package:flutter_firebase_chat_app/my_button.dart';
import 'package:flutter_firebase_chat_app/route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class SignupScreen extends ConsumerWidget {
  const SignupScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final formState = ref.watch(authFormProvider);
    final formNotifer = ref.read(authFormProvider.notifier);
    final authMethod = ref.read(authMethodProvider);
    void sigup() async {
      formNotifer.setLoading(true);
      final res = await authMethod.signUpUser(
        email: formState.email,
        password: formState.password,
        name: formState.name,
      );
      formNotifer.setLoading(false);
      if (res == "success" && context.mounted) {
        NavigationHelper.pushReplacement(context, UserLoginScreen());
        showAppSnackbar(
          context: context,
          type: SnackbarType.success,
          description: "Sinup Up Successful. Now turn to login",
        );
      } else {
        if (context.mounted) {
          showAppSnackbar(
            context: context,
            type: SnackbarType.error,
            description: res,
          );
        }
      }
    }

    double height = MediaQuery.of(context).size.height;
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: ListView(
          children: [
            Container(
              height: height / 2.4,
              width: double.maxFinite,
              decoration: BoxDecoration(),
              child: Image.asset("assets/77881.jpg", fit: BoxFit.cover),
            ),
            SizedBox(height: 20),
            Padding(
              padding: EdgeInsets.all(15),
              child: Column(
                children: [
                  TextField(
                    autocorrect: false,
                    onChanged: (value) => formNotifer.updateName(value),
                    keyboardType: TextInputType.text,
                    decoration: InputDecoration(
                      prefixIcon: Icon(Icons.person),
                      labelText: "Enter your name",
                      border: OutlineInputBorder(),
                      contentPadding: EdgeInsets.all(15),
                      errorText: formState.nameError,
                    ),
                  ),
                  SizedBox(height: 15),
                  TextField(
                    autocorrect: false,
                    onChanged: (value) => formNotifer.updateEmail(value),
                    keyboardType: TextInputType.emailAddress,
                    decoration: InputDecoration(
                      prefixIcon: Icon(Icons.email),
                      labelText: "Enter your email",
                      border: OutlineInputBorder(),
                      contentPadding: EdgeInsets.all(15),
                      errorText: formState.emailError,
                    ),
                  ),
                  SizedBox(height: 15),
                  TextField(
                    autocorrect: false,
                    onChanged: (value) => formNotifer.updatePassword(value),
                    keyboardType: TextInputType.visiblePassword,
                    obscureText: formState.isPasswordHidden,
                    decoration: InputDecoration(
                      prefixIcon: Icon(Icons.lock),
                      labelText: "Enter your password",
                      border: OutlineInputBorder(),
                      contentPadding: EdgeInsets.all(15),
                      errorText: formState.passwordError,
                      suffixIcon: IconButton(
                        onPressed: () => formNotifer.togglePasswordVisibility(),
                        icon: Icon(
                          formState.isPasswordHidden
                              ? Icons.visibility_off
                              : Icons.visibility,
                        ),
                      ),
                    ),
                  ),
                  SizedBox(height: 20),
                  formState.isLoading
                      ? Center(child: CircularProgressIndicator())
                      : MyButton(
                          onTab: formState.isFormValid ? sigup : null,
                          buttonText: "Sign Up",
                        ),
                  SizedBox(height: 20),
                  Row(
                    children: [
                      Spacer(),
                      Text("Already have an account?"),
                      GestureDetector(
                        onTap: () {
                          NavigationHelper.push(context, UserLoginScreen());
                        },
                        child: Text(
                          "Login",
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

login_screen.dart

// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:flutter_firebase_chat_app/auth/screen/google_login_screen.dart';
import 'package:flutter_firebase_chat_app/auth/screen/signup_screen.dart';
import 'package:flutter_firebase_chat_app/auth/service/auth_provider.dart';
import 'package:flutter_firebase_chat_app/auth/service/auth_service.dart';
import 'package:flutter_firebase_chat_app/chat/screens/app_home_screen.dart';
import 'package:flutter_firebase_chat_app/core/utils/utils.dart';
import 'package:flutter_firebase_chat_app/my_button.dart';
import 'package:flutter_firebase_chat_app/route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class UserLoginScreen extends ConsumerWidget {
  const UserLoginScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    double height = MediaQuery.of(context).size.height;
    final formState = ref.watch(authFormProvider);
    final formNotifer = ref.read(authFormProvider.notifier);
    final authMethod = ref.read(authMethodProvider);
    void login() async {
      formNotifer.setLoading(true);
      final res = await authMethod.loginUser(
        email: formState.email,
        password: formState.password,
      );
      formNotifer.setLoading(false);
      if (res == "success") {
        NavigationHelper.pushReplacement(context, MainHomeScreen());
        // mySnackBar(message: "Successful Login.", context: context);
        showAppSnackbar(
          context: context,
          type: SnackbarType.success,
          description: "Successful Login",
        );
      } else {
           showAppSnackbar(
          context: context,
          type: SnackbarType.error,
          description: res,
        );
      }
    }

    return Scaffold(
      backgroundColor: Colors.white,
      body: SingleChildScrollView(
        child: Column(
          children: [
            SizedBox(
              height: height / 2.1,
              width: double.maxFinite,
              child: Image.asset("assets/2752392.jpg", fit: BoxFit.cover),
            ),
            Padding(
              padding: EdgeInsets.all(15),
              child: Column(
                children: [
                  TextField(
                    autocorrect: false,
                    onChanged: (value) => formNotifer.updateEmail(value),
                    keyboardType: TextInputType.emailAddress,
                    decoration: InputDecoration(
                      prefixIcon: Icon(Icons.email),
                      labelText: "Enter your email",
                      border: OutlineInputBorder(),
                      contentPadding: EdgeInsets.all(15),
                      errorText: formState.emailError,
                    ),
                  ),
                  SizedBox(height: 15),
                  TextField(
                    autocorrect: false,
                    onChanged: (value) => formNotifer.updatePassword(value),
                    keyboardType: TextInputType.visiblePassword,
                    obscureText: formState.isPasswordHidden,
                    decoration: InputDecoration(
                      prefixIcon: Icon(Icons.lock),
                      labelText: "Enter your password",
                      border: OutlineInputBorder(),
                      contentPadding: EdgeInsets.all(15),
                      errorText: formState.passwordError,
                      suffixIcon: IconButton(
                        onPressed: () => formNotifer.togglePasswordVisibility(),
                        icon: Icon(
                          formState.isPasswordHidden
                              ? Icons.visibility_off
                              : Icons.visibility,
                        ),
                      ),
                    ),
                  ),
                  SizedBox(height: 20),
                  formState.isLoading
                      ? Center(child: CircularProgressIndicator())
                      : MyButton(
                          onTab: formState.isFormValid ? login : null,

                          buttonText: "Login",
                        ),
                  SizedBox(height: 20),
                  Row(
                    children: [
                      Expanded(
                        child: Container(height: 1, color: Colors.black26),
                      ),
                      Text(" or "),
                      Expanded(
                        child: Container(height: 1, color: Colors.black26),
                      ),
                    ],
                  ),
                  SizedBox(height: 15),
                  // for google auth
                  GoogleLoginScreen(),
                  SizedBox(height: 15),

                  Row(
                    children: [
                      Spacer(),
                      Text("Don't have an account? "),
                      GestureDetector(
                        onTap: () {
                          NavigationHelper.push(context, SignupScreen());
                        },
                        child: Text(
                          "SignUp",
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Finally, Email-password authentication is now complete. I’ve provided you with the full code for this feature. If you face any issues, you can also watch the separate tutorial video I created on email-password authentication.

Next, we’ve also added Google login as an authentication method in our project. To learn the complete process with the latest updates(7.1.1), you can check out the separate blog article and YouTube video on my channel and website, where I’ve also shared the source code.

ZEGOCLOUD Setup in Flutter

After completing authentication, we’ll continue with the remaining features of our Flutter chat app with Firebase. One of the main features is the ability to make audio and video calls. If you’re a beginner, setting up ZegoCloud for Quick Start (with call invitation) can be challenging. To make it easier, I’ll guide you through a step-by-step process to set up the ZegoCloud call invitation feature in a Flutter app.

  • For Android: Add ZegoUIKitPrebuiltCall as dependencies
    Run the following code in your project root directory :

    flutter pub add zego_uikit_prebuilt_call 
  • Import the SDK: Now, in your Dart code, import the prebuilt Call Kit SDK.
    import 'package:zego_uiki/zego_uiki.dart';
    import 'package:zego_uikit_prebuilt_call/zego_uikit_prebuilt_call.dart';
  •   Enter your project’s root directory, open the settings.gradle file to add the jitpack to dependencyResolutionManagement > repositories like this:
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
        maven { url = uri("https://maven.zego.im") }   // ✅ add Kotlin DSL
        maven { url = uri("https://www.jitpack.io") }  // ✅ add Kotlin DSL
    }
// add this two line in your settings.gradle.kts
  • Modify your app-level build.gradle file by adding this: 
    dependencies {
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version")
    }
  • change the minSdkVersion and compileSdkVersion
    minSdkVersion at least 26 and compileSdkVersion 36 .
  • Open the file your_project/app/src/main/AndroidManifest.xml, and add the following code:
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.CAMERA" />
    
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.VIBRATE"/>
    
    <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY"/>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <!--for bring app to foreground from background in Android 10 -->
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
  • In your project’s ‘your_project > android > app’ folder, create a ‘proguard-rules.pro’ file with the following content as shown below
    -keep class **.zego.** { *; }
  • Add the following config code to the ‘release’ part of the ‘your_project/android/app/build.gradle’ file
    proguardFiles(
                   getDefaultProguardFile("proguard-android.txt"),
                   file("proguard-rules.pro")
               )
  •  Add the latest Kotlin version in gradle.properties
    kotlin_version=2.1.0

     

  • For IOS: Open the ‘your_project/ios/Podfile’ file, and add the following to the ‘post_install do |installer|’ part
    # Start of the permission_handler configuration
    target.build_configurations.each do |config|
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',
        'PERMISSION_CAMERA=1',
        'PERMISSION_MICROPHONE=1',
      ]
    end
    # End of the permission_handler configuration
  • Open the ‘your_project/ios/Runner/Info.plist’ file, and add the following to the ‘dict’ part
    <key>NSCameraUsageDescription</key>
    <string>We require camera access to connect</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>We require microphone access to connect</string>

At the same time, I’ll provide a common toast message that we’ll use throughout the app — one for errors and another for success. For this, I’ve used the cherry_toast package.

import 'package:cherry_toast/cherry_toast.dart';
import 'package:cherry_toast/resources/arrays.dart';
import 'package:flutter/material.dart';

enum SnackbarType { success, error }

void showAppSnackbar({
  required BuildContext context,
  required SnackbarType type,
  required String description,
}) {
  switch (type) {
    case SnackbarType.success:
      CherryToast.success(
        toastDuration: Duration(milliseconds: 2500),
        height: 70,
        toastPosition: Position.top,
        shadowColor: Colors.white,

        animationType: AnimationType.fromTop,
        displayCloseButton: false,
        backgroundColor: Colors.green.withAlpha(40),
        description: Text(
          description,
          style: const TextStyle(color: Colors.green),
        ),
        title: const Text(
          "Successful",
          style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold),
        ),
      ).show(context);
      break;

    case SnackbarType.error:
      CherryToast.error(
        toastDuration: Duration(milliseconds: 2500),
        height: 70,
        toastPosition: Position.top,
        shadowColor: Colors.white,
        animationType: AnimationType.fromTop,
        displayCloseButton: false,
        backgroundColor: Colors.red.withAlpha(40),
        description: Text(
          description,
          style: const TextStyle(color: Colors.red),
        ),
        title: const Text(
          "Fail",
          style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
        ),
      ).show(context);
      break;
  }
}

Finally, we have completed all the setup and starter code to begin the project. For the full step-by-step process, you can follow our YouTube video.

Previous articleDelivery Boy App Using Flutter Google Map and Provider

LEAVE A REPLY

Please enter your comment!
Please enter your name here