Chat App In Flutter Using Google Firebase

Introduction

 
In this article, we will learn how to create a chat app in Flutter using Google Firebase as backend. This article consists of a number of articles in which you will learn -
  1. OTP Authentication in Flutter
  2. Chat App Data Structure In Firebase Firestore
  3. Pagination In Flutter Using Firebase Cloud Firestore
  4. Upload Image File To Firebase Storage Using Flutter
I have divided the chat app series into multiple articles. You will learn a lot of stuff regarding Flutter in this Flutter chat app series. So, let’s begin our app.
 
Output
Chat App In Flutter Using Google Firebase
 
Plugin Required
  • firebase_auth: // for firebase otp authentication
  • shared_preferences: ^0.5.3+1 // for storing user credentials persistence
  • cloud_firestore: ^0.12.7 // to access firebase real time database
  • contact_picker: ^0.0.2 // to add friends from contact list
  • image_picker: ^0.6.0+17 // to select image from device
  • firebase_storage: ^3.0.3 // to send image to user for that we need to store image on server
  • photo_view: ^0.4.2 // to view sent and received image in expanded view

Programming Steps

 
Step 1
 
The first and most basic step is to create a new application in Flutter. If you are a beginner in Flutter, you can check my blog Create a first app in Flutter. I have created an app named as “flutter_chat_app”.
 
Step 2
 
Open the pubspec.yaml file in your project and add the following dependencies into it.
  1. dependencies:  
  2.  flutter:  
  3.    sdk: flutter  
  4.  cupertino_icons: ^0.1.2  
  5.  firebase_auth:  
  6.  shared_preferences: ^0.5.3+1  
  7.  cloud_firestore: ^0.12.7  
  8.  contact_picker: ^0.0.2  
  9.  image_picker: ^0.6.0+17  
  10.  firebase_storage: ^3.0.3  
  11.  photo_view: ^0.4.2  
Step 3
 
Now, we need to setup firebase project to provide authentication and storage feature. I have placed important implementation below but you can study full article OTP Authentication in Flutter , Chat App Data Structure In Firebase Firestore. Following is the OTP Authentication (registration_page.dart) Programming Implementation. 
  1. import 'package:flutter/material.dart';  
  2. import 'package:firebase_auth/firebase_auth.dart';  
  3. import 'package:flutter/services.dart';  
  4. import 'package:flutter_chat_app/pages/home_page.dart';  
  5. import 'package:shared_preferences/shared_preferences.dart';  
  6. import 'package:cloud_firestore/cloud_firestore.dart';  
  7.    
  8. class RegistrationPage extends StatefulWidget {  
  9.  final SharedPreferences prefs;  
  10.  RegistrationPage({this.prefs});  
  11.  @override  
  12.  _RegistrationPageState createState() => _RegistrationPageState();  
  13. }  
  14.    
  15. class _RegistrationPageState extends State<RegistrationPage> {  
  16.  String phoneNo;  
  17.  String smsOTP;  
  18.  String verificationId;  
  19.  String errorMessage = '';  
  20.  FirebaseAuth _auth = FirebaseAuth.instance;  
  21.  final db = Firestore.instance;  
  22.    
  23.  @override  
  24.  initState() {  
  25.    super.initState();  
  26.  }  
  27.    
  28.  Future<void> verifyPhone() async {  
  29.    final PhoneCodeSent smsOTPSent = (String verId, [int forceCodeResend]) {  
  30.      this.verificationId = verId;  
  31.      smsOTPDialog(context).then((value) {});  
  32.    };  
  33.    try {  
  34.      await _auth.verifyPhoneNumber(  
  35.          phoneNumber: this.phoneNo, // PHONE NUMBER TO SEND OTP  
  36.          codeAutoRetrievalTimeout: (String verId) {  
  37.            //Starts the phone number verification process for the given phone number.  
  38.            //Either sends an SMS with a 6 digit code to the phone number specified, or sign's the user in and [verificationCompleted] is called.  
  39.            this.verificationId = verId;  
  40.          },  
  41.          codeSent:  
  42.              smsOTPSent, // WHEN CODE SENT THEN WE OPEN DIALOG TO ENTER OTP.  
  43.          timeout: const Duration(seconds: 20),  
  44.          verificationCompleted: (AuthCredential phoneAuthCredential) {  
  45.            print(phoneAuthCredential);  
  46.          },  
  47.          verificationFailed: (AuthException e) {  
  48.            print('${e.message}');  
  49.          });  
  50.    } catch (e) {  
  51.      handleError(e);  
  52.    }  
  53.  }  
  54.    
  55.  Future<bool> smsOTPDialog(BuildContext context) {  
  56.    return showDialog(  
  57.        context: context,  
  58.        barrierDismissible: false,  
  59.        builder: (BuildContext context) {  
  60.          return new AlertDialog(  
  61.            title: Text('Enter SMS Code'),  
  62.            content: Container(  
  63.              height: 85,  
  64.              child: Column(children: [  
  65.                TextField(  
  66.                  onChanged: (value) {  
  67.                    this.smsOTP = value;  
  68.                  },  
  69.                ),  
  70.                (errorMessage != ''  
  71.                    ? Text(  
  72.                        errorMessage,  
  73.                        style: TextStyle(color: Colors.red),  
  74.                      )  
  75.                    : Container())  
  76.              ]),  
  77.            ),  
  78.            contentPadding: EdgeInsets.all(10),  
  79.            actions: <Widget>[  
  80.              FlatButton(  
  81.                child: Text('Done'),  
  82.                onPressed: () {  
  83.                  _auth.currentUser().then((user) async {  
  84.                    signIn();  
  85.                  });  
  86.                },  
  87.              )  
  88.            ],  
  89.          );  
  90.        });  
  91.  }  
  92.    
  93.  signIn() async {  
  94.    try {  
  95.      final AuthCredential credential = PhoneAuthProvider.getCredential(  
  96.        verificationId: verificationId,  
  97.        smsCode: smsOTP,  
  98.      );  
  99.      final FirebaseUser user = await _auth.signInWithCredential(credential);  
  100.      final FirebaseUser currentUser = await _auth.currentUser();  
  101.      assert(user.uid == currentUser.uid);  
  102.      Navigator.of(context).pop();  
  103.      DocumentReference mobileRef = db  
  104.          .collection("mobiles")  
  105.          .document(phoneNo.replaceAll(new RegExp(r'[^\w\s]+'), ''));  
  106.      await mobileRef.get().then((documentReference) {  
  107.        if (!documentReference.exists) {  
  108.          mobileRef.setData({}).then((documentReference) async {  
  109.            await db.collection("users").add({  
  110.              'name'"No Name",  
  111.              'mobile': phoneNo.replaceAll(new RegExp(r'[^\w\s]+'), ''),  
  112.              'profile_photo'"",  
  113.            }).then((documentReference) {  
  114.              widget.prefs.setBool('is_verified'true);  
  115.              widget.prefs.setString(  
  116.                'mobile',  
  117.                phoneNo.replaceAll(new RegExp(r'[^\w\s]+'), ''),  
  118.              );  
  119.              widget.prefs.setString('uid', documentReference.documentID);  
  120.              widget.prefs.setString('name'"No Name");  
  121.              widget.prefs.setString('profile_photo'"");  
  122.    
  123.              mobileRef.setData({'uid': documentReference.documentID}).then(  
  124.                  (documentReference) async {  
  125.                Navigator.of(context).pushReplacement(MaterialPageRoute(  
  126.                    builder: (context) => HomePage(prefs: widget.prefs)));  
  127.              }).catchError((e) {  
  128.                print(e);  
  129.              });  
  130.            }).catchError((e) {  
  131.              print(e);  
  132.            });  
  133.          });  
  134.        } else {  
  135.          widget.prefs.setBool('is_verified'true);  
  136.          widget.prefs.setString(  
  137.            'mobile_number',  
  138.            phoneNo.replaceAll(new RegExp(r'[^\w\s]+'), ''),  
  139.          );  
  140.          widget.prefs.setString('uid', documentReference["uid"]);  
  141.          widget.prefs.setString('name', documentReference["name"]);  
  142.          widget.prefs  
  143.              .setString('profile_photo', documentReference["profile_photo"]);  
  144.    
  145.          Navigator.of(context).pushReplacement(  
  146.            MaterialPageRoute(  
  147.              builder: (context) => HomePage(prefs: widget.prefs),  
  148.            ),  
  149.          );  
  150.        }  
  151.      }).catchError((e) {});  
  152.    } catch (e) {  
  153.      handleError(e);  
  154.    }  
  155.  }  
  156.    
  157.  handleError(PlatformException error) {  
  158.    switch (error.code) {  
  159.      case 'ERROR_INVALID_VERIFICATION_CODE':  
  160.        FocusScope.of(context).requestFocus(new FocusNode());  
  161.        setState(() {  
  162.          errorMessage = 'Invalid Code';  
  163.        });  
  164.        Navigator.of(context).pop();  
  165.        smsOTPDialog(context).then((value) {});  
  166.        break;  
  167.      default:  
  168.        setState(() {  
  169.          errorMessage = error.message;  
  170.        });  
  171.    
  172.        break;  
  173.    }  
  174.  }  
  175.    
  176.  @override  
  177.  Widget build(BuildContext context) {  
  178.    return Scaffold(  
  179.      body: Center(  
  180.        child: Column(  
  181.          mainAxisAlignment: MainAxisAlignment.center,  
  182.          children: <Widget>[  
  183.            Padding(  
  184.              padding: EdgeInsets.all(10),  
  185.              child: TextField(  
  186.                decoration: InputDecoration(hintText: '+910000000000'),  
  187.                onChanged: (value) {  
  188.                  this.phoneNo = value;  
  189.                },  
  190.              ),  
  191.            ),  
  192.            (errorMessage != ''  
  193.                ? Text(  
  194.                    errorMessage,  
  195.                    style: TextStyle(color: Colors.red),  
  196.                  )  
  197.                : Container()),  
  198.            SizedBox(  
  199.              height: 10,  
  200.            ),  
  201.            RaisedButton(  
  202.              onPressed: () {  
  203.                verifyPhone();  
  204.              },  
  205.              child: Text('Verify'),  
  206.              textColor: Colors.white,  
  207.              elevation: 7,  
  208.              color: Colors.blue,  
  209.            )  
  210.          ],  
  211.        ),  
  212.      ),  
  213.    );  
  214.  }  
  215. }  

Step 4

Now, we will implement the add friend from contact list. Following is the programming implementation for access contacts from the device and adding them as a friend to chat.
  1. openContacts() async {  
  2.   Contact contact = await _contactPicker.selectContact();  
  3.   if (contact != null) {  
  4.     String phoneNumber = contact.phoneNumber.number  
  5.         .toString()  
  6.         .replaceAll(new RegExp(r"\s\b|\b\s"), "")  
  7.         .replaceAll(new RegExp(r'[^\w\s]+'), '');  
  8.     if (phoneNumber.length == 10) {  
  9.       phoneNumber = '+91$phoneNumber';  
  10.     }  
  11.     if (phoneNumber.length == 12) {  
  12.       phoneNumber = '+$phoneNumber';  
  13.     }  
  14.     if (phoneNumber.length == 13) {  
  15.       DocumentReference mobileRef = db  
  16.           .collection("mobiles")  
  17.           .document(phoneNumber.replaceAll(new RegExp(r'[^\w\s]+'), ''));  
  18.       await mobileRef.get().then((documentReference) {  
  19.         if (documentReference.exists) {  
  20.           contactsReference.add({  
  21.             'uid': documentReference['uid'],  
  22.             'name': contact.fullName,  
  23.             'mobile': phoneNumber.replaceAll(new RegExp(r'[^\w\s]+'), ''),  
  24.           });  
  25.         } else {  
  26.           print('User Not Registered');  
  27.         }  
  28.       }).catchError((e) {});  
  29.     } else {  
  30.       print('Wrong Mobile Number');  
  31.     }  
  32.   }  
  33. }  
Step 5
 
Now, we will implement chat screen in which user will send text and image message to friend and vice versa. Following is the programming implementation for that. chat_page.dart. Pagination and image upload both are covered in this page. For full article reference please see Pagination In Flutter Using Firebase Cloud Firestore, Upload Image File To Firebase Storage Using Flutter.
  1. import 'package:cloud_firestore/cloud_firestore.dart';  
  2. import 'package:firebase_storage/firebase_storage.dart';  
  3. import 'package:flutter/material.dart';  
  4. import 'package:flutter_chat_app/pages/gallary_page.dart';  
  5. import 'package:image_picker/image_picker.dart';  
  6. import 'package:shared_preferences/shared_preferences.dart';  
  7.    
  8. class ChatPage extends StatefulWidget {  
  9.  final SharedPreferences prefs;  
  10.  final String chatId;  
  11.  final String title;  
  12.  ChatPage({this.prefs, this.chatId,this.title});  
  13.  @override  
  14.  ChatPageState createState() {  
  15.    return new ChatPageState();  
  16.  }  
  17. }  
  18.    
  19. class ChatPageState extends State<ChatPage> {  
  20.  final db = Firestore.instance;  
  21.  CollectionReference chatReference;  
  22.  final TextEditingController _textController =  
  23.      new TextEditingController();  
  24.  bool _isWritting = false;  
  25.    
  26.  @override  
  27.  void initState() {  
  28.    super.initState();  
  29.    chatReference =  
  30.        db.collection("chats").document(widget.chatId).collection('messages');  
  31.  }  
  32.    
  33.  List<Widget> generateSenderLayout(DocumentSnapshot documentSnapshot) {  
  34.    return <Widget>[  
  35.      new Expanded(  
  36.        child: new Column(  
  37.          crossAxisAlignment: CrossAxisAlignment.end,  
  38.          children: <Widget>[  
  39.            new Text(documentSnapshot.data['sender_name'],  
  40.                style: new TextStyle(  
  41.                    fontSize: 14.0,  
  42.                    color: Colors.black,  
  43.                    fontWeight: FontWeight.bold)),  
  44.            new Container(  
  45.              margin: const EdgeInsets.only(top: 5.0),  
  46.              child: documentSnapshot.data['image_url'] != ''  
  47.                  ? InkWell(  
  48.                      child: new Container(  
  49.                        child: Image.network(  
  50.                          documentSnapshot.data['image_url'],  
  51.                          fit: BoxFit.fitWidth,  
  52.                        ),  
  53.                        height: 150,  
  54.                        width: 150.0,  
  55.                        color: Color.fromRGBO(0, 0, 0, 0.2),  
  56.                        padding: EdgeInsets.all(5),  
  57.                      ),  
  58.                      onTap: () {  
  59.                        Navigator.of(context).push(  
  60.                          MaterialPageRoute(  
  61.                            builder: (context) => GalleryPage(  
  62.                              imagePath: documentSnapshot.data['image_url'],  
  63.                            ),  
  64.                          ),  
  65.                        );  
  66.                      },  
  67.                    )  
  68.                  : new Text(documentSnapshot.data['text']),  
  69.            ),  
  70.          ],  
  71.        ),  
  72.      ),  
  73.      new Column(  
  74.        crossAxisAlignment: CrossAxisAlignment.end,  
  75.        children: <Widget>[  
  76.          new Container(  
  77.              margin: const EdgeInsets.only(left: 8.0),  
  78.              child: new CircleAvatar(  
  79.                backgroundImage:  
  80.                    new NetworkImage(documentSnapshot.data['profile_photo']),  
  81.              )),  
  82.        ],  
  83.      ),  
  84.    ];  
  85.  }  
  86.    
  87.  List<Widget> generateReceiverLayout(DocumentSnapshot documentSnapshot) {  
  88.    return <Widget>[  
  89.      new Column(  
  90.        crossAxisAlignment: CrossAxisAlignment.start,  
  91.        children: <Widget>[  
  92.          new Container(  
  93.              margin: const EdgeInsets.only(right: 8.0),  
  94.              child: new CircleAvatar(  
  95.                backgroundImage:  
  96.                    new NetworkImage(documentSnapshot.data['profile_photo']),  
  97.              )),  
  98.        ],  
  99.      ),  
  100.      new Expanded(  
  101.        child: new Column(  
  102.          crossAxisAlignment: CrossAxisAlignment.start,  
  103.          children: <Widget>[  
  104.            new Text(documentSnapshot.data['sender_name'],  
  105.                style: new TextStyle(  
  106.                    fontSize: 14.0,  
  107.                    color: Colors.black,  
  108.                    fontWeight: FontWeight.bold)),  
  109.            new Container(  
  110.              margin: const EdgeInsets.only(top: 5.0),  
  111.              child: documentSnapshot.data['image_url'] != ''  
  112.                  ? InkWell(  
  113.                      child: new Container(  
  114.                        child: Image.network(  
  115.                          documentSnapshot.data['image_url'],  
  116.                          fit: BoxFit.fitWidth,  
  117.                        ),  
  118.                        height: 150,  
  119.                        width: 150.0,  
  120.                        color: Color.fromRGBO(0, 0, 0, 0.2),  
  121.                        padding: EdgeInsets.all(5),  
  122.                      ),  
  123.                      onTap: () {  
  124.                        Navigator.of(context).push(  
  125.                          MaterialPageRoute(  
  126.                            builder: (context) => GalleryPage(  
  127.                              imagePath: documentSnapshot.data['image_url'],  
  128.                            ),  
  129.                          ),  
  130.                        );  
  131.                      },  
  132.                    )  
  133.                  : new Text(documentSnapshot.data['text']),  
  134.            ),  
  135.          ],  
  136.        ),  
  137.      ),  
  138.    ];  
  139.  }  
  140.    
  141.  generateMessages(AsyncSnapshot<QuerySnapshot> snapshot) {  
  142.    return snapshot.data.documents  
  143.        .map<Widget>((doc) => Container(  
  144.              margin: const EdgeInsets.symmetric(vertical: 10.0),  
  145.              child: new Row(  
  146.                children: doc.data['sender_id'] != widget.prefs.getString('uid')  
  147.                    ? generateReceiverLayout(doc)  
  148.                    : generateSenderLayout(doc),  
  149.              ),  
  150.            ))  
  151.        .toList();  
  152.  }  
  153.    
  154.  @override  
  155.  Widget build(BuildContext context) {  
  156.    return Scaffold(  
  157.      appBar: AppBar(  
  158.        title: Text(widget.title),  
  159.      ),  
  160.      body: Container(  
  161.        padding: EdgeInsets.all(5),  
  162.        child: new Column(  
  163.          children: <Widget>[  
  164.            StreamBuilder<QuerySnapshot>(  
  165.              stream: chatReference.orderBy('time',descending: true).snapshots(),  
  166.              builder: (BuildContext context,  
  167.                  AsyncSnapshot<QuerySnapshot> snapshot) {  
  168.                if (!snapshot.hasData) return new Text("No Chat");  
  169.                return Expanded(  
  170.                  child: new ListView(  
  171.                    reverse: true,  
  172.                    children: generateMessages(snapshot),  
  173.                  ),  
  174.                );  
  175.              },  
  176.            ),  
  177.            new Divider(height: 1.0),  
  178.            new Container(  
  179.              decoration: new BoxDecoration(color: Theme.of(context).cardColor),  
  180.              child: _buildTextComposer(),  
  181.            ),  
  182.            new Builder(builder: (BuildContext context) {  
  183.              return new Container(width: 0.0, height: 0.0);  
  184.            })  
  185.          ],  
  186.        ),  
  187.      ),  
  188.    );  
  189.  }  
  190.    
  191.  IconButton getDefaultSendButton() {  
  192.    return new IconButton(  
  193.      icon: new Icon(Icons.send),  
  194.      onPressed: _isWritting  
  195.          ? () => _sendText(_textController.text)  
  196.          : null,  
  197.    );  
  198.  }  
  199.    
  200.  Widget _buildTextComposer() {  
  201.    return new IconTheme(  
  202.        data: new IconThemeData(  
  203.          color: _isWritting  
  204.              ? Theme.of(context).accentColor  
  205.              : Theme.of(context).disabledColor,  
  206.        ),  
  207.        child: new Container(  
  208.          margin: const EdgeInsets.symmetric(horizontal: 8.0),  
  209.          child: new Row(  
  210.            children: <Widget>[  
  211.              new Container(  
  212.                margin: new EdgeInsets.symmetric(horizontal: 4.0),  
  213.                child: new IconButton(  
  214.                    icon: new Icon(  
  215.                      Icons.photo_camera,  
  216.                      color: Theme.of(context).accentColor,  
  217.                    ),  
  218.                    onPressed: () async {  
  219.                      var image = await ImagePicker.pickImage(  
  220.                          source: ImageSource.gallery);  
  221.                      int timestamp = new DateTime.now().millisecondsSinceEpoch;  
  222.                      StorageReference storageReference = FirebaseStorage  
  223.                          .instance  
  224.                          .ref()  
  225.                          .child('chats/img_' + timestamp.toString() + '.jpg');  
  226.                      StorageUploadTask uploadTask =  
  227.                          storageReference.putFile(image);  
  228.                      await uploadTask.onComplete;  
  229.                      String fileUrl = await storageReference.getDownloadURL();  
  230.                      _sendImage(messageText: null, imageUrl: fileUrl);  
  231.                    }),  
  232.              ),  
  233.              new Flexible(  
  234.                child: new TextField(  
  235.                  controller: _textController,  
  236.                  onChanged: (String messageText) {  
  237.                    setState(() {  
  238.                      _isWritting = messageText.length > 0;  
  239.                    });  
  240.                  },  
  241.                  onSubmitted: _sendText,  
  242.                  decoration:  
  243.                      new InputDecoration.collapsed(hintText: "Send a message"),  
  244.                ),  
  245.              ),  
  246.              new Container(  
  247.                margin: const EdgeInsets.symmetric(horizontal: 4.0),  
  248.                child: getDefaultSendButton(),  
  249.              ),  
  250.            ],  
  251.          ),  
  252.        ));  
  253.  }  
  254.    
  255.  Future<Null> _sendText(String text) async {  
  256.    _textController.clear();  
  257.    chatReference.add({  
  258.      'text': text,  
  259.      'sender_id': widget.prefs.getString('uid'),  
  260.      'sender_name': widget.prefs.getString('name'),  
  261.      'profile_photo': widget.prefs.getString('profile_photo'),  
  262.      'image_url''',  
  263.      'time': FieldValue.serverTimestamp(),  
  264.    }).then((documentReference) {  
  265.      setState(() {  
  266.        _isWritting = false;  
  267.      });  
  268.    }).catchError((e) {});  
  269.  }  
  270.    
  271.  void _sendImage({String messageText, String imageUrl}) {  
  272.    chatReference.add({  
  273.      'text': messageText,  
  274.      'sender_id': widget.prefs.getString('uid'),  
  275.      'sender_name': widget.prefs.getString('name'),  
  276.      'profile_photo': widget.prefs.getString('profile_photo'),  
  277.      'image_url': imageUrl,  
  278.      'time': FieldValue.serverTimestamp(),  
  279.    });  
  280.  }  
  281. }  
Step 6
 
Great -- you are done with chat app in Flutter using google firebase firestore. Please download our source code attached and run the code on device or emulator.
 
NOTE
PLEASE CHECK OUT GIT REPO FOR FULL SOURCE CODE. YOU NEED TO ADD YOUR google-services.json FILE IN ANDROID => APP FOLDER.
 

Possible Errors

  1. flutter barcode scan Failed to notify project evaluation listener. > java.lang.AbstractMethodError (no error message)
  2. Android dependency 'androidx.core:core' has different version for the compile (1.0.0) and runtime (1.0.1) classpath. You should manually set the same version via DependencyResolution
  3. import androidx.annotation.NonNull;

Solution

 
1 & 2. In android/build.grader change the version
classpath 'com.android.tools.build:gradle:3.3.1'
3. Put
android.useAndroidX=true
android.enableJetifier=true
In android/gradle.properties file
 

Conclusion

 
In this article we have learned how to create chat app in Flutter using Google Firebase.
Author
Parth Patel
271 7k 1.6m