Flutter Chat UI Tutorial | Apps From Scratch


Learn how to create a Chat App in Flutter using Google Firebase as backend

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. Firebase Phone Authentication in Flutter
  2. Firebase Database Structure in Chat App
  3. Pagination in Flutter using Firebase’s 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

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 post 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.

    dependencies:  
     flutter:  
       sdk: flutter  
     cupertino_icons: ^0.1.2  
     firebase_auth:  
     shared_preferences: ^0.5.3+1  
     cloud_firestore: ^0.12.7  
     contact_picker: ^0.0.2  
     image_picker: ^0.6.0+17  
     firebase_storage: ^3.0.3  
     photo_view: ^0.4.2  

Step 3

Now, we need to setup firebase project to provide authentication and storage feature. Following is the OTP Authentication (registration_page.dart) Programming Implementation.

    import 'package:flutter/material.dart';  
    import 'package:firebase_auth/firebase_auth.dart';  
    import 'package:flutter/services.dart';  
    import 'package:flutter_chat_app/pages/home_page.dart';  
    import 'package:shared_preferences/shared_preferences.dart';  
    import 'package:cloud_firestore/cloud_firestore.dart';  
       
    class RegistrationPage extends StatefulWidget {  
     final SharedPreferences prefs;  
     RegistrationPage({this.prefs});  
     @override  
     _RegistrationPageState createState() => _RegistrationPageState();  
    }  
       
    class _RegistrationPageState extends State<RegistrationPage> {  
     String phoneNo;  
     String smsOTP;  
     String verificationId;  
     String errorMessage = '';  
     FirebaseAuth _auth = FirebaseAuth.instance;  
     final db = Firestore.instance;  
       
     @override  
     initState() {  
       super.initState();  
     }  
       
     Future<void> verifyPhone() async {  
       final PhoneCodeSent smsOTPSent = (String verId, [int forceCodeResend]) {  
         this.verificationId = verId;  
         smsOTPDialog(context).then((value) {});  
       };  
       try {  
         await _auth.verifyPhoneNumber(  
             phoneNumber: this.phoneNo, // PHONE NUMBER TO SEND OTP  
             codeAutoRetrievalTimeout: (String verId) {  
               //Starts the phone number verification process for the given phone number.  
               //Either sends an SMS with a 6 digit code to the phone number specified, or sign's the user in and [verificationCompleted] is called.  
               this.verificationId = verId;  
             },  
             codeSent:  
                 smsOTPSent, // WHEN CODE SENT THEN WE OPEN DIALOG TO ENTER OTP.  
             timeout: const Duration(seconds: 20),  
             verificationCompleted: (AuthCredential phoneAuthCredential) {  
               print(phoneAuthCredential);  
             },  
             verificationFailed: (AuthException e) {  
               print('${e.message}');  
             });  
       } catch (e) {  
         handleError(e);  
       }  
     }  
       
     Future<bool> smsOTPDialog(BuildContext context) {  
       return showDialog(  
           context: context,  
           barrierDismissible: false,  
           builder: (BuildContext context) {  
             return new AlertDialog(  
               title: Text('Enter SMS Code'),  
               content: Container(  
                 height: 85,  
                 child: Column(children: [  
                   TextField(  
                     onChanged: (value) {  
                       this.smsOTP = value;  
                     },  
                   ),  
                   (errorMessage != ''  
                       ? Text(  
                           errorMessage,  
                           style: TextStyle(color: Colors.red),  
                         )  
                       : Container())  
                 ]),  
               ),  
               contentPadding: EdgeInsets.all(10),  
               actions: <Widget>[  
                 FlatButton(  
                   child: Text('Done'),  
                   onPressed: () {  
                     _auth.currentUser().then((user) async {  
                       signIn();  
                     });  
                   },  
                 )  
               ],  
             );  
           });  
     }  
       
     signIn() async {  
       try {  
         final AuthCredential credential = PhoneAuthProvider.getCredential(  
           verificationId: verificationId,  
           smsCode: smsOTP,  
         );  
         final FirebaseUser user = await _auth.signInWithCredential(credential);  
         final FirebaseUser currentUser = await _auth.currentUser();  
         assert(user.uid == currentUser.uid);  
         Navigator.of(context).pop();  
         DocumentReference mobileRef = db  
             .collection("mobiles")  
             .document(phoneNo.replaceAll(new RegExp(r'[^\w\s]+'), ''));  
         await mobileRef.get().then((documentReference) {  
           if (!documentReference.exists) {  
             mobileRef.setData({}).then((documentReference) async {  
               await db.collection("users").add({  
                 'name': "No Name",  
                 'mobile': phoneNo.replaceAll(new RegExp(r'[^\w\s]+'), ''),  
                 'profile_photo': "",  
               }).then((documentReference) {  
                 widget.prefs.setBool('is_verified', true);  
                 widget.prefs.setString(  
                   'mobile',  
                   phoneNo.replaceAll(new RegExp(r'[^\w\s]+'), ''),  
                 );  
                 widget.prefs.setString('uid', documentReference.documentID);  
                 widget.prefs.setString('name', "No Name");  
                 widget.prefs.setString('profile_photo', "");  
       
                 mobileRef.setData({'uid': documentReference.documentID}).then(  
                     (documentReference) async {  
                   Navigator.of(context).pushReplacement(MaterialPageRoute(  
                       builder: (context) => HomePage(prefs: widget.prefs)));  
                 }).catchError((e) {  
                   print(e);  
                 });  
               }).catchError((e) {  
                 print(e);  
               });  
             });  
           } else {  
             widget.prefs.setBool('is_verified', true);  
             widget.prefs.setString(  
               'mobile_number',  
               phoneNo.replaceAll(new RegExp(r'[^\w\s]+'), ''),  
             );  
             widget.prefs.setString('uid', documentReference["uid"]);  
             widget.prefs.setString('name', documentReference["name"]);  
             widget.prefs  
                 .setString('profile_photo', documentReference["profile_photo"]);  
       
             Navigator.of(context).pushReplacement(  
               MaterialPageRoute(  
                 builder: (context) => HomePage(prefs: widget.prefs),  
               ),  
             );  
           }  
         }).catchError((e) {});  
       } catch (e) {  
         handleError(e);  
       }  
     }  
       
     handleError(PlatformException error) {  
       switch (error.code) {  
         case 'ERROR_INVALID_VERIFICATION_CODE':  
           FocusScope.of(context).requestFocus(new FocusNode());  
           setState(() {  
             errorMessage = 'Invalid Code';  
           });  
           Navigator.of(context).pop();  
           smsOTPDialog(context).then((value) {});  
           break;  
         default:  
           setState(() {  
             errorMessage = error.message;  
           });  
       
           break;  
       }  
     }  
       
     @override  
     Widget build(BuildContext context) {  
       return Scaffold(  
         body: Center(  
           child: Column(  
             mainAxisAlignment: MainAxisAlignment.center,  
             children: <Widget>[  
               Padding(  
                 padding: EdgeInsets.all(10),  
                 child: TextField(  
                   decoration: InputDecoration(hintText: '+910000000000'),  
                   onChanged: (value) {  
                     this.phoneNo = value;  
                   },  
                 ),  
               ),  
               (errorMessage != ''  
                   ? Text(  
                       errorMessage,  
                       style: TextStyle(color: Colors.red),  
                     )  
                   : Container()),  
               SizedBox(  
                 height: 10,  
               ),  
               RaisedButton(  
                 onPressed: () {  
                   verifyPhone();  
                 },  
                 child: Text('Verify'),  
                 textColor: Colors.white,  
                 elevation: 7,  
                 color: Colors.blue,  
               )  
             ],  
           ),  
         ),  
       );  
     }  
    }  

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.


    openContacts() async {  
      Contact contact = await _contactPicker.selectContact();  
      if (contact != null) {  
        String phoneNumber = contact.phoneNumber.number  
            .toString()  
            .replaceAll(new RegExp(r"\s\b|\b\s"), "")  
            .replaceAll(new RegExp(r'[^\w\s]+'), '');  
        if (phoneNumber.length == 10) {  
          phoneNumber = '+91$phoneNumber';  
        }  
        if (phoneNumber.length == 12) {  
          phoneNumber = '+$phoneNumber';  
        }  
        if (phoneNumber.length == 13) {  
          DocumentReference mobileRef = db  
              .collection("mobiles")  
              .document(phoneNumber.replaceAll(new RegExp(r'[^\w\s]+'), ''));  
          await mobileRef.get().then((documentReference) {  
            if (documentReference.exists) {  
              contactsReference.add({  
                'uid': documentReference['uid'],  
                'name': contact.fullName,  
                'mobile': phoneNumber.replaceAll(new RegExp(r'[^\w\s]+'), ''),  
              });  
            } else {  
              print('User Not Registered');  
            }  
          }).catchError((e) {});  
        } else {  
          print('Wrong Mobile Number');  
        }  
      }  
    }  

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.


    import 'package:cloud_firestore/cloud_firestore.dart';  
    import 'package:firebase_storage/firebase_storage.dart';  
    import 'package:flutter/material.dart';  
    import 'package:flutter_chat_app/pages/gallary_page.dart';  
    import 'package:image_picker/image_picker.dart';  
    import 'package:shared_preferences/shared_preferences.dart';  
       
    class ChatPage extends StatefulWidget {  
     final SharedPreferences prefs;  
     final String chatId;  
     final String title;  
     ChatPage({this.prefs, this.chatId,this.title});  
     @override  
     ChatPageState createState() {  
       return new ChatPageState();  
     }  
    }  
       
    class ChatPageState extends State<ChatPage> {  
     final db = Firestore.instance;  
     CollectionReference chatReference;  
     final TextEditingController _textController =  
         new TextEditingController();  
     bool _isWritting = false;  
       
     @override  
     void initState() {  
       super.initState();  
       chatReference =  
           db.collection("chats").document(widget.chatId).collection('messages');  
     }  
       
     List<Widget> generateSenderLayout(DocumentSnapshot documentSnapshot) {  
       return <Widget>[  
         new Expanded(  
           child: new Column(  
             crossAxisAlignment: CrossAxisAlignment.end,  
             children: <Widget>[  
               new Text(documentSnapshot.data['sender_name'],  
                   style: new TextStyle(  
                       fontSize: 14.0,  
                       color: Colors.black,  
                       fontWeight: FontWeight.bold)),  
               new Container(  
                 margin: const EdgeInsets.only(top: 5.0),  
                 child: documentSnapshot.data['image_url'] != ''  
                     ? InkWell(  
                         child: new Container(  
                           child: Image.network(  
                             documentSnapshot.data['image_url'],  
                             fit: BoxFit.fitWidth,  
                           ),  
                           height: 150,  
                           width: 150.0,  
                           color: Color.fromRGBO(0, 0, 0, 0.2),  
                           padding: EdgeInsets.all(5),  
                         ),  
                         onTap: () {  
                           Navigator.of(context).push(  
                             MaterialPageRoute(  
                               builder: (context) => GalleryPage(  
                                 imagePath: documentSnapshot.data['image_url'],  
                               ),  
                             ),  
                           );  
                         },  
                       )  
                     : new Text(documentSnapshot.data['text']),  
               ),  
             ],  
           ),  
         ),  
         new Column(  
           crossAxisAlignment: CrossAxisAlignment.end,  
           children: <Widget>[  
             new Container(  
                 margin: const EdgeInsets.only(left: 8.0),  
                 child: new CircleAvatar(  
                   backgroundImage:  
                       new NetworkImage(documentSnapshot.data['profile_photo']),  
                 )),  
           ],  
         ),  
       ];  
     }  
       
     List<Widget> generateReceiverLayout(DocumentSnapshot documentSnapshot) {  
       return <Widget>[  
         new Column(  
           crossAxisAlignment: CrossAxisAlignment.start,  
           children: <Widget>[  
             new Container(  
                 margin: const EdgeInsets.only(right: 8.0),  
                 child: new CircleAvatar(  
                   backgroundImage:  
                       new NetworkImage(documentSnapshot.data['profile_photo']),  
                 )),  
           ],  
         ),  
         new Expanded(  
           child: new Column(  
             crossAxisAlignment: CrossAxisAlignment.start,  
             children: <Widget>[  
               new Text(documentSnapshot.data['sender_name'],  
                   style: new TextStyle(  
                       fontSize: 14.0,  
                       color: Colors.black,  
                       fontWeight: FontWeight.bold)),  
               new Container(  
                 margin: const EdgeInsets.only(top: 5.0),  
                 child: documentSnapshot.data['image_url'] != ''  
                     ? InkWell(  
                         child: new Container(  
                           child: Image.network(  
                             documentSnapshot.data['image_url'],  
                             fit: BoxFit.fitWidth,  
                           ),  
                           height: 150,  
                           width: 150.0,  
                           color: Color.fromRGBO(0, 0, 0, 0.2),  
                           padding: EdgeInsets.all(5),  
                         ),  
                         onTap: () {  
                           Navigator.of(context).push(  
                             MaterialPageRoute(  
                               builder: (context) => GalleryPage(  
                                 imagePath: documentSnapshot.data['image_url'],  
                               ),  
                             ),  
                           );  
                         },  
                       )  
                     : new Text(documentSnapshot.data['text']),  
               ),  
             ],  
           ),  
         ),  
       ];  
     }  
       
     generateMessages(AsyncSnapshot<QuerySnapshot> snapshot) {  
       return snapshot.data.documents  
           .map<Widget>((doc) => Container(  
                 margin: const EdgeInsets.symmetric(vertical: 10.0),  
                 child: new Row(  
                   children: doc.data['sender_id'] != widget.prefs.getString('uid')  
                       ? generateReceiverLayout(doc)  
                       : generateSenderLayout(doc),  
                 ),  
               ))  
           .toList();  
     }  
       
     @override  
     Widget build(BuildContext context) {  
       return Scaffold(  
         appBar: AppBar(  
           title: Text(widget.title),  
         ),  
         body: Container(  
           padding: EdgeInsets.all(5),  
           child: new Column(  
             children: <Widget>[  
               StreamBuilder<QuerySnapshot>(  
                 stream: chatReference.orderBy('time',descending: true).snapshots(),  
                 builder: (BuildContext context,  
                     AsyncSnapshot<QuerySnapshot> snapshot) {  
                   if (!snapshot.hasData) return new Text("No Chat");  
                   return Expanded(  
                     child: new ListView(  
                       reverse: true,  
                       children: generateMessages(snapshot),  
                     ),  
                   );  
                 },  
               ),  
               new Divider(height: 1.0),  
               new Container(  
                 decoration: new BoxDecoration(color: Theme.of(context).cardColor),  
                 child: _buildTextComposer(),  
               ),  
               new Builder(builder: (BuildContext context) {  
                 return new Container(width: 0.0, height: 0.0);  
               })  
             ],  
           ),  
         ),  
       );  
     }  
       
     IconButton getDefaultSendButton() {  
       return new IconButton(  
         icon: new Icon(Icons.send),  
         onPressed: _isWritting  
             ? () => _sendText(_textController.text)  
             : null,  
       );  
     }  
       
     Widget _buildTextComposer() {  
       return new IconTheme(  
           data: new IconThemeData(  
             color: _isWritting  
                 ? Theme.of(context).accentColor  
                 : Theme.of(context).disabledColor,  
           ),  
           child: new Container(  
             margin: const EdgeInsets.symmetric(horizontal: 8.0),  
             child: new Row(  
               children: <Widget>[  
                 new Container(  
                   margin: new EdgeInsets.symmetric(horizontal: 4.0),  
                   child: new IconButton(  
                       icon: new Icon(  
                         Icons.photo_camera,  
                         color: Theme.of(context).accentColor,  
                       ),  
                       onPressed: () async {  
                         var image = await ImagePicker.pickImage(  
                             source: ImageSource.gallery);  
                         int timestamp = new DateTime.now().millisecondsSinceEpoch;  
                         StorageReference storageReference = FirebaseStorage  
                             .instance  
                             .ref()  
                             .child('chats/img_' + timestamp.toString() + '.jpg');  
                         StorageUploadTask uploadTask =  
                             storageReference.putFile(image);  
                         await uploadTask.onComplete;  
                         String fileUrl = await storageReference.getDownloadURL();  
                         _sendImage(messageText: null, imageUrl: fileUrl);  
                       }),  
                 ),  
                 new Flexible(  
                   child: new TextField(  
                     controller: _textController,  
                     onChanged: (String messageText) {  
                       setState(() {  
                         _isWritting = messageText.length > 0;  
                       });  
                     },  
                     onSubmitted: _sendText,  
                     decoration:  
                         new InputDecoration.collapsed(hintText: "Send a message"),  
                   ),  
                 ),  
                 new Container(  
                   margin: const EdgeInsets.symmetric(horizontal: 4.0),  
                   child: getDefaultSendButton(),  
                 ),  
               ],  
             ),  
           ));  
     }  
       
     Future<Null> _sendText(String text) async {  
       _textController.clear();  
       chatReference.add({  
         'text': text,  
         'sender_id': widget.prefs.getString('uid'),  
         'sender_name': widget.prefs.getString('name'),  
         'profile_photo': widget.prefs.getString('profile_photo'),  
         'image_url': '',  
         'time': FieldValue.serverTimestamp(),  
       }).then((documentReference) {  
         setState(() {  
           _isWritting = false;  
         });  
       }).catchError((e) {});  
     }  
       
     void _sendImage({String messageText, String imageUrl}) {  
       chatReference.add({  
         'text': messageText,  
         'sender_id': widget.prefs.getString('uid'),  
         'sender_name': widget.prefs.getString('name'),  
         'profile_photo': widget.prefs.getString('profile_photo'),  
         'image_url': imageUrl,  
         'time': FieldValue.serverTimestamp(),  
       });  
     }  
    }  

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’

  1. 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.


Build A Chat Application from Scratch (Part-1) | Design Chat List Page

Build A Chat Application from Scratch (Part-2) | Design Chat Item Page

Source Code: https://github.com/robbyrahmana/app_chat


Flutter Chat Application with Cloud Firestore and Firebase Authentication


Build a Real-time Flutter Chat Application using Stream

#flutter #firebase #mobile-apps

How to create Chat App in Flutter using Firebase
467.10 GEEK