在 Flutter 中创建聊天应用程序(18)
介绍
在本文中,我们将学习如何使用 Google Firebase 作为后端在 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 文件。
可能的错误
- flutter 条码扫描无法通知项目评估监听器。> java.lang.AbstractMethodError(无错误提示)
- Android 依赖项“androidx.core:core”在编译 (1.0.0) 和运行时 (1.0.1) 类路径中具有不同的版本。您应该通过 DependencyResolution 手动设置相同的版本
- 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 中创建聊天应用程序。
常见问题FAQ
- 程序仅供学习研究,请勿用于非法用途,不得违反国家法律,否则后果自负,一切法律责任与本站无关。
- 请仔细阅读以上条款再购买,拍下即代表同意条款并遵守约定,谢谢大家支持理解!