在 Flutter 中创建聊天应用程序(18)

作者 : 慕源网 本文共12763个字,预计阅读时间需要32分钟 发布时间: 2021-11-16 共440人阅读

介绍

在本文中,我们将学习如何使用 Google Firebase 作为后端在 Flutter 中创建聊天应用程序。本文由多篇文章组成,您将在其中学习——

  1. Flutter 中的 OTP 身份验证
  2. Firebase Firestore 中的聊天应用数据结构
  3. 使用 Firebase Cloud Firestore 在 Flutter 中进行分页

我已将聊天应用系列分成多篇文章。在这个 Flutter 聊天应用系列中,您将学到很多关于 Flutter 的东西。所以,让我们开始我们的应用程序。

 

输出

需要插件

  • firebase_auth: // 用于 firebase otp 身份验证
  • shared_preferences: ^0.5.3+1 // 用于存储用户凭证持久化
  • cloud_firestore: ^0.12.7 // 访问 firebase 实时数据库
  • contact_picker: ^0.0.2 // 从联系人列表中添加好友
  • image_picker: ^0.6.0+17 // 从设备中选择图像
  • firebase_storage: ^3.0.3 // 要将图像发送给用户,我们需要将图像存储在服务器上
  • photo_view: ^0.4.2 // 在扩展视图中查看发送和接收的图像

编程步骤

第1步

第一步也是最基本的步骤是在 Flutter 中创建一个新应用程序。如果你是 Flutter 初学者,可以查看我的博客Create a first app in Flutter。我创建了一个名为“flutter_chat_app”的应用程序。

第2步

 

打开项目中的 pubspec.yaml 文件并将以下依赖项添加到其中。

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

第 3 步

现在,我们需要设置 firebase 项目以提供身份验证和存储功能。我在下面放置了重要的实现,但您可以学习Flutter 中的 OTP 身份验证全文,Firebase Firestore 中的聊天应用程序数据结构。以下是 OTP 身份验证 (registration_page.dart) 编程实现。

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,  
           )  
         ],  
       ),  
     ),  
   );  
 }  
} 

第4步

现在,我们将实现从联系人列表中添加好友。以下是从设备访问联系人并将其添加为好友聊天的编程实现。

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');  
    }  
  }  
} 

第 5 步

现在,我们将实现聊天屏幕,用户将在其中向朋友发送文本和图像消息,反之亦然。以下是为此的编程实现。chat_page.dart。本页介绍了分页和图像上传。

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(),  
   });  
 }  
}  

第 6 步

太棒了——你已经在 Flutter 中使用 google firebase firestore 完成了聊天应用程序。请下载我们附带的源代码并在设备或模拟器上运行代码。

笔记
请查看 Git 仓库以获得完整的源代码。您需要在 ANDROID => 应用程序文件夹中添加您的 google-services.json 文件。

可能的错误

  1. flutter 条码扫描无法通知项目评估监听器。> java.lang.AbstractMethodError(无错误提示)
  2. Android 依赖项“androidx.core:core”在编译 (1.0.0) 和运行时 (1.0.1) 类路径中具有不同的版本。您应该通过 DependencyResolution 手动设置相同的版本
  3. import androidx.annotation.NonNull;

解决方案

1 & 2. 在 android/build.grader 中更改版本
类路径 ‘com.android.tools.build:gradle:3.3.1’
3. 设置
在 android/gradle.properties 文件中设置
android.useAndroidX=true
android.enableJetifier=true

结论

在本文中,我们学习了如何使用 Google Firebase 在 Flutter 中创建聊天应用程序。


慕源网 » 在 Flutter 中创建聊天应用程序(18)

常见问题FAQ

程序仅供学习研究,请勿用于非法用途,不得违反国家法律,否则后果自负,一切法律责任与本站无关。
请仔细阅读以上条款再购买,拍下即代表同意条款并遵守约定,谢谢大家支持理解!

发表评论

开通VIP 享更多特权,建议使用QQ登录