在正文开始之前,我们先组织一下数据,所有Firebase实时数据库的数据都被存储为Json对象。我们可以将该数据库视为云托管Json树,该数据库与SQL数据库不同,没有任何表格或记录。当我们将数据添加至Json树时,它变为现有Json结构中的一个节点。
虽然Firebase实时数据库允许嵌套数据的深度多达32层,然而,当我们提取数据库中某个位置的数据时,也会检索所有子节点。另外,当我们向某用户授予数据库中某个节点的读写访问权时,也会将该节点下所有数据的访问权授予该用户。
在Firebase实时数据库中,数据结构最佳做法是平展数据结构,数据被拆分到不同路径(又称反规范化),可以根据需要通过不同调用有效地下载。即使列表含有成千上万条记录,也可单独提取和显示,从而确保UI 的及时响应和速度。
{
"users": {
"1380001": {
"email": "xingmi@163.com",
"name": "小明",
"password": "123456",
"phone": "1380001"
},
"1380002": {
"..": ".."
},
"1380003": {
"..": ".."
}
},
"chats": {
"1380001": {
"1380002": {
"name": "小红",
"phone": "1380002",
"messages": "13800011380002",
"lastMessage": "电路好像出问题了!",
"timestamp": 1459361875666,
"activate": "true"
},
"1380003": {
"name": "小刚",
"phone": "1380003",
"messages": "13800011380003",
"lastMessage": "发现问题,有人把地线和火线接反",
"timestamp": 1459361875666,
"activate": "false"
}
},
"1380002": {
"..": ".."
},
"1380003": {
"..": ".."
}
},
"messages": {
"13800011380002": {
"Ph6dARrtdEAUY5PDL2gt": {
"name": "小红",
"message": "电路好像出问题了!"
},
"x415NpxFZM2CJiBRMCcL": {
"..": ".."
}
},
"13800011380003": {
"..": ".."
},
"13800031380002": {
"..": ".."
}
}
}
在我们的应用程序中,用户个人资料位于“/users”路径中。现在有“/users/1380001”这个账户,它在“/chats/1380001”下检索所有的会话,再通过“/chats/1380001/messages”的值,在“/messages”中读取这个会话的所有聊天消息。
在上一篇文章中,我们实现了聊天列表屏幕的基本UI,在这一篇文本中,会具体实现添加会话的功能。具体实现则是当用户点击右下方的添加按钮时,会进入到添加会话屏幕,因此我们需要在/lib
目录下新建一个add_session.dart
文件,并添加以下代码。
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'dart:async';
import 'prompt_wait.dart';
class AddSession extends StatefulWidget {
AddSession(this.myPhone);
final String myPhone;
@override
State createState() => new _AddSessionState(myPhone);
}
class _AddSessionState extends State<AddSession> {
_AddSessionState(this._myPhone);
final String _myPhone;
@override
Widget build(BuildContext context) {
return new SimpleDialog(title: new Text("添加会话"), children: <Widget>[
new Text("这里用来放一些相关控件");
]);
}
}
在添加完add_session.dart
文件后,我们需要把聊天列表屏幕中的添加按钮与添加会话屏幕联系起来。修改group_chat_list.dart
文件,添加下面的代码,在用户点击按钮后,就会跳转到添加会话屏幕。这里传递了当前账户的手机号码给添加会话屏幕,使我们能访问对应账户的数据节点,以便添加或修改数据。
//...
import 'add_session.dart';
//...
class _GroupChatListState extends State<GroupChatList> {
//...
void _floatingButtonCallback() {
showDialog<Null>(
context: context, barrierDismissible: false, child: new AddSession(phone));
}
//...
Widget build(BuildContext context) {
//...
return new Scaffold(
//...
floatingActionButton: new FloatingActionButton(
backgroundColor: Colors.orange[800],
elevation: 0.0,
onPressed: _floatingButtonCallback,
child: new Icon(Icons.person_add)));
}
}
回到add_session.dart
文件中,在_AddSessionState
中添加一个_findUser
方法,用于查找用户是否存在,如果真实存在,则保存这个用户的手机号码和名称,用于给用户展示搜索结果。
class _AddSessionState extends State<AddSession> {
//...
final usersReference = FirebaseDatabase.instance.reference().child('users');
String _searchUsername = "";
String _searchPhone = "";
//...
Future<int> _findUser(String phone) async {
return await usersReference
.child(phone)
.once()
.then((DataSnapshot onValue) {
if (onValue.value != null) {
_searchUsername = onValue.value["name"];
_searchPhone = onValue.value["phone"];
return 1;
} else {
return 0;
}
});
}
//...
}
我们还需要一个处理用户点击搜索按钮的事件,在_AddSessionState
中添加一个_handleFind
方法,用于判断用户输入的手机号码是否格式正确,确定没有格式问题,再调用上面的_findUser
方法,在数据库中查找该手机号码的账户,最后根据返回结果做对应操作。
class _AddSessionState extends State<AddSession> {
//...
final TextEditingController _phoneController = new TextEditingController();
//...
void _handleFind() {
FocusScope.of(context).requestFocus(new FocusNode());
if (_phoneController.text.isEmpty) {
showMessage(context, "手机号码不能为空!");
return;
} else if (_phoneController.text.trim() == widget.myPhone) {
showMessage(context, "这是你的手机号码哦!");
return;
} else if (_phoneController.text.trim().length < 7 ||
_phoneController.text.trim().length > 12) {
showMessage(context, "手机号码的格式不正确!");
return;
}
showDialog<int>(
context: context,
barrierDismissible: false,
child: new ShowAwait(_findUser(_phoneController.text)))
.then((int onValue) {
if (onValue == 0) {
showMessage(context, "该用户不存在!");
} else if (onValue == 1) {
setState(() {});
}
});
}
//...
}
现在我们在build
方法中添加手机号码输入框与搜索按钮,并将搜索按钮的点击事件设置成上面的_handleFind
方法。用户一但点击搜索按钮,首先会判断手机号码格式,然后再搜索是否有该用户并保存用户信息。
class _AddSessionState extends State<AddSession> {
//...
@override
Widget build(BuildContext context) {
return new SimpleDialog(title: new Text("添加会话"), children: <Widget>[
new Container(
margin: const EdgeInsets.symmetric(horizontal: 23.0),
child: new Row(
children: <Widget>[
new Flexible(
child: new TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
decoration:
new InputDecoration.collapsed(hintText: '点击此处输入手机号码'),
)),
new IconButton(
icon: new Icon(Icons.search),
onPressed: () {
_handleFind();
}),
],
)),
]);
}
}
上面的代码中,如果手机号码为真实账户,我们会保存账户信息,所以我们还需要将搜索到的账户信息展示给用户。在build
方法中添加一个展示用户信息的自定义控件。
class _AddSessionState extends State<AddSession> {
//...
@override
Widget build(BuildContext context) {
return new SimpleDialog(title: new Text("添加会话"), children: <Widget>[
//...
_searchUsername == ""
? new Text("")
: new Container(
margin: const EdgeInsets.symmetric(horizontal: 23.0),
child: new Row(
children: <Widget>[
new CircleAvatar(
child: new Text(_searchUsername[0]),
backgroundColor: Theme.of(context).buttonColor),
new Flexible(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(
" " + _searchUsername,
textScaleFactor: 1.2,
overflow: TextOverflow.ellipsis,
),
new Text(" " + _searchPhone)
],
))
],
)),
]);
}
}
我们在_AddSessionState
中添加一个_addSession
方法,用于在“/chats/$user/”下添加一个会话记录。在添加之前,还需要判断是否已经存在该记录,如果已经存在,说明已经添加过这个会话了,这时我们再判断该条记录的activate
值,如果是true就说明现在用户的聊天列表中有该条会话,否则说明用户已经删除该会话。
class _AddSessionState extends State<AddSession> {
//...
final chatsReference = FirebaseDatabase.instance.reference().child('chats');
//...
Future<int> _addSession() async {
return await chatsReference
.child('$_myPhone/$_searchPhone')
.once()
.then((DataSnapshot onValue) {
if (onValue.value == null) {
chatsReference.child('$_myPhone/$_searchPhone').set({
"name": _searchUsername,
"phone": _searchPhone,
"messages": "$_myPhone$_searchPhone",
"lastMessage": "一起来聊天吧!",
"activate": "true"
});
return 1;
} else {
if (onValue.value["activate"] == true) {
print("跳转到对应的聊天窗口");
return 0;
} else {
print("移除以前的记录,创建一条新记录");
return 2;
}
}
});
}
//...
}
现在添加一个处理用户点击添加按钮的事件,在_AddSessionState
中添加一个_handleAppend
方法,用于获取_addSession
方法的返回值,并做对应的操作。这里关于添加的流程还没有完成,我们会在完成聊天列表屏幕之后再写这些内容。
class _AddSessionState extends State<AddSession> {
//...
void _handleAppend() {
showDialog<int>(
context: context,
barrierDismissible: false,
child: new ShowAwait(_addSession())).then((int onValue) {
if (onValue == 1) {
print("会话创建成功,返回到聊天列表屏幕");
}
});
}
//...
}
最后,我们在build
方法中增加一个取消按钮和添加按钮,其中添加按钮在用户没有搜索到好友之前为不可点击状态,搜索到好友之后才是可点击状态。
class _AddSessionState extends State<AddSession> {
//...
@override
Widget build(BuildContext context) {
return new SimpleDialog(title: new Text("添加会话"), children: <Widget>[
//...
new Container(
margin: const EdgeInsets.only(top: 18.0),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
new RaisedButton(
elevation: 0.0,
onPressed: () {
Navigator.of(context).pop();
},
colorBrightness: Brightness.dark,
child: const Text('取消'),
),
new RaisedButton(
elevation: 0.0,
onPressed: _searchUsername == "" ? null : _handleAppend,
colorBrightness:
_searchUsername == "" ? Brightness.light : Brightness.dark,
child: const Text('添加'),
),
],
))
]);
}
}
大家可以在GitHub上直接查看add_session.dart文件的代码。